This file provides guidance for AI coding assistants working with the Craft codebase.
- Always use
pnpmfor package management. Never usenpmoryarn. - Node.js version is managed by Volta (currently v22.12.0).
- Install dependencies with
pnpm install --frozen-lockfile.
| Command | Description |
|---|---|
pnpm build |
Build the project (outputs to dist/craft) |
pnpm test |
Run tests |
pnpm lint |
Run ESLint |
pnpm fix |
Auto-fix lint issues |
To manually test changes:
pnpm build && ./dist/craft- TypeScript is used throughout the codebase.
- Prettier 3.x with single quotes and no arrow parens (configured in
.prettierrc.yml). - ESLint 9.x with flat config (
eslint.config.mjs) usingtypescript-eslint. - Unused variables prefixed with
_are allowed (e.g.,_unusedParam).
src/
├── __mocks__/ # Test mocks
├── __tests__/ # Test files (*.test.ts)
├── artifact_providers/ # Artifact provider implementations
├── commands/ # CLI command implementations
├── schemas/ # Zod schemas and TypeScript types for config
├── status_providers/ # Status provider implementations
├── targets/ # Release target implementations
├── types/ # Shared TypeScript types
├── utils/ # Utility functions
├── config.ts # Configuration loading
├── index.ts # CLI entry point
└── logger.ts # Logging utilities
dist/
└── craft # Single bundled executable (esbuild output)
- Tests use Vitest.
- Test files are located in
src/__tests__/and follow the*.test.tsnaming pattern. - Run tests with
pnpm test. - Use
vi.fn(),vi.mock(),vi.spyOn()for mocking (Vitest's mock API).
- Main branch is
master. - CI runs tests on Node.js 20 and 22.
- Craft releases itself using its own tooling (dogfooding).
- Project configuration lives in
.craft.ymlat the repository root. - The configuration schema is defined in
src/schemas/.
Craft supports a --dry-run flag that prevents destructive operations. This is implemented via a centralized abstraction layer.
Instead of checking isDryRun() manually in every function, destructive operations are wrapped with dry-run-aware proxies:
- Git operations: Use
getGitClient()fromsrc/utils/git.tsorcreateGitClient(directory)for working with specific directories - GitHub API: Use
getGitHubClient()fromsrc/utils/githubApi.ts - File writes: Use
safeFsfromsrc/utils/dryRun.ts - Other actions: Use
safeExec()orsafeExecSync()fromsrc/utils/dryRun.ts
ESLint rules prevent direct usage of raw APIs:
no-restricted-imports: Blocks directsimple-gitimportsno-restricted-syntax: Blocksnew Octokit()instantiation
If you're writing a wrapper module that needs raw access, use:
// eslint-disable-next-line no-restricted-imports -- This is the wrapper module
import simpleGit from 'simple-git';When adding new code that performs destructive operations:
- Git: Get the git client via
getGitClient()orcreateGitClient()- mutating methods are automatically blocked - GitHub API: Get the client via
getGitHubClient()-create*,update*,delete*,upload*methods are automatically blocked - File writes: Use
safeFs.writeFile(),safeFs.unlink(), etc. instead of rawfsmethods - Other: Wrap with
safeExec(action, description)for custom operations
Some operations need explicit isDryRun() checks:
- Commands with their own
--dry-runflag (e.g.,dart pub publish --dry-runin pubDev target) - Operations that need to return mock data in dry-run mode
- User experience optimizations (e.g., skipping sleep timers)
- 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).
- 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 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."
- 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.
- 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.
- 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.
- 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).
- 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.
- 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()`.