Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 21 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,49 +112,42 @@ Some operations need explicit `isDryRun()` checks:
- Operations that need to return mock data in dry-run mode
- User experience optimizations (e.g., skipping sleep timers)

<!-- This section is auto-maintained by lore (https://github.com/BYK/opencode-lore) -->

<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/opencode-lore) -->
## Long-term Knowledge

### Architecture

<!-- lore:019c9be1-33d8-7edb-8e74-95d7369f4abb -->
<!-- lore:019c9f57-aa0f-70b2-82fb-e87fb9fc591f -->
* **Craft npm target auth: temp .npmrc via npm\_config\_userconfig bypasses all default config**: Craft's npm target creates a temporary \`.npmrc\` file containing \`//registry.npmjs.org/:\_authToken=${NPM\_TOKEN}\` and sets the \`npm\_config\_userconfig\` env var to point to it. This completely overrides npm's default config file lookup chain — the user's home \`.npmrc\` and project \`.npmrc\` are both bypassed. This is why OIDC (which relies on \`setup-node\` creating a properly configured project \`.npmrc\`) requires a separate code path that skips the temp file entirely. The pattern is used in both \`publishPackage()\` and \`getLatestVersion()\`. The \`npm\_config\_userconfig\` approach (instead of \`--userconfig\` CLI flag) was chosen for yarn compatibility.

- **Craft tsconfig.build.json is now self-contained — no @sentry/typescript base**: The \`@sentry/typescript\` package was removed as a dev dependency. It only provided a base \`tsconfig.json\` with strict TS settings, but dragged in deprecated \`tslint\` and vulnerable \`minimatch@3.1.2\`. All useful compiler options from its tsconfig are now inlined directly in \`tsconfig.build.json\`. Key settings carried forward: \`declaration\`, \`declarationMap\`, \`downlevelIteration\`, \`inlineSources\`, \`noFallthroughCasesInSwitch\`, \`noImplicitAny\`, \`noImplicitReturns\`, \`noUnusedLocals\`, \`noUnusedParameters\`, \`pretty\`, \`sourceMap\`, \`strict\`. The chain is: \`tsconfig.json\` extends \`tsconfig.build.json\` (no further extends).
<!-- lore:019c9be1-33d8-7edb-8e74-95d7369f4abb -->
* **Craft tsconfig.build.json is now self-contained — no @sentry/typescript base**: The \`@sentry/typescript\` package was removed as a dev dependency. It only provided a base \`tsconfig.json\` with strict TS settings, but dragged in deprecated \`tslint\` and vulnerable \`minimatch@3.1.2\`. All useful compiler options from its tsconfig are now inlined directly in \`tsconfig.build.json\`. Key settings carried forward: \`declaration\`, \`declarationMap\`, \`downlevelIteration\`, \`inlineSources\`, \`noFallthroughCasesInSwitch\`, \`noImplicitAny\`, \`noImplicitReturns\`, \`noUnusedLocals\`, \`noUnusedParameters\`, \`pretty\`, \`sourceMap\`, \`strict\`. The chain is: \`tsconfig.json\` extends \`tsconfig.build.json\` (no further extends).

### Gotcha

<!-- lore:019c9eb7-a648-70f7-8a8d-5fc5b5c0f221 -->
<!-- lore:019c9f57-aa0c-7a2a-8a10-911b13b48fc0 -->
* **ESM modules prevent vi.spyOn of child\_process.spawnSync — use test subclass pattern**: In ESM (Vitest or Bun), you cannot \`vi.spyOn\` exports from Node built-in modules — throws 'Module namespace is not configurable'. Workaround: create a test subclass that overrides the method calling the built-in and injects controllable values. \`vi.mock\` at module level works but affects all tests in the file.

<!-- lore:019c9ee7-f55f-7697-b9b4-e7b9c93e9858 -->
* **Lore tool seeds generic entries unrelated to the project — clean before committing**: The opencode-lore tool (https://github.com/BYK/opencode-lore) can seed AGENTS.md with generic/template lore entries that are unrelated to the actual project. These are identifiable by: (1) shared UUID prefix like \`019c9aa1-\*\` suggesting batch creation, (2) content referencing technologies not in the codebase (e.g., React useState, Kubernetes helm charts, TypeScript strict mode boilerplate in a Node CLI project). These mislead AI assistants about the project's tech stack. Always review lore-managed sections in AGENTS.md before committing and remove entries that don't apply to the actual codebase. Cursor BugBot will flag these as "Irrelevant lore entries."

- **git stash pop after merge can cause second conflict on same file**: When you stash local changes, merge, then \`git stash pop\`, the stash apply can create a NEW conflict on the same file that was just conflict-resolved in the merge. This happens because the stash was based on the pre-merge state and conflicts with the post-merge-resolution content. The resolution requires a second manual conflict resolution pass. To avoid: if stashed changes are lore/auto-generated content, consider just dropping the stash and re-running the generation tool after merge instead of popping.
<!-- lore:019c9eb7-a640-776a-929d-89120f81733a -->
- **pnpm-lock.yaml merge conflicts: regenerate don't manually merge**: When \`pnpm-lock.yaml\` has merge conflicts, never try to manually resolve the conflict markers. Instead: (1) \`git checkout --theirs pnpm-lock.yaml\` (or \`--ours\` depending on which package.json changes you want as base), (2) run \`pnpm install\` to regenerate the lockfile incorporating both sides' \`package.json\` changes (including overrides). This produces a clean lockfile that reflects the merged dependency state. Manual conflict resolution in lockfiles is error-prone and unnecessary since pnpm can regenerate it deterministically.
<!-- lore:019c9e9c-fa8f-7ab2-b26e-d47e50cb04bb -->
- **marked-terminal unconditionally imports cli-highlight and node-emoji — no tree-shaking possible**: marked-terminal has static top-level imports of \`cli-highlight\` (which pulls in highlight.js, ~570KB minified output) and \`node-emoji\` (which pulls in emojilib, ~208KB minified output) at lines 5-6 of its index.js. These are unconditional — there's no config option to disable them, and the \`emoji: false\` option only skips the emoji replacement function but doesn't prevent the import. esbuild cannot tree-shake static imports. This means any bundle including marked-terminal will grow by ~970KB (highlight.js + emojilib + parse5). To avoid this in a CLI bundle, you'd need to either: (1) write a custom marked renderer using only chalk, (2) fork marked-terminal with dynamic imports, or (3) use esbuild's \`external\` option (but then those packages must be available at runtime).
<!-- lore:019c9be1-33db-7bba-bb0a-297d5de6edb7 -->
- **prepare-dry-run e2e tests require EDITOR env var for git commit**: The 6 tests in \`src/\_\_tests\_\_/prepare-dry-run.e2e.test.ts\` fail in environments where \`EDITOR\` is unset and the terminal is non-interactive (e.g., headless CI agents, worktrees). The error is \`Terminal is dumb, but EDITOR unset\` from git refusing to commit without a message editor. These are environment-dependent failures, not code bugs. They pass in environments with \`EDITOR=vi\` or similar set.
<!-- lore:019c9be1-33d1-7b6e-b107-ae7ad42a4ea4 -->
- **pnpm overrides with >= can cross major versions — use ^ to constrain**: When using pnpm overrides to patch a transitive dependency vulnerability, \`"ajv@<6.14.0": ">=6.14.0"\` will resolve to the latest ajv (v8.x), not the latest 6.x. ajv v6 and v8 have incompatible APIs — this broke eslint (\`@eslint/eslintrc\` calls \`ajv\` v6 API, crashes with \`Cannot set properties of undefined (setting 'defaultMeta')\` on v8). Fix: use \`"ajv@<6.14.0": "^6.14.0"\` to constrain within the same major. This applies to any override where the target package has multiple major versions in the registry — always use \`^\` (or \`~\`) instead of \`>=\` to stay within the compatible major line.
<!-- lore:019c9be1-33ca-714e-8ad9-dfda5350a106 -->
- **pnpm overrides with version-range keys don't force upgrades of already-compatible resolutions**: pnpm overrides with version-range selectors like \`"minimatch@>=10.0.0 <10.2.1": ">=10.2.1"\` do NOT work as expected for forcing upgrades of transitive deps that already satisfy their parent's semver range. If a parent requests \`^10.1.1\` and pnpm resolves \`10.1.1\`, the override key \`>=10.0.0 <10.2.1\` should match but doesn't reliably force re-resolution — even with \`pnpm install --force\`. The workaround is a blanket override without a version selector: \`"minimatch": ">=10.2.1"\`. This is only safe when ALL consumers are on the same major version line (otherwise it's a breaking change). Verify first with \`pnpm why \<pkg>\` that no other major versions exist in the tree before using a blanket override.
<!-- lore:019c9ba5-5158-77df-b32d-08980d0753c4 -->
- **git notes are lost on commit amend — must re-attach to new SHA**: Git notes are attached to a specific commit SHA. When you \`git commit --amend\`, the old commit is replaced with a new one (different SHA), and the note attached to the old SHA becomes orphaned. After amending, you must re-add the note to the new commit with \`git notes add\` targeting the new SHA. This also affects \`git push --force\` of notes refs — the remote note ref still points to the old SHA.
* **pnpm overrides with >= can cross major versions — use ^ to constrain**: pnpm overrides gotchas: (1) \`>=\` crosses major versions — use \`^\` to constrain within same major. (2) Version-range selectors don't reliably force re-resolution of compatible transitive deps; use blanket overrides when safe. (3) Overrides become stale — audit with \`pnpm why \<pkg>\` after dependency changes. (4) Never manually resolve pnpm-lock.yaml conflicts — \`git checkout --theirs\` then \`pnpm install\` to regenerate deterministically.

### Pattern
<!-- lore:019c9be1-33db-7bba-bb0a-297d5de6edb7 -->
* **prepare-dry-run e2e tests require EDITOR env var for git commit**: The 6 tests in \`src/\_\_tests\_\_/prepare-dry-run.e2e.test.ts\` fail in environments where \`EDITOR\` is unset and the terminal is non-interactive (e.g., headless CI agents, worktrees). The error is \`Terminal is dumb, but EDITOR unset\` from git refusing to commit without a message editor. These are environment-dependent failures, not code bugs. They pass in environments with \`EDITOR=vi\` or similar set.

<!-- lore:019c9eb7-a633-78aa-aaeb-8ddca3719975 -->
### Pattern

- **Craft publish_repo 'self' sentinel resolves to GITHUB_REPOSITORY at runtime**: The Craft composite action's \`publish_repo\` input supports a special sentinel value \`"self"\` which resolves to \`$GITHUB_REPOSITORY\` at runtime in the bash script of the 'Request publish' step. This allows repos to create publish request issues in themselves rather than in a separate \`{owner}/publish\` repo. The resolution happens in bash (not in the GitHub Actions expression) because the expression layer sets \`PUBLISH_REPO\` via \`inputs.publish_repo || format('{0}/publish', github.repository_owner)\` — the string \`"self"\` passes through as-is and gets resolved to the actual repo name in the shell. Useful for personal/small repos where the default GITHUB_TOKEN already has write access to the repo itself.
<!-- lore:019c9e9c-fa91-758c-8be9-f8ddb4e46eb5 -->
- **esbuild metafile output bytes vs input bytes — use output for real size impact**: When analyzing bundle size with esbuild's metafile, \`result.metafile.inputs\` shows raw source file sizes BEFORE minification and tree-shaking — these are misleading for size impact analysis. A 3.3MB input file may contribute 0 bytes to output if tree-shaken. Use \`result.metafile.outputs\[outfile].inputs\` to see actual per-file output contribution after minification. To dump metafile: add \`import { writeFileSync } from 'node:fs'; writeFileSync('/tmp/meta.json', JSON.stringify(result.metafile));\` after the build call, then analyze with \`jq\`. The bundle script at script/bundle.ts generates metafile but doesn't write it to disk by default.
<!-- lore:019c9bb9-a79b-71e0-9f71-d94e77119b4b -->
- **CLI UX: auto-correct common user mistakes with stderr warnings instead of hard errors**: When a CLI command can unambiguously detect a common user mistake (like using the wrong separator character), prefer auto-correcting the input and printing a warning to stderr over throwing a hard error. This is safe when: (1) the input is already invalid and would fail anyway, (2) there's no ambiguity in the correction, and (3) the warning goes to stderr so it doesn't interfere with JSON/stdout output. Implementation pattern: normalize inputs at the command level before passing to pure parsing functions, keeping the parsers side-effect-free. The \`gh\` CLI (GitHub CLI) is the UX model — match its conventions.
* **CLI UX: auto-correct common user mistakes with stderr warnings instead of hard errors**: When a CLI command can unambiguously detect a user mistake (e.g., wrong separator character), auto-correct and print a warning to stderr instead of a hard error. Safe when: input would fail anyway, no ambiguity, warning goes to stderr. Normalize at command level, keep parsers pure. Model after \`gh\` CLI conventions.

### Preference
<!-- lore:019c9f57-aa11-74f6-9532-7c8a45fe12a5 -->
* **Craft npm target OIDC detection via CI environment variables**: The \`isOidcEnvironment()\` helper in \`src/targets/npm.ts\` detects OIDC capability by checking CI-specific env vars that npm itself uses for OIDC token exchange: - \*\*GitHub Actions:\*\* \`ACTIONS\_ID\_TOKEN\_REQUEST\_URL\` AND \`ACTIONS\_ID\_TOKEN\_REQUEST\_TOKEN\` (both present when \`id-token: write\` permission is set) - \*\*GitLab CI/CD:\*\* \`NPM\_ID\_TOKEN\` (present when \`id\_tokens\` with \`aud: "npm:registry.npmjs.org"\` is configured) This auto-detection means zero config changes for the common case. The explicit \`oidc: true\` config is only needed to force OIDC when \`NPM\_TOKEN\` is also set (e.g., migration period).

<!-- lore:019c9aa1-f7a2-7c42-b067-a87eff21df63 -->
<!-- lore:019c9eb7-a633-78aa-aaeb-8ddca3719975 -->
* **Craft publish\_repo 'self' sentinel resolves to GITHUB\_REPOSITORY at runtime**: The Craft composite action's \`publish\_repo\` input supports a special sentinel value \`"self"\` which resolves to \`$GITHUB\_REPOSITORY\` at runtime in the bash script of the 'Request publish' step. This allows repos to create publish request issues in themselves rather than in a separate \`{owner}/publish\` repo. The resolution happens in bash (not in the GitHub Actions expression) because the expression layer sets \`PUBLISH\_REPO\` via \`inputs.publish\_repo || format('{0}/publish', github.repository\_owner)\` — the string \`"self"\` passes through as-is and gets resolved to the actual repo name in the shell. Useful for personal/small repos where the default GITHUB\_TOKEN already has write access to the repo itself.

- **General coding preference**: Prefer explicit error handling over silent failures
<!-- lore:019c9aa1-f75c-7cf4-921e-cc1d5fdccbe7 -->
- **Code style**: User prefers no backwards-compat shims, fix callers directly
<!-- lore:019c9fa3-fcfe-7b07-8351-90944df38ca0 -->
* **Craft uses home-grown SemVer utils — don't add semver package for version comparisons**: Despite \`semver\` being a dependency (used in \`src/utils/autoVersion.ts\` for \`semver.inc()\`), the codebase has its own \`SemVer\` interface and utilities in \`src/utils/version.ts\`: \`parseVersion()\`, \`versionGreaterOrEqualThan()\`, \`isPreviewRelease()\`, etc. These are used throughout the codebase (npm target, publish tag logic, etc.). When adding version comparison logic, use these existing utilities rather than introducing new custom comparison functions or reaching for the \`semver\` package. Example: the OIDC minimum npm version check was initially implemented with 3 separate constants and a custom comparison helper, then refactored to a single \`SemVer\` constant + \`versionGreaterOrEqualThan()\`.
<!-- End lore-managed section -->
4 changes: 3 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"pnpm": {
"overrides": {
"h3": "^1.15.5",
"devalue": "^5.6.3"
"devalue": "^5.6.3",
"rollup": "^4.59.0",
"svgo": "^4.0.1"
}
}
}
Loading
Loading