get-tbd 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +653 -229
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +644 -232
- package/dist/cli.mjs.map +1 -1
- package/dist/{config-BJz1m9eN.mjs → config-1ouUTKQr.mjs} +15 -4
- package/dist/config-1ouUTKQr.mjs.map +1 -0
- package/dist/{config-DlCUMyCG.mjs → config-YRRW9l89.mjs} +1 -1
- package/dist/docs/SKILL.md +8 -1
- package/dist/docs/guidelines/bun-monorepo-patterns.md +65 -66
- package/dist/docs/guidelines/cli-agent-skill-patterns.md +415 -169
- package/dist/docs/guidelines/common-doc-guidelines.md +2 -2
- package/dist/docs/guidelines/convex-limits-best-practices.md +39 -39
- package/dist/docs/guidelines/convex-rules.md +13 -13
- package/dist/docs/guidelines/electron-app-development-patterns.md +18 -18
- package/dist/docs/guidelines/error-handling-rules.md +3 -0
- package/dist/docs/guidelines/general-coding-rules.md +2 -1
- package/dist/docs/guidelines/general-comment-rules.md +3 -2
- package/dist/docs/guidelines/general-eng-agent-principles.md +126 -0
- package/dist/docs/guidelines/general-tdd-guidelines.md +10 -4
- package/dist/docs/guidelines/general-testing-rules.md +4 -0
- package/dist/docs/guidelines/golden-testing-guidelines.md +9 -9
- package/dist/docs/guidelines/pnpm-monorepo-patterns.md +49 -49
- package/dist/docs/guidelines/python-cli-patterns.md +5 -1
- package/dist/docs/guidelines/python-modern-guidelines.md +7 -4
- package/dist/docs/guidelines/python-rules.md +6 -0
- package/dist/docs/guidelines/release-notes-guidelines.md +18 -2
- package/dist/docs/guidelines/supply-chain-hardening.md +84 -29
- package/dist/docs/guidelines/tbd-sync-troubleshooting.md +27 -5
- package/dist/docs/guidelines/typescript-cli-tool-rules.md +18 -18
- package/dist/docs/guidelines/typescript-code-coverage.md +8 -6
- package/dist/docs/guidelines/typescript-rules.md +9 -11
- package/dist/docs/guidelines/typescript-sorting-patterns.md +1 -1
- package/dist/docs/guidelines/typescript-yaml-handling-rules.md +6 -6
- package/dist/docs/shortcuts/standard/new-shortcut.md +14 -0
- package/dist/docs/shortcuts/standard/setup-github-cli.md +4 -1
- package/dist/docs/shortcuts/system/shortcut-explanation.md +16 -1
- package/dist/docs/shortcuts/system/skill-baseline.md +8 -1
- package/dist/docs/tbd-design.md +43 -43
- package/dist/docs/tbd-docs.md +1 -1
- package/dist/docs/tbd-prime.md +3 -3
- package/dist/index.mjs +1 -1
- package/dist/{src-CtZIHxYM.mjs → src-DTyyuaG_.mjs} +2 -2
- package/dist/{src-CtZIHxYM.mjs.map → src-DTyyuaG_.mjs.map} +1 -1
- package/dist/tbd +653 -229
- package/package.json +1 -1
- package/dist/config-BJz1m9eN.mjs.map +0 -1
- package/dist/docs/guidelines/general-eng-assistant-rules.md +0 -59
|
@@ -3,9 +3,8 @@ import { o as sortKeys, s as stringifyYaml } from "./yaml-utils-BPy991by.mjs";
|
|
|
3
3
|
import { parse } from "yaml";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { access, mkdir, readFile, realpath } from "node:fs/promises";
|
|
6
|
-
import { dirname, isAbsolute, join, parse as parse$1, resolve } from "node:path";
|
|
6
|
+
import { dirname, isAbsolute, join, parse as parse$1, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { writeFile } from "atomically";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
8
|
import { promisify } from "node:util";
|
|
10
9
|
|
|
11
10
|
//#region src/lib/paths.ts
|
|
@@ -150,6 +149,18 @@ function buildSharedTbdPaths(gitCommonDir) {
|
|
|
150
149
|
async function resolveSharedTbdPaths(baseDir) {
|
|
151
150
|
return buildSharedTbdPaths(await resolveGitCommonDir(baseDir));
|
|
152
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* True when the Git common dir lives outside the project checkout — the linked
|
|
154
|
+
* worktree shape where the checkout can be writable while `$GIT_COMMON_DIR/tbd`
|
|
155
|
+
* (shared sync state + lock) is not. This is the generic signal behind the
|
|
156
|
+
* Codex-sandbox case in #164; it does not depend on any `CODEX_*` env var, which
|
|
157
|
+
* that sandbox did not expose. Used by both the `doctor` lock-writability finding
|
|
158
|
+
* and the write-side `SharedLockUnwritableError` so their wording matches.
|
|
159
|
+
*/
|
|
160
|
+
function isCommonDirOutsideProject(gitCommonDir, projectRoot) {
|
|
161
|
+
const rel = relative(resolve(projectRoot), resolve(gitCommonDir));
|
|
162
|
+
return rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
163
|
+
}
|
|
153
164
|
/** The workspaces directory name within .tbd/ */
|
|
154
165
|
const WORKSPACES_DIR_NAME = "workspaces";
|
|
155
166
|
/** Full path to workspaces directory: .tbd/workspaces/ */
|
|
@@ -778,5 +789,5 @@ async function markWelcomeSeen(baseDir) {
|
|
|
778
789
|
}
|
|
779
790
|
|
|
780
791
|
//#endregion
|
|
781
|
-
export { WORKSPACES_DIR as A, SYNC_BRANCH as C, TBD_SHORTCUTS_STANDARD as D, TBD_GUIDELINES_DIR as E,
|
|
782
|
-
//# sourceMappingURL=config-
|
|
792
|
+
export { WORKSPACES_DIR as A, SYNC_BRANCH as C, TBD_SHORTCUTS_STANDARD as D, TBD_GUIDELINES_DIR as E, resolveAtticDir as F, resolveDataSyncDir as I, resolveSharedTbdPaths as L, getWorkspaceDir as M, isCommonDirOutsideProject as N, TBD_SHORTCUTS_SYSTEM as O, isValidWorkspaceName as P, LEGACY_WORKTREE_DIR as S, TBD_DOCS_DIR as T, DATA_SYNC_DIR as _, isInitialized as a, DEFAULT_SHORTCUT_PATHS as b, readConfigWithMigration as c, writeConfig as d, writeLocalState as f, CHARS_PER_TOKEN as g, isCompatibleFormat as h, initConfig as i, WORKTREE_DIR_NAME as j, TBD_TEMPLATES_DIR as k, readLocalState as l, formatUpgradeMessage as m, findTbdRoot as n, markWelcomeSeen as o, CURRENT_FORMAT as p, hasSeenWelcome as r, readConfig as s, IncompatibleFormatError as t, updateLocalState as u, DATA_SYNC_DIR_NAME as v, TBD_DIR as w, DEFAULT_TEMPLATE_PATHS as x, DEFAULT_GUIDELINES_PATHS as y };
|
|
793
|
+
//# sourceMappingURL=config-1ouUTKQr.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-1ouUTKQr.mjs","names":["parseYaml","parsePath"],"sources":["../src/lib/paths.ts","../src/lib/tbd-format.ts","../src/file/config.ts"],"sourcesContent":["/**\n * Centralized path constants for tbd.\n *\n * Directory structure (per spec):\n *\n * On main/dev branches:\n * .tbd/\n * Committed to the repo:\n * config.yml - Project configuration\n * .gitignore - Controls what's gitignored below\n * workspaces/ - Persistent state (outbox, named workspaces)\n * Gitignored (local only):\n * state.yml - Local state\n * docs/ - Installed documentation (regenerated on setup)\n *\n * In the Git common dir shared by all linked worktrees:\n * $GIT_COMMON_DIR/tbd/\n * layout.yml - Shared layout metadata\n * locks/data-sync.lock/ - Repo-scoped lock directory\n * backups/ - Local repair/migration backups\n * data-sync-worktree/ - Hidden worktree checkout of tbd-sync branch\n * .tbd/data-sync/ - issues/, mappings/, attic/, meta.yml\n *\n * On tbd-sync branch:\n * .tbd/\n * data-sync/\n * issues/\n * mappings/\n * attic/\n * meta.yml\n */\n\nimport { execFile } from 'node:child_process';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { promisify } from 'node:util';\n\n/** The tbd configuration directory on main branch */\nexport const TBD_DIR = '.tbd';\n\n/** The config file path */\nexport const CONFIG_FILE = join(TBD_DIR, 'config.yml');\n\n/** The local state file (gitignored) */\nexport const STATE_FILE = join(TBD_DIR, 'state.yml');\n\n/** The worktree directory name */\nexport const WORKTREE_DIR_NAME = 'data-sync-worktree';\n\n/** Legacy per-checkout worktree path used by f03 and earlier clients. */\nexport const LEGACY_WORKTREE_DIR = join(TBD_DIR, WORKTREE_DIR_NAME);\n\n/**\n * @internal Primary-checkout relative path to the shared sync worktree.\n *\n * Only valid when `.git` is a directory (i.e., the primary checkout). Production\n * code must call resolveSharedTbdPaths() instead: linked worktrees have a `.git`\n * file, so this constant resolves to the wrong location for them. Intended for\n * tests and the non-git fallback in resolveDataSyncDir().\n */\nexport const PRIMARY_CHECKOUT_WORKTREE_DIR = join('.git', 'tbd', WORKTREE_DIR_NAME);\n\n/** The data directory name on the sync branch */\nexport const DATA_SYNC_DIR_NAME = 'data-sync';\n\n/**\n * The data directory path as it appears on the tbd-sync branch.\n * In a normal checkout this same relative path is a legacy/wrong-location fallback;\n * production callers should resolve the absolute shared worktree path with\n * resolveDataSyncDir().\n */\nexport const DATA_SYNC_DIR = join(TBD_DIR, DATA_SYNC_DIR_NAME);\n\n/**\n * @internal Primary-checkout relative path to the synced data via the shared worktree.\n *\n * Same caveat as `PRIMARY_CHECKOUT_WORKTREE_DIR`: only valid for a primary checkout.\n * Production code should resolve the absolute path with resolveDataSyncDir(); this\n * constant is intended for tests and the non-git fallback path.\n */\nexport const PRIMARY_CHECKOUT_DATA_SYNC_DIR = join(\n PRIMARY_CHECKOUT_WORKTREE_DIR,\n TBD_DIR,\n DATA_SYNC_DIR_NAME,\n);\n\n/** Issues directory */\nexport const ISSUES_DIR = join(DATA_SYNC_DIR, 'issues');\n\n/** Mappings directory */\nexport const MAPPINGS_DIR = join(DATA_SYNC_DIR, 'mappings');\n\n/** Attic directory for conflict resolution */\nexport const ATTIC_DIR = join(DATA_SYNC_DIR, 'attic');\n\n/** Meta file for schema version */\nexport const META_FILE = join(DATA_SYNC_DIR, 'meta.yml');\n\n/** The sync branch name */\nexport const SYNC_BRANCH = 'tbd-sync';\n\n// =============================================================================\n// Git Common-Dir Shared Sync Paths\n// =============================================================================\n\nconst execFileAsync = promisify(execFile);\n\n/** Directory name under $GIT_COMMON_DIR for tbd local machinery. */\nexport const GIT_COMMON_TBD_DIR_NAME = 'tbd';\n\n/** Common-dir layout metadata file name. */\nexport const COMMON_DIR_LAYOUT_FILE_NAME = 'layout.yml';\n\n/** Shared lock directory name under $GIT_COMMON_DIR/tbd/. */\nexport const SHARED_LOCKS_DIR_NAME = 'locks';\n\n/** Shared backups directory name under $GIT_COMMON_DIR/tbd/. */\nexport const SHARED_BACKUPS_DIR_NAME = 'backups';\n\n/** Directory-lock name for shared data-sync operations. */\nexport const DATA_SYNC_LOCK_DIR_NAME = 'data-sync.lock';\n\n/**\n * Resolved Git common-dir paths for the repo-scoped sync layout.\n */\nexport interface SharedTbdPaths {\n /** Absolute Git common directory shared by all linked worktrees. */\n gitCommonDir: string;\n /** Absolute $GIT_COMMON_DIR/tbd path. */\n sharedTbdDir: string;\n /** Absolute shared hidden worktree path. */\n sharedWorktreePath: string;\n /** Absolute data-sync directory inside the shared worktree. */\n sharedDataSyncDir: string;\n /** Absolute common-dir layout metadata path. */\n sharedLayoutPath: string;\n /** Absolute shared lock directory parent. */\n sharedLocksDir: string;\n /** Absolute data-sync lock path. */\n sharedLockPath: string;\n /** Absolute shared backups directory. */\n sharedBackupsDir: string;\n}\n\n/**\n * Resolve Git's common directory from any checkout or linked worktree.\n */\nexport async function resolveGitCommonDir(cwd: string): Promise<string> {\n let output: string;\n try {\n const { stdout } = await execFileAsync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], {\n maxBuffer: 1024 * 1024,\n });\n output = stdout.trim();\n } catch {\n const { stdout } = await execFileAsync(\n 'git',\n ['-C', cwd, 'rev-parse', '--path-format=absolute', '--git-common-dir'],\n { maxBuffer: 1024 * 1024 },\n );\n output = stdout.trim();\n }\n\n if (!output) {\n throw new Error(`Unable to resolve Git common directory from ${cwd}`);\n }\n\n const gitCommonDir = isAbsolute(output) ? output : resolve(cwd, output);\n return realpath(gitCommonDir).catch(() => gitCommonDir);\n}\n\n/**\n * Build all shared tbd paths from an absolute Git common directory.\n */\nexport function buildSharedTbdPaths(gitCommonDir: string): SharedTbdPaths {\n const sharedTbdDir = join(gitCommonDir, GIT_COMMON_TBD_DIR_NAME);\n const sharedWorktreePath = join(sharedTbdDir, WORKTREE_DIR_NAME);\n const sharedDataSyncDir = join(sharedWorktreePath, TBD_DIR, DATA_SYNC_DIR_NAME);\n const sharedLayoutPath = join(sharedTbdDir, COMMON_DIR_LAYOUT_FILE_NAME);\n const sharedLocksDir = join(sharedTbdDir, SHARED_LOCKS_DIR_NAME);\n const sharedLockPath = join(sharedLocksDir, DATA_SYNC_LOCK_DIR_NAME);\n const sharedBackupsDir = join(sharedTbdDir, SHARED_BACKUPS_DIR_NAME);\n\n return {\n gitCommonDir,\n sharedTbdDir,\n sharedWorktreePath,\n sharedDataSyncDir,\n sharedLayoutPath,\n sharedLocksDir,\n sharedLockPath,\n sharedBackupsDir,\n };\n}\n\n/**\n * Resolve the shared tbd paths for the repository containing baseDir.\n */\nexport async function resolveSharedTbdPaths(baseDir: string): Promise<SharedTbdPaths> {\n return buildSharedTbdPaths(await resolveGitCommonDir(baseDir));\n}\n\n/**\n * True when the Git common dir lives outside the project checkout — the linked\n * worktree shape where the checkout can be writable while `$GIT_COMMON_DIR/tbd`\n * (shared sync state + lock) is not. This is the generic signal behind the\n * Codex-sandbox case in #164; it does not depend on any `CODEX_*` env var, which\n * that sandbox did not expose. Used by both the `doctor` lock-writability finding\n * and the write-side `SharedLockUnwritableError` so their wording matches.\n */\nexport function isCommonDirOutsideProject(gitCommonDir: string, projectRoot: string): boolean {\n const rel = relative(resolve(projectRoot), resolve(gitCommonDir));\n return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel);\n}\n\n// =============================================================================\n// Workspace Paths (for sync failure recovery, backups, bulk editing)\n// =============================================================================\n\n/** The workspaces directory name within .tbd/ */\nexport const WORKSPACES_DIR_NAME = 'workspaces';\n\n/** Full path to workspaces directory: .tbd/workspaces/ */\nexport const WORKSPACES_DIR = join(TBD_DIR, WORKSPACES_DIR_NAME);\n\n/**\n * Get the path to a named workspace directory.\n *\n * Workspaces are stored at: .tbd/workspaces/{name}/\n *\n * @param workspaceName - The name of the workspace (e.g., 'outbox', 'my-feature')\n * @returns Path to the workspace directory\n */\nexport function getWorkspaceDir(workspaceName: string): string {\n return join(WORKSPACES_DIR, workspaceName);\n}\n\n/**\n * Get the path to a workspace's issues directory.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's issues directory\n */\nexport function getWorkspaceIssuesDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'issues');\n}\n\n/**\n * Get the path to a workspace's mappings directory.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's mappings directory\n */\nexport function getWorkspaceMappingsDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'mappings');\n}\n\n/**\n * Get the path to a workspace's attic directory.\n *\n * The attic stores conflict backups during workspace save operations.\n *\n * @param workspaceName - The name of the workspace\n * @returns Path to the workspace's attic directory\n */\nexport function getWorkspaceAtticDir(workspaceName: string): string {\n return join(getWorkspaceDir(workspaceName), 'attic');\n}\n\n/**\n * Validate a workspace name.\n *\n * Valid workspace names:\n * - Lowercase alphanumeric characters\n * - Hyphens and underscores allowed\n * - Must not be empty\n * - Must not contain path separators or dots at start\n *\n * @param name - The workspace name to validate\n * @returns true if the name is valid\n */\nexport function isValidWorkspaceName(name: string): boolean {\n if (!name || name.length === 0) {\n return false;\n }\n\n // Must not start with dot (hidden files)\n if (name.startsWith('.')) {\n return false;\n }\n\n // Only allow lowercase alphanumeric, hyphens, and underscores\n // No spaces, path separators, or special characters\n const validPattern = /^[a-z0-9][a-z0-9_-]*$/;\n return validPattern.test(name);\n}\n\n// =============================================================================\n// Documentation/Shortcuts Paths\n// =============================================================================\n\n/** Docs directory name within .tbd/ */\nexport const DOCS_DIR = 'docs';\n\n/** Shortcuts directory name within docs/ */\nexport const SHORTCUTS_DIR = 'shortcuts';\n\n/** System shortcuts directory name (core docs like skill-baseline.md) */\nexport const SYSTEM_DIR = 'system';\n\n/** Standard shortcuts directory name (workflow shortcuts) */\nexport const STANDARD_DIR = 'standard';\n\n/** Guidelines directory name (coding rules and best practices) */\nexport const GUIDELINES_DIR = 'guidelines';\n\n/** Templates directory name (document templates) */\nexport const TEMPLATES_DIR = 'templates';\n\n/** Full path to docs directory: .tbd/docs/ */\nexport const TBD_DOCS_DIR = join(TBD_DIR, DOCS_DIR);\n\n/** Full path to shortcuts directory: .tbd/docs/shortcuts/ */\nexport const TBD_SHORTCUTS_DIR = join(TBD_DOCS_DIR, SHORTCUTS_DIR);\n\n/** Full path to system shortcuts: .tbd/docs/shortcuts/system/ */\nexport const TBD_SHORTCUTS_SYSTEM = join(TBD_SHORTCUTS_DIR, SYSTEM_DIR);\n\n/** Full path to standard shortcuts: .tbd/docs/shortcuts/standard/ */\nexport const TBD_SHORTCUTS_STANDARD = join(TBD_SHORTCUTS_DIR, STANDARD_DIR);\n\n/** Full path to guidelines: .tbd/docs/guidelines/ (top-level, not under shortcuts) */\nexport const TBD_GUIDELINES_DIR = join(TBD_DOCS_DIR, GUIDELINES_DIR);\n\n/** Full path to templates: .tbd/docs/templates/ (top-level, not under shortcuts) */\nexport const TBD_TEMPLATES_DIR = join(TBD_DOCS_DIR, TEMPLATES_DIR);\n\n/** Built-in docs source paths (relative to package docs/) */\nexport const BUILTIN_SHORTCUTS_SYSTEM = join(SHORTCUTS_DIR, SYSTEM_DIR);\nexport const BUILTIN_SHORTCUTS_STANDARD = join(SHORTCUTS_DIR, STANDARD_DIR);\n\n/** Built-in guidelines source path (relative to package docs/) */\nexport const BUILTIN_GUIDELINES_DIR = GUIDELINES_DIR;\n\n/** Built-in templates source path (relative to package docs/) */\nexport const BUILTIN_TEMPLATES_DIR = TEMPLATES_DIR;\n\n/** Install directory name (header files for tool-specific installation) */\nexport const INSTALL_DIR = 'install';\n\n/** Built-in install source path (relative to package docs/) */\nexport const BUILTIN_INSTALL_DIR = INSTALL_DIR;\n\n/**\n * Default shortcut lookup paths (searched in order, relative to tbd root).\n * Earlier paths take precedence over later paths.\n * Note: Guidelines and templates are now separate top-level directories.\n */\nexport const DEFAULT_SHORTCUT_PATHS = [\n TBD_SHORTCUTS_SYSTEM, // .tbd/docs/shortcuts/system/\n TBD_SHORTCUTS_STANDARD, // .tbd/docs/shortcuts/standard/\n];\n\n/**\n * Default guidelines lookup paths (relative to tbd root).\n */\nexport const DEFAULT_GUIDELINES_PATHS = [\n TBD_GUIDELINES_DIR, // .tbd/docs/guidelines/\n];\n\n/**\n * Default template lookup paths (relative to tbd root).\n */\nexport const DEFAULT_TEMPLATE_PATHS = [\n TBD_TEMPLATES_DIR, // .tbd/docs/templates/\n];\n\n/**\n * Get the full path to an issue file.\n */\nexport function getIssuePath(issueId: string): string {\n return join(ISSUES_DIR, `${issueId}.md`);\n}\n\n/**\n * Get the full path to a mapping file.\n */\nexport function getMappingPath(name: string): string {\n return join(MAPPINGS_DIR, `${name}.yml`);\n}\n\n/**\n * Get the full path to an attic entry.\n */\nexport function getAtticPath(issueId: string, filename: string): string {\n return join(ATTIC_DIR, 'conflicts', issueId, filename);\n}\n\n// =============================================================================\n// Dynamic Path Resolution\n// =============================================================================\n\nimport { access, realpath } from 'node:fs/promises';\n\n/**\n * Options for resolveDataSyncDir.\n */\nexport interface ResolveDataSyncDirOptions {\n /**\n * Allow fallback to direct path when worktree is missing.\n * Set to true for test environments or diagnostic tools.\n * Default: true. When false and worktree is missing, throws WorktreeMissingError.\n */\n allowFallback?: boolean;\n}\n\n/**\n * Error thrown when worktree is missing and fallback is not allowed.\n * Defined inline to avoid circular dependency with errors.ts.\n */\nexport class WorktreeMissingError extends Error {\n constructor(\n message = \"Shared worktree not found under $GIT_COMMON_DIR/tbd/data-sync-worktree/. Run 'tbd doctor --fix' to repair.\",\n ) {\n super(message);\n this.name = 'WorktreeMissingError';\n }\n}\n\n/**\n * Cache for resolved data sync directory.\n * Reset when baseDir changes.\n */\nlet _resolvedDataSyncDir: string | null = null;\nlet _resolvedBaseDir: string | null = null;\nlet _resolvedAllowFallback: boolean | null = null;\n\n/**\n * Resolve the actual data sync directory path.\n *\n * This function detects whether we're running with a git worktree\n * (production) or in a test environment without worktree.\n *\n * Order of preference:\n * 1. Shared worktree path if it exists:\n * $GIT_COMMON_DIR/tbd/data-sync-worktree/.tbd/data-sync/\n * 2. Direct path as fallback (only if allowFallback: true, for tests/diagnostics)\n *\n * @param baseDir - The tbd root directory (from requireInit or findTbdRoot)\n * @param options - Options for path resolution\n * @returns Resolved data sync directory path\n * @throws WorktreeMissingError if worktree missing and allowFallback is false\n *\n * See: plan-2026-01-28-sync-worktree-recovery-and-hardening.md\n */\nexport async function resolveDataSyncDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const allowFallback = options?.allowFallback ?? true;\n\n // Return cached result if baseDir and options haven't changed\n if (\n _resolvedDataSyncDir &&\n _resolvedBaseDir === baseDir &&\n _resolvedAllowFallback === allowFallback\n ) {\n return _resolvedDataSyncDir;\n }\n\n let worktreePath: string | null = null;\n try {\n worktreePath = (await resolveSharedTbdPaths(baseDir)).sharedDataSyncDir;\n } catch {\n // Not in a git repository or git is unavailable. Check the static primary-checkout\n // path for unit tests before falling back to the direct diagnostic path.\n worktreePath = join(baseDir, PRIMARY_CHECKOUT_DATA_SYNC_DIR);\n }\n const directPath = join(baseDir, DATA_SYNC_DIR);\n\n // Check if worktree path exists\n if (worktreePath) {\n try {\n await access(worktreePath);\n _resolvedDataSyncDir = worktreePath;\n _resolvedBaseDir = baseDir;\n _resolvedAllowFallback = allowFallback;\n return worktreePath;\n } catch {\n // Worktree doesn't exist\n }\n }\n\n {\n // Worktree doesn't exist\n if (!allowFallback) {\n throw new WorktreeMissingError();\n }\n\n // Fallback to direct path (test mode or diagnostic tools)\n // Note: In production, sync.ts checks worktree health before calling this\n // Debug warning to help detect unintended fallback usage\n if (process.env.DEBUG || process.env.TBD_DEBUG) {\n console.warn(\n '[tbd:paths] resolveDataSyncDir: worktree not found, falling back to direct path',\n );\n }\n // Intentionally do NOT cache the fallback result: a later call after the\n // worktree is created must rediscover the real path, not keep returning\n // the stale fallback.\n return directPath;\n }\n}\n\n/**\n * Resolve issues directory path.\n */\nexport async function resolveIssuesDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'issues');\n}\n\n/**\n * Resolve mappings directory path.\n */\nexport async function resolveMappingsDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'mappings');\n}\n\n/**\n * Resolve attic directory path.\n */\nexport async function resolveAtticDir(\n baseDir: string,\n options?: ResolveDataSyncDirOptions,\n): Promise<string> {\n const dataSyncDir = await resolveDataSyncDir(baseDir, options);\n return join(dataSyncDir, 'attic');\n}\n\n/**\n * Clear the resolved path cache.\n * Call this when the repository state changes (e.g., after init).\n */\nexport function clearPathCache(): void {\n _resolvedDataSyncDir = null;\n _resolvedBaseDir = null;\n _resolvedAllowFallback = null;\n}\n\n// =============================================================================\n// Doc Path Resolution\n// =============================================================================\n\n/**\n * Resolve a doc path for consistent handling across the codebase.\n *\n * Path resolution rules:\n * - Absolute paths (starting with /): used as-is\n * - Home directory paths (starting with ~/): expanded to user home directory\n * - Relative paths: resolved from tbd root (baseDir)\n *\n * @param docPath - The path to resolve\n * @param baseDir - The tbd root directory (parent of .tbd/)\n * @returns Resolved absolute path\n *\n * @example\n * // Absolute path - returned as-is\n * resolveDocPath('/usr/local/docs/file.md') // => '/usr/local/docs/file.md'\n *\n * // Home path - expanded\n * resolveDocPath('~/docs/file.md') // => '/Users/username/docs/file.md'\n *\n * // Relative path - resolved from baseDir\n * resolveDocPath('docs/file.md', '/project') // => '/project/docs/file.md'\n */\nexport function resolveDocPath(docPath: string, baseDir: string): string {\n // Handle home directory expansion\n if (docPath.startsWith('~/')) {\n return join(homedir(), docPath.slice(2));\n }\n\n // Absolute paths used as-is\n if (isAbsolute(docPath)) {\n return docPath;\n }\n\n // Relative paths resolved from baseDir (tbd root)\n return join(baseDir, docPath);\n}\n\n// =============================================================================\n// Token Estimation Settings\n// =============================================================================\n\n/**\n * Characters per token ratio for estimating token counts.\n *\n * Based on research of OpenAI (tiktoken) and Claude tokenizers:\n * - Pure English prose: ~4-5 chars/token\n * - Code and symbols: ~3 chars/token\n * - Mixed markdown/code docs: ~3.5 chars/token\n *\n * We use 3.5 as our docs are markdown with code examples.\n * This provides ~15-20% accuracy, sufficient for cost estimation.\n */\nexport const CHARS_PER_TOKEN = 3.5;\n","/**\n * tbd Directory Format Versioning\n * ================================\n *\n * This file is the SINGLE SOURCE OF TRUTH for .tbd/ directory format versions.\n *\n * WHEN TO BUMP THE FORMAT VERSION:\n * - Bump when changes REQUIRE migration (deleting files, changing formats, moving files)\n * - **Bump when changing config schema** (adding, removing, or modifying fields)\n * - **Bump when the shape of a generated agent-integration surface changes** (e.g. the\n * managed AGENTS.md block). This same format is stamped there via\n * AGENT_INTEGRATION_FORMAT (integration-paths.ts), so there is ONE format code across\n * all tbd-managed surfaces.\n * - Do NOT bump for additive changes that don't affect config.yml (new directories, etc.)\n *\n * HOW TO ADD A NEW FORMAT VERSION:\n * 1. Add entry to FORMAT_HISTORY with detailed description\n * 2. Implement migrate_fXX_to_fYY() function\n * 3. Add case to migrateToLatest()\n * 4. Update CURRENT_FORMAT\n * 5. Add tests for the migration path\n *\n * FORWARD COMPATIBILITY POLICY:\n * ConfigSchema uses Zod's strip() mode, which discards unknown fields. To prevent\n * data loss when users mix tbd versions:\n *\n * 1. When changing config schema, bump the format version (e.g., f03 → f04)\n * 2. config.ts checks format compatibility via isCompatibleFormat()\n * 3. Older tbd versions will error with \"format 'fXX' is from a newer tbd version\"\n * 4. The error tells users to upgrade: npm install -g get-tbd@latest\n *\n * This ensures older versions fail fast rather than silently corrupting config.\n * See ConfigSchema in schemas.ts and checkFormatCompatibility() in config.ts.\n */\n\n// =============================================================================\n// Format Constants\n// =============================================================================\n\n/**\n * Current format version.\n * Bump this ONLY for breaking changes that require migration.\n */\nexport const CURRENT_FORMAT = 'f04';\n\n/**\n * Initial format version for configs that don't have tbd_format field.\n */\nexport const INITIAL_FORMAT = 'f01';\n\n// =============================================================================\n// Format History\n// =============================================================================\n\n/**\n * Complete history of format versions with their changes.\n * This serves as documentation and enables version detection.\n */\nexport const FORMAT_HISTORY = {\n f01: {\n introduced: '0.1.0',\n description: 'Initial format',\n structure: {\n 'config.yml': 'Project configuration',\n 'state.yml': 'Local state (gitignored)',\n 'docs/': 'Documentation cache (gitignored)',\n 'issues/': 'Issue YAML files',\n },\n },\n f02: {\n introduced: '0.1.5',\n description: 'Adds configurable doc_cache',\n changes: [\n 'Added doc_cache: key to config.yml for configurable doc sources',\n 'Added settings.doc_auto_sync_hours for automatic doc refresh',\n 'Added last_doc_sync_at to state.yml for tracking sync time',\n ],\n migration: 'Populates default doc_cache config from bundled docs',\n },\n f03: {\n introduced: '0.1.6',\n description: 'Consolidates docs_cache config structure',\n changes: [\n 'Consolidated doc_cache: and docs: into single docs_cache: key',\n 'Moved doc_cache: -> docs_cache.files:',\n 'Moved docs.paths: -> docs_cache.lookup_path:',\n 'Removed separate docs: key',\n ],\n migration: 'Migrates old config keys to new docs_cache structure',\n },\n f04: {\n introduced: '0.2.0',\n description: 'Moves local issue sync worktree into the Git common directory',\n changes: [\n 'Added sync.storage: git-common-dir-v1 to config.yml',\n 'Moved local data-sync worktree machinery to $GIT_COMMON_DIR/tbd/',\n 'Added $GIT_COMMON_DIR/tbd/layout.yml using the same tbd_format ID',\n ],\n migration:\n 'Initializes shared common-dir sync layout before writing config.yml with tbd_format f04',\n },\n} as const;\n\nexport type FormatVersion = keyof typeof FORMAT_HISTORY;\n\n// =============================================================================\n// Migration Types\n// =============================================================================\n\n/**\n * Raw config data before parsing/validation.\n * Used during migration when we need to work with potentially old formats.\n */\nexport interface RawConfig {\n tbd_format?: string;\n tbd_version?: string;\n sync?: {\n branch?: string;\n remote?: string;\n storage?: 'git-common-dir-v1';\n };\n display?: {\n id_prefix?: string;\n };\n settings?: {\n auto_sync?: boolean;\n doc_auto_sync_hours?: number;\n };\n // Old format (f02 and earlier)\n docs?: {\n paths?: string[];\n };\n doc_cache?: Record<string, string>;\n // New format (f03+)\n docs_cache?: {\n files?: Record<string, string>;\n lookup_path?: string[];\n };\n}\n\n/**\n * Result of a migration operation.\n */\nexport interface MigrationResult {\n /** The migrated config */\n config: RawConfig;\n /** Format version before migration */\n fromFormat: FormatVersion;\n /** Format version after migration */\n toFormat: FormatVersion;\n /** Whether any changes were made */\n changed: boolean;\n /** Description of changes made */\n changes: string[];\n}\n\n// =============================================================================\n// Migration Functions\n// =============================================================================\n\n/**\n * Migrate from f01 to f02.\n * - Adds tbd_format field\n * - Adds doc_auto_sync_hours setting (default: 24)\n * - doc_cache will be populated separately during setup (requires file system access)\n */\nfunction migrate_f01_to_f02(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n // Add format version\n migrated.tbd_format = 'f02';\n changes.push('Added tbd_format: f02');\n\n // Ensure settings exists and add doc_auto_sync_hours\n migrated.settings ??= {};\n if (migrated.settings.doc_auto_sync_hours === undefined) {\n migrated.settings.doc_auto_sync_hours = 24;\n changes.push('Added settings.doc_auto_sync_hours: 24');\n }\n\n // Note: doc_cache is intentionally NOT added here.\n // It will be populated during setup when we have access to the file system\n // and can enumerate the bundled docs.\n\n return {\n config: migrated,\n fromFormat: 'f01',\n toFormat: 'f02',\n changed: changes.length > 0,\n changes,\n };\n}\n\n/**\n * Migrate from f02 to f03.\n * - Consolidates doc_cache: and docs: into docs_cache:\n * - Moves doc_cache: -> docs_cache.files:\n * - Moves docs.paths: -> docs_cache.lookup_path:\n * - Removes separate docs: and doc_cache: keys\n */\nfunction migrate_f02_to_f03(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n // Update format version\n migrated.tbd_format = 'f03';\n changes.push('Updated tbd_format: f03');\n\n // Initialize docs_cache if it doesn't exist\n migrated.docs_cache ??= {};\n\n // Migrate doc_cache -> docs_cache.files\n if (migrated.doc_cache && Object.keys(migrated.doc_cache).length > 0) {\n migrated.docs_cache.files = { ...migrated.doc_cache };\n changes.push('Moved doc_cache: -> docs_cache.files:');\n delete migrated.doc_cache;\n }\n\n // Migrate docs.paths -> docs_cache.lookup_path\n if (migrated.docs?.paths && migrated.docs.paths.length > 0) {\n migrated.docs_cache.lookup_path = [...migrated.docs.paths];\n changes.push('Moved docs.paths: -> docs_cache.lookup_path:');\n }\n\n // Remove old docs: key\n if (migrated.docs) {\n delete migrated.docs;\n changes.push('Removed docs: key');\n }\n\n return {\n config: migrated,\n fromFormat: 'f02',\n toFormat: 'f03',\n changed: changes.length > 0,\n changes,\n };\n}\n\n/**\n * Migrate from f03 to f04.\n * - Adds sync.storage marker for the Git common-dir shared worktree layout\n * - Bumps tbd_format so old clients fail before writing legacy worktrees\n */\nfunction migrate_f03_to_f04(config: RawConfig): MigrationResult {\n const changes: string[] = [];\n const migrated = { ...config };\n\n migrated.tbd_format = 'f04';\n changes.push('Updated tbd_format: f04');\n\n migrated.sync = { ...migrated.sync, storage: 'git-common-dir-v1' };\n changes.push('Added sync.storage: git-common-dir-v1');\n\n return {\n config: migrated,\n fromFormat: 'f03',\n toFormat: 'f04',\n changed: changes.length > 0,\n changes,\n };\n}\n\n// =============================================================================\n// Public API\n// =============================================================================\n\n/**\n * Detect the format version of a config.\n * Returns INITIAL_FORMAT ('f01') if no tbd_format field is present.\n */\nexport function detectFormat(config: RawConfig): FormatVersion {\n const format = config.tbd_format;\n if (!format) {\n return INITIAL_FORMAT;\n }\n if (format in FORMAT_HISTORY) {\n return format as FormatVersion;\n }\n // Unknown format - treat as latest (will fail validation if incompatible)\n return CURRENT_FORMAT;\n}\n\n/**\n * Check if a config needs migration.\n */\nexport function needsMigration(config: RawConfig): boolean {\n const currentFormat = detectFormat(config);\n return currentFormat !== CURRENT_FORMAT;\n}\n\n/**\n * Migrate a config to the latest format version.\n *\n * This function applies all necessary migrations in sequence.\n * It does NOT populate doc_cache - that requires file system access\n * and should be done separately during setup.\n *\n * @param config - The raw config to migrate\n * @returns Migration result with the migrated config and change log\n */\nexport function migrateToLatest(config: RawConfig): MigrationResult {\n const fromFormat = detectFormat(config);\n\n if (fromFormat === CURRENT_FORMAT) {\n return {\n config,\n fromFormat,\n toFormat: CURRENT_FORMAT,\n changed: false,\n changes: [],\n };\n }\n\n let current = config;\n let currentFormat: FormatVersion = fromFormat;\n const allChanges: string[] = [];\n\n // Apply migrations in sequence\n if (currentFormat === 'f01') {\n const result = migrate_f01_to_f02(current);\n current = result.config;\n currentFormat = 'f02' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n if (currentFormat === 'f02') {\n const result = migrate_f02_to_f03(current);\n current = result.config;\n currentFormat = 'f03' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n if (currentFormat === 'f03') {\n const result = migrate_f03_to_f04(current);\n current = result.config;\n currentFormat = 'f04' as FormatVersion;\n allChanges.push(...result.changes);\n }\n\n return {\n config: current,\n fromFormat,\n toFormat: currentFormat,\n changed: allChanges.length > 0,\n changes: allChanges,\n };\n}\n\n/**\n * Check if a format version is compatible with the current tbd version.\n * Future format versions are considered incompatible (would need tbd upgrade).\n */\nexport function isCompatibleFormat(format: string): boolean {\n return isFormatCompatibleWithSupported(format, CURRENT_FORMAT);\n}\n\n/**\n * Check whether a format version is compatible with a tbd client that supports\n * versions up to supportedFormat. This makes the old-client contract testable:\n * an f03 client must reject an f04 repository instead of writing legacy data.\n */\nexport function isFormatCompatibleWithSupported(\n format: string,\n supportedFormat: FormatVersion,\n): boolean {\n const formatVersions = Object.keys(FORMAT_HISTORY);\n const currentIndex = formatVersions.indexOf(supportedFormat);\n const checkIndex = formatVersions.indexOf(format);\n\n if (checkIndex === -1) {\n // Unknown format - might be from a newer tbd version\n return false;\n }\n\n // Compatible if same or older format (we can migrate up)\n return checkIndex <= currentIndex;\n}\n\n/**\n * Build the standard message shown when a repository has a format newer than\n * this tbd client supports.\n */\nexport function formatUpgradeMessage(\n subject: string,\n foundFormat: string,\n supportedFormat: string,\n): string {\n return (\n `This repository requires a newer version of tbd.\\n` +\n `${subject} format '${foundFormat}' is from a newer tbd version.\\n` +\n `This tbd version supports up to format '${supportedFormat}'.\\n` +\n `Upgrade tbd: npm install -g get-tbd@latest`\n );\n}\n\n/**\n * Get a human-readable description of what migrations will be applied.\n */\nexport function describeMigration(fromFormat: FormatVersion): string[] {\n const descriptions: string[] = [];\n let current = fromFormat;\n\n if (current === 'f01') {\n descriptions.push('f01 → f02: Add doc_cache configuration support');\n current = 'f02';\n }\n\n if (current === 'f02') {\n descriptions.push('f02 → f03: Consolidate doc_cache and docs into docs_cache');\n current = 'f03';\n }\n\n if (current === 'f03') {\n descriptions.push('f03 → f04: Move local sync worktree to Git common directory');\n current = 'f04';\n }\n\n return descriptions;\n}\n","/**\n * Config file operations.\n *\n * Config is stored at .tbd/config.yml and contains project-level settings.\n *\n * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.\n *\n * See: tbd-design.md §2.2.2 Config File\n */\n\nimport { readFile, mkdir, access } from 'node:fs/promises';\nimport { join, dirname, parse as parsePath } from 'node:path';\nimport { writeFile } from 'atomically';\nimport { parse as parseYaml } from 'yaml';\n\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Config, LocalState } from '../lib/types.js';\nimport {\n ConfigSchema,\n LocalStateSchema,\n CONFIG_FIELD_ORDER,\n LOCAL_STATE_FIELD_ORDER,\n} from '../lib/schemas.js';\nimport { CONFIG_FILE, STATE_FILE, SYNC_BRANCH } from '../lib/paths.js';\nimport {\n CURRENT_FORMAT,\n formatUpgradeMessage,\n needsMigration,\n migrateToLatest,\n isCompatibleFormat,\n type RawConfig,\n} from '../lib/tbd-format.js';\n\n/**\n * Error thrown when the config format version is from a newer tbd version.\n * This prevents older tbd versions from silently stripping new config fields.\n */\nexport class IncompatibleFormatError extends Error {\n constructor(\n public readonly foundFormat: string,\n public readonly supportedFormat: string,\n ) {\n super(formatUpgradeMessage('Config', foundFormat, supportedFormat));\n this.name = 'IncompatibleFormatError';\n }\n}\n\n/**\n * Check if config format is compatible, throw if not.\n * This prevents older tbd versions from silently stripping fields added by newer versions.\n */\nfunction checkFormatCompatibility(data: RawConfig): void {\n const format = data.tbd_format;\n if (format && !isCompatibleFormat(format)) {\n throw new IncompatibleFormatError(format, CURRENT_FORMAT);\n }\n}\n\n/**\n * Create default config for a new project.\n * @param prefix - Required: the project prefix for display IDs (e.g., \"proj\", \"myapp\")\n */\nfunction createDefaultConfig(version: string, prefix: string): Config {\n return ConfigSchema.parse({\n tbd_format: CURRENT_FORMAT,\n tbd_version: version,\n sync: {\n branch: SYNC_BRANCH,\n remote: 'origin',\n storage: 'git-common-dir-v1',\n },\n display: {\n id_prefix: prefix,\n },\n settings: {\n auto_sync: false,\n doc_auto_sync_hours: 24,\n },\n });\n}\n\n/**\n * Initialize a new config file with default settings.\n * Creates .tbd directory if it doesn't exist.\n * @param prefix - Required: the project prefix for display IDs (e.g., \"proj\", \"myapp\")\n */\nexport async function initConfig(\n baseDir: string,\n version: string,\n prefix: string,\n): Promise<Config> {\n const tbdDir = join(baseDir, '.tbd');\n await mkdir(tbdDir, { recursive: true });\n\n const config = createDefaultConfig(version, prefix);\n await writeConfig(baseDir, config);\n\n return config;\n}\n\n/**\n * Read config from file with automatic migration if needed.\n *\n * ⚠️ FORMAT VERSIONING: See tbd-format.ts for version history and migration rules.\n *\n * @throws {IncompatibleFormatError} If config is from a newer tbd version.\n * @throws If config file doesn't exist or is invalid.\n */\nexport async function readConfig(baseDir: string): Promise<Config> {\n const configPath = join(baseDir, CONFIG_FILE);\n const content = await readFile(configPath, 'utf-8');\n const data = parseYaml(content) as RawConfig;\n\n // Check for incompatible (future) format versions first\n checkFormatCompatibility(data);\n\n // Check if migration is needed (for older formats)\n if (needsMigration(data)) {\n const result = migrateToLatest(data);\n // Note: We don't automatically write the migrated config here.\n // Migration writes should be explicit via writeConfig() after setup.\n return ConfigSchema.parse(result.config);\n }\n\n return ConfigSchema.parse(data);\n}\n\n/**\n * Read config from file, returning migration info if a migration was applied.\n * Use this when you need to know if the config was migrated.\n *\n * @throws {IncompatibleFormatError} If config is from a newer tbd version.\n */\nexport async function readConfigWithMigration(baseDir: string): Promise<{\n config: Config;\n migrated: boolean;\n changes: string[];\n /**\n * The `tbd_format` value found in the file before migration. Useful for showing\n * the user what was upgraded (e.g., \"f03 → f04\"). `undefined` for very old\n * configs that have no `tbd_format` field.\n */\n fromFormat: string | undefined;\n}> {\n const configPath = join(baseDir, CONFIG_FILE);\n const content = await readFile(configPath, 'utf-8');\n const data = parseYaml(content) as RawConfig;\n\n // Check for incompatible (future) format versions first\n checkFormatCompatibility(data);\n\n const fromFormat = data.tbd_format;\n\n if (needsMigration(data)) {\n const result = migrateToLatest(data);\n return {\n config: ConfigSchema.parse(result.config),\n migrated: result.changed,\n changes: result.changes,\n fromFormat,\n };\n }\n\n return {\n config: ConfigSchema.parse(data),\n migrated: false,\n changes: [],\n fromFormat,\n };\n}\n\n/**\n * Write config to file with explanatory comments.\n */\nexport async function writeConfig(baseDir: string, config: Config): Promise<void> {\n const configPath = join(baseDir, CONFIG_FILE);\n\n // Sort keys using canonical field order, then serialize with compact output.\n // sortMapEntries: false preserves our manual ordering.\n const sorted = sortKeys(config as unknown as Record<string, unknown>, CONFIG_FIELD_ORDER);\n const yaml = stringifyYaml(sorted, { lineWidth: 0, sortMapEntries: false });\n\n // Add explanatory comments for docs_cache section\n let content = yaml;\n if (config.docs_cache && Object.keys(config.docs_cache).length > 0) {\n const docsCacheComment = `# Documentation cache configuration.\n# files: Maps destination paths (relative to .tbd/docs/) to source locations.\n# Sources can be:\n# - internal: prefix for bundled docs (e.g., \"internal:shortcuts/standard/code-review-and-commit.md\")\n# - Full URL for external docs (e.g., \"https://raw.githubusercontent.com/org/repo/main/file.md\")\n# lookup_path: Search paths for doc lookup (like shell $PATH). Earlier paths take precedence.\n#\n# To sync docs: tbd sync --docs\n# To check status: tbd sync --status\n#\n# Auto-sync: Docs are automatically synced when stale (default: every 24 hours).\n# Configure with settings.doc_auto_sync_hours (0 = disabled).\n`;\n content = content.replace('docs_cache:', docsCacheComment + 'docs_cache:');\n }\n\n await writeFile(configPath, content);\n}\n\n/**\n * Check if tbd is properly initialized in the given directory.\n * Returns true only if .tbd/config.yml exists (not just a .tbd/ directory).\n *\n * This prevents spurious .tbd/ directories (e.g., containing only state.yml\n * created by a bug) from being mistaken for tbd roots. A valid tbd root\n * always has config.yml created during `tbd init`.\n */\nasync function hasTbdDir(dir: string): Promise<boolean> {\n const configPath = join(dir, CONFIG_FILE);\n try {\n await access(configPath);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Find the tbd repository root by walking up the directory tree.\n * Similar to how git finds .git/ directories.\n *\n * @param startDir - Directory to start searching from\n * @returns The tbd root directory path, or null if not found\n */\nexport async function findTbdRoot(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n const { root } = parsePath(startDir);\n\n while (currentDir !== root) {\n if (await hasTbdDir(currentDir)) {\n return currentDir;\n }\n currentDir = dirname(currentDir);\n }\n\n // Check root directory as well\n if (await hasTbdDir(root)) {\n return root;\n }\n\n return null;\n}\n\n/**\n * Check if tbd is initialized in the given directory or any parent directory.\n * Walks up the directory tree looking for .tbd/.\n */\nexport async function isInitialized(baseDir: string): Promise<boolean> {\n const root = await findTbdRoot(baseDir);\n return root !== null;\n}\n\n// =============================================================================\n// Local State Operations\n// =============================================================================\n\n/**\n * Read local state from .tbd/state.yml\n * Returns empty state if file doesn't exist.\n */\nexport async function readLocalState(baseDir: string): Promise<LocalState> {\n const statePath = join(baseDir, STATE_FILE);\n try {\n const content = await readFile(statePath, 'utf-8');\n const data: unknown = parseYaml(content);\n return LocalStateSchema.parse(data ?? {});\n } catch {\n // File doesn't exist or is invalid - return empty state\n return {};\n }\n}\n\n/**\n * Write local state to .tbd/state.yml\n *\n * Uses `atomically` for safe writes (atomic rename, auto parent-dir creation).\n * However, we intentionally guard against .tbd/ not existing: `atomically`\n * would auto-create it, which is wrong if baseDir is a subdirectory rather\n * than the true tbd root. Only `tbd init` (via initConfig) should create .tbd/.\n */\nexport async function writeLocalState(baseDir: string, state: LocalState): Promise<void> {\n // Guard: refuse to write if .tbd/ directory doesn't exist.\n // Without this, `atomically` would auto-create .tbd/ in subdirectories,\n // producing spurious directories that confuse findTbdRoot().\n const tbdDir = join(baseDir, '.tbd');\n try {\n await access(tbdDir);\n } catch {\n throw new Error(\n `Cannot write state: .tbd/ directory does not exist at ${baseDir}. ` +\n `Run 'tbd init' first or ensure the correct tbd root is being used.`,\n );\n }\n\n const statePath = join(baseDir, STATE_FILE);\n\n // Sort keys using canonical field order, then serialize with compact output.\n // sortMapEntries: false preserves our manual ordering.\n const sorted = sortKeys(state as unknown as Record<string, unknown>, LOCAL_STATE_FIELD_ORDER);\n const yaml = stringifyYaml(sorted, { lineWidth: 0, sortMapEntries: false });\n\n await writeFile(statePath, yaml);\n}\n\n/**\n * Update specific fields in local state (merge with existing).\n */\nexport async function updateLocalState(\n baseDir: string,\n updates: Partial<LocalState>,\n): Promise<LocalState> {\n const current = await readLocalState(baseDir);\n const updated = { ...current, ...updates };\n await writeLocalState(baseDir, updated);\n return updated;\n}\n\n// =============================================================================\n// Welcome State Operations\n// =============================================================================\n\n/**\n * Check if the user has seen the welcome message.\n */\nexport async function hasSeenWelcome(baseDir: string): Promise<boolean> {\n const state = await readLocalState(baseDir);\n return state.welcome_seen === true;\n}\n\n/**\n * Mark the welcome message as seen.\n */\nexport async function markWelcomeSeen(baseDir: string): Promise<void> {\n await updateLocalState(baseDir, { welcome_seen: true });\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as isInitialized, c as readConfigWithMigration, d as writeConfig, f as writeLocalState, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as updateLocalState } from "./config-
|
|
1
|
+
import { a as isInitialized, c as readConfigWithMigration, d as writeConfig, f as writeLocalState, i as initConfig, l as readLocalState, n as findTbdRoot, o as markWelcomeSeen, r as hasSeenWelcome, s as readConfig, t as IncompatibleFormatError, u as updateLocalState } from "./config-1ouUTKQr.mjs";
|
|
2
2
|
|
|
3
3
|
export { readConfig };
|
package/dist/docs/SKILL.md
CHANGED
|
@@ -23,7 +23,7 @@ allowed-tools: Bash(tbd:*), Read, Write
|
|
|
23
23
|
Drop-in replacement for `bd`.
|
|
24
24
|
2. **Spec-Driven Workflows**: Plan features → break into beads → implement
|
|
25
25
|
systematically.
|
|
26
|
-
3. **Knowledge Injection**:
|
|
26
|
+
3. **Knowledge Injection**: 25+ engineering guidelines (TypeScript, Python, TDD,
|
|
27
27
|
testing, Convex, monorepos) available on demand.
|
|
28
28
|
4. **Shortcuts**: Reusable instruction templates for common workflows (code review,
|
|
29
29
|
commits, PRs, cleanup, handoffs).
|
|
@@ -85,6 +85,7 @@ or want help → run `tbd shortcut welcome-user`
|
|
|
85
85
|
| “Create a PR” / “File a PR” | `tbd shortcut create-or-update-pr-simple` |
|
|
86
86
|
| “Merge main into my branch” | `tbd shortcut merge-upstream` |
|
|
87
87
|
| **Guidelines & Knowledge** | |
|
|
88
|
+
| *(any engineering work)* | Load the **General engineering** group first (see below) |
|
|
88
89
|
| “Use TypeScript best practices” | `tbd guidelines typescript-rules` |
|
|
89
90
|
| “Use Python best practices” | `tbd guidelines python-rules` |
|
|
90
91
|
| “Build a TypeScript CLI” | `tbd guidelines typescript-cli-tool-rules` |
|
|
@@ -103,6 +104,12 @@ or want help → run `tbd shortcut welcome-user`
|
|
|
103
104
|
| “Check out this library’s source” | `tbd shortcut checkout-third-party-repo` |
|
|
104
105
|
| *(your choice whenever appropriate)* | `tbd list`, `tbd dep add`, `tbd close`, `tbd sync`, etc. |
|
|
105
106
|
|
|
107
|
+
**Loading guidelines for engineering work:** Before writing or reviewing code, load the
|
|
108
|
+
**General engineering** group—the `general-*` rules plus `error-handling-rules`—since
|
|
109
|
+
these apply to all code regardless of language.
|
|
110
|
+
Then load the group for the language or framework in use (TypeScript, Python, Convex,
|
|
111
|
+
etc.). Run `tbd guidelines --list` to see all available guidelines.
|
|
112
|
+
|
|
106
113
|
**Note:** Never gitignore `.tbd/workspaces/` — the outbox must be committed to your
|
|
107
114
|
working branch. See `tbd guidelines tbd-sync-troubleshooting` for details.
|
|
108
115
|
|
|
@@ -24,19 +24,19 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
|
24
24
|
|
|
25
25
|
| Tool / Package | Version | Check For Updates |
|
|
26
26
|
| --- | --- | --- |
|
|
27
|
-
| **Bun** | 1.3.13 (1.3.14 too recent) | [bun.sh/blog](https://bun.sh/blog)
|
|
28
|
-
| **TypeScript** | ^6.0.3 | [github.com/microsoft/TypeScript/releases](https://github.com/microsoft/TypeScript/releases)
|
|
29
|
-
| **Bunup** | ^0.16.31 | [npmjs.com/package/bunup](https://www.npmjs.com/package/bunup)
|
|
30
|
-
| **Biome** | ^2.4.14 (2.4.15 borderline) | [biomejs.dev](https://biomejs.dev/)
|
|
31
|
-
| **@changesets/cli** | ^2.31.0 | [github.com/changesets/changesets/releases](https://github.com/changesets/changesets/releases)
|
|
32
|
-
| **bun-types** | ^1.3.13 | [npmjs.com/package/bun-types](https://www.npmjs.com/package/bun-types)
|
|
33
|
-
| **tryscript** | ^0.1.6 | [npmjs.com/package/tryscript](https://www.npmjs.com/package/tryscript)
|
|
34
|
-
| **flowmark** | latest | [github.com/jlevy/flowmark](https://github.com/jlevy/flowmark)
|
|
35
|
-
| **publint** | ^0.3.20 (0.3.21 too recent) | [npmjs.com/package/publint](https://www.npmjs.com/package/publint)
|
|
36
|
-
| **actions/checkout** | v6 | [github.com/actions/checkout/releases](https://github.com/actions/checkout/releases)
|
|
37
|
-
| **oven-sh/setup-bun** | v2 | [github.com/oven-sh/setup-bun](https://github.com/oven-sh/setup-bun)
|
|
38
|
-
| **lefthook** | ^2.1.5 (2.1.7/2.1.8 too recent) | [github.com/evilmartians/lefthook/releases](https://github.com/evilmartians/lefthook/releases)
|
|
39
|
-
| **npm-check-updates** | ^22.0.0 (22.2.0 too recent) | [npmjs.com/package/npm-check-updates](https://www.npmjs.com/package/npm-check-updates)
|
|
27
|
+
| **Bun** | 1.3.13 (1.3.14 too recent) | [bun.sh/blog](https://bun.sh/blog)—Runtime, bundler, package manager, test runner. Acquired by Anthropic (Dec 2025). **Pinned to 1.3.13 (2026-04-20) per the 14-day rule**—1.3.14 (2026-05-13) is the **last Zig-based release** and the Rust rewrite merged to `main` 2026-05-14 (~960K lines, generated by Claude AI agents over ~6 days). New built-ins in 1.3.x: `Bun.Image` (Sharp replacement), experimental HTTP/3 in `Bun.serve()`, 7× faster warm installs via isolated linker global store, rewritten `fs.watch()` on Linux/macOS. Bump to 1.3.14 (or 1.4.x once it ships) once the 14-day window has elapsed. |
|
|
28
|
+
| **TypeScript** | ^6.0.3 | [github.com/microsoft/TypeScript/releases](https://github.com/microsoft/TypeScript/releases)—**6.0.3 stable** (shipped 2026-03-23). TS 6.0 is the **last JavaScript-based release** and is positioned as a bridge to 7.0: `strict: true` is now the default, ESM is the default module system, and ~9 compiler settings flipped defaults. **TS 7.0 Beta** (Project Corsa, Go rewrite) shipped 2026-04-21 as `@typescript/native-preview` (binary: `tsgo`); claims ~10× type-check speed and ~3× less memory. TS 7.0 stable is expected mid-to-late 2026. Do not adopt `tsgo` for production builds yet. |
|
|
29
|
+
| **Bunup** | ^0.16.31 | [npmjs.com/package/bunup](https://www.npmjs.com/package/bunup)—Build tool for TS libs. Still 0.x; 0.16.23 through 0.16.31 are bug-fix only (DTS alias fixes, workspace `onSuccess` fix, external/noExternal honoring). Pin to a specific minor version for stability. |
|
|
30
|
+
| **Biome** | ^2.4.14 (2.4.15 borderline) | [biomejs.dev](https://biomejs.dev/)—Formatter + linter. **Pinned to 2.4.14 (2026-05-01) per the 14-day rule**; 2.4.15 (2026-05-09) is 13 days old today—bump on next refresh. v2.4 (Feb 2026) added experimental embedded snippet formatting (CSS/GraphQL in JS/TS template literals), 15 HTML accessibility lint rules enabled by default, and promoted 24 nursery rules to stable (Vue 3, Next.js, Qwik). |
|
|
31
|
+
| **@changesets/cli** | ^2.31.0 | [github.com/changesets/changesets/releases](https://github.com/changesets/changesets/releases)—2.31.0 latest. **Still no native Bun support**; `changeset version` does not resolve `workspace:*` references for Bun workspaces—workarounds below remain required. |
|
|
32
|
+
| **bun-types** | ^1.3.13 | [npmjs.com/package/bun-types](https://www.npmjs.com/package/bun-types)—Type definitions for Bun runtime APIs. Track Bun version (pinned to 1.3.13 per the 14-day rule). |
|
|
33
|
+
| **tryscript** | ^0.1.6 | [npmjs.com/package/tryscript](https://www.npmjs.com/package/tryscript)—Golden/CLI testing via Markdown test files. |
|
|
34
|
+
| **flowmark** | latest | [github.com/jlevy/flowmark](https://github.com/jlevy/flowmark)—Markdown auto-formatter (via `uvx`). |
|
|
35
|
+
| **publint** | ^0.3.20 (0.3.21 too recent) | [npmjs.com/package/publint](https://www.npmjs.com/package/publint)—**Pinned to 0.3.20 (2026-05-08) per the 14-day rule**; 0.3.21 (2026-05-13) is 9 days old today. Incremental: re-enabled TS/TSX file existence checks; `exports["default"]` support. |
|
|
36
|
+
| **actions/checkout** | v6 | [github.com/actions/checkout/releases](https://github.com/actions/checkout/releases)—v6.0.2 (2026-01-09). Credentials now stored in `$RUNNER_TEMP` rather than `.git/config`; Node 24 runtime; requires runner ≥ 2.327.1. |
|
|
37
|
+
| **oven-sh/setup-bun** | v2 | [github.com/oven-sh/setup-bun](https://github.com/oven-sh/setup-bun)—v2.2.0 (2026-03-14). Migrated to Node.js 24 runtime ahead of GitHub’s **2026-06-02 deadline** that forces all Node.js 20 actions to Node.js 24. |
|
|
38
|
+
| **lefthook** | ^2.1.5 (2.1.7/2.1.8 too recent) | [github.com/evilmartians/lefthook/releases](https://github.com/evilmartians/lefthook/releases)—**Pinned to 2.1.5 (2026-04-06) per the 14-day rule**; 2.1.7 and 2.1.8 both shipped 2026-05-19 (3 days old). Patch-level since 2.1.1: dependency bumps and config-warning improvements. v2 still excludes regexp `exclude` and `skip_output` from v1. |
|
|
39
|
+
| **npm-check-updates** | ^22.0.0 (22.2.0 too recent) | [npmjs.com/package/npm-check-updates](https://www.npmjs.com/package/npm-check-updates)—**Major version jump from 19 to 22.** **Pinned to 22.0.0 (2026-04-25) per the 14-day rule**; 22.2.0 (2026-05-12) is 10 days old today. Now pure ESM; named imports only (`import { run } from 'npm-check-updates'`); `.ncurc.js` with `module.exports` no longer works in `"type": "module"` projects (use `.ncurc.cjs`). **Critical for supply chain: now ships `--cooldown <days>` to refuse versions younger than the specified age.** See [Supply-Chain Mitigation](#supply-chain-mitigation). |
|
|
40
40
|
|
|
41
41
|
### Reminders When Updating
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
|
44
44
|
|
|
45
45
|
2. **Update the table** with new versions and any relevant notes
|
|
46
46
|
|
|
47
|
-
3. **Search and update code examples
|
|
47
|
+
3. **Search and update code examples**—version numbers appear in:
|
|
48
48
|
|
|
49
49
|
- GitHub Actions workflows (CI and Release sections)
|
|
50
50
|
|
|
@@ -56,24 +56,24 @@ author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
|
56
56
|
|
|
57
57
|
- Appendices (complete examples)
|
|
58
58
|
|
|
59
|
-
4. **Verify compatibility
|
|
59
|
+
4. **Verify compatibility**—check that tools still work together
|
|
60
60
|
|
|
61
61
|
5. **Update the “Last Updated” date** at the top of the document
|
|
62
62
|
|
|
63
63
|
6. **Review “Open Research Questions”** section for any resolved items
|
|
64
64
|
|
|
65
65
|
7. **Honor the 14-day package-age rule** when bumping versions in code examples.
|
|
66
|
-
See [Supply-Chain Mitigation](#supply-chain-mitigation)
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
See [Supply-Chain Mitigation](#supply-chain-mitigation)—versions cited here should be
|
|
67
|
+
≥14 days old at the time the table is updated, except where a clearly-noted security
|
|
68
|
+
exception applies.
|
|
69
69
|
|
|
70
70
|
* * *
|
|
71
71
|
|
|
72
72
|
## Executive Summary
|
|
73
73
|
|
|
74
74
|
This research brief provides a comprehensive guide for setting up a modern TypeScript
|
|
75
|
-
monorepo using the **Bun ecosystem** end-to-end
|
|
76
|
-
|
|
75
|
+
monorepo using the **Bun ecosystem** end-to-end—Bun as runtime, package manager, bundler
|
|
76
|
+
(via Bunup), and test runner.
|
|
77
77
|
It serves as a direct comparison to the companion document on pnpm-based monorepos,
|
|
78
78
|
covering the same architectural scope but using Bun-native tooling wherever possible.
|
|
79
79
|
|
|
@@ -83,7 +83,7 @@ building ESM (or dual ESM/CJS) outputs with TypeScript declarations, **Changeset
|
|
|
83
83
|
(single-package repos) for versioning and release automation, **Biome** for formatting
|
|
84
84
|
and linting, **publint** for package validation, and **lefthook** for git hooks.
|
|
85
85
|
The architecture also covers Bun’s unique capability for **compiling standalone
|
|
86
|
-
executables
|
|
86
|
+
executables**—a native binary distribution path unavailable in the pnpm ecosystem.
|
|
87
87
|
|
|
88
88
|
**Research Questions**:
|
|
89
89
|
|
|
@@ -123,7 +123,7 @@ research stock.
|
|
|
123
123
|
|
|
124
124
|
## Research Findings
|
|
125
125
|
|
|
126
|
-
### 1. Package Manager
|
|
126
|
+
### 1. Package Manager and Workspace Structure
|
|
127
127
|
|
|
128
128
|
#### Bun Workspaces
|
|
129
129
|
|
|
@@ -141,13 +141,12 @@ research stock.
|
|
|
141
141
|
|
|
142
142
|
- Supports `workspace:*` protocol for inter-package references
|
|
143
143
|
|
|
144
|
-
- Does not use a content-addressable store like pnpm
|
|
145
|
-
`node_modules`
|
|
144
|
+
- Does not use a content-addressable store like pnpm—installs are flat in `node_modules`
|
|
146
145
|
|
|
147
146
|
- Missing some pnpm features: no `pnpm deploy`, less strict `node_modules` (phantom
|
|
148
147
|
dependencies possible)
|
|
149
148
|
|
|
150
|
-
- **Notable**: Bun was acquired by Anthropic on 2025-12-02
|
|
149
|
+
- **Notable**: Bun was acquired by Anthropic on 2025-12-02—Anthropic’s first
|
|
151
150
|
acquisition. Bun powers Claude Code (which ships as a `bun build --compile`
|
|
152
151
|
executable), the Claude Agent SDK, and other Anthropic AI tooling, signaling strong
|
|
153
152
|
ongoing investment and maintenance.
|
|
@@ -354,7 +353,7 @@ Bun to directly import TypeScript source files, bypassing compiled output entire
|
|
|
354
353
|
```
|
|
355
354
|
|
|
356
355
|
This means during development within the monorepo, Bun can consume TypeScript directly
|
|
357
|
-
without a build step
|
|
356
|
+
without a build step—a significant DX advantage over Node.js-based setups.
|
|
358
357
|
|
|
359
358
|
**Assessment**: This is a unique Bun advantage.
|
|
360
359
|
During local development, packages can be consumed without building.
|
|
@@ -568,8 +567,8 @@ export default defineConfig([
|
|
|
568
567
|
|
|
569
568
|
**Key details**:
|
|
570
569
|
|
|
571
|
-
- Set `clean: true` only on the first config
|
|
572
|
-
|
|
570
|
+
- Set `clean: true` only on the first config—subsequent configs must use `clean: false`
|
|
571
|
+
to avoid deleting earlier outputs.
|
|
573
572
|
- CLI binaries typically don’t need `dts` since they aren’t imported by consumers.
|
|
574
573
|
- The `banner` field adds the shebang line for direct execution.
|
|
575
574
|
|
|
@@ -634,7 +633,7 @@ has platform-specific issues.
|
|
|
634
633
|
|
|
635
634
|
* * *
|
|
636
635
|
|
|
637
|
-
### 4. Package Exports
|
|
636
|
+
### 4. Package Exports and Dual Module Support
|
|
638
637
|
|
|
639
638
|
#### Subpath Exports
|
|
640
639
|
|
|
@@ -690,7 +689,7 @@ This auto-generates:
|
|
|
690
689
|
|
|
691
690
|
**Manual configuration** (if not using auto-exports):
|
|
692
691
|
|
|
693
|
-
Identical to the pnpm research
|
|
692
|
+
Identical to the pnpm research—use the same `exports` structure with `"types"` before
|
|
694
693
|
`"default"` in each condition block.
|
|
695
694
|
|
|
696
695
|
**Bun export condition**: Optionally add a `"bun"` condition pointing to TypeScript
|
|
@@ -784,7 +783,7 @@ publint is runtime-agnostic.
|
|
|
784
783
|
|
|
785
784
|
* * *
|
|
786
785
|
|
|
787
|
-
### 7. Versioning
|
|
786
|
+
### 7. Versioning and Release Automation
|
|
788
787
|
|
|
789
788
|
#### Changesets (with Bun Workarounds)
|
|
790
789
|
|
|
@@ -904,7 +903,7 @@ No `tsx` dependency needed.
|
|
|
904
903
|
**Details**:
|
|
905
904
|
|
|
906
905
|
Bun ships with a built-in test runner that is API-compatible with Jest/Vitest.
|
|
907
|
-
It is extremely fast
|
|
906
|
+
It is extremely fast—roughly 2x faster than Node’s built-in test runner and
|
|
908
907
|
significantly faster than Jest or Vitest.
|
|
909
908
|
|
|
910
909
|
**Key features**:
|
|
@@ -1062,7 +1061,7 @@ maintain.
|
|
|
1062
1061
|
|
|
1063
1062
|
* * *
|
|
1064
1063
|
|
|
1065
|
-
### 9. Code Formatting
|
|
1064
|
+
### 9. Code Formatting and Linting
|
|
1066
1065
|
|
|
1067
1066
|
#### Biome
|
|
1068
1067
|
|
|
@@ -1300,7 +1299,7 @@ jobs:
|
|
|
1300
1299
|
```
|
|
1301
1300
|
|
|
1302
1301
|
**`biome ci` vs `biome check`**: Use `biome ci .` (via `"check:ci": "biome ci ."`) in CI
|
|
1303
|
-
workflows instead of `biome check .`. The `biome ci` command is stricter
|
|
1302
|
+
workflows instead of `biome check .`. The `biome ci` command is stricter—it errors on
|
|
1304
1303
|
formatting issues rather than just reporting them, and produces cleaner output for CI
|
|
1305
1304
|
logs. Use `biome check --write .` for local development (auto-fixes issues) and
|
|
1306
1305
|
`biome ci .` in CI (fails on any issue).
|
|
@@ -1315,7 +1314,7 @@ logs. Use `biome check --write .` for local development (auto-fixes issues) and
|
|
|
1315
1314
|
| Lint + format | Separate `format:check` + `lint:check` | Single `biome check` |
|
|
1316
1315
|
| Node.js setup | Required | Not required (Bun includes runtime) |
|
|
1317
1316
|
|
|
1318
|
-
**Assessment**: Bun CI is simpler
|
|
1317
|
+
**Assessment**: Bun CI is simpler—one setup action instead of two, and a single check
|
|
1319
1318
|
command instead of separate format and lint steps.
|
|
1320
1319
|
The text-based `bun.lock` (since Bun 1.2) is diffable in PRs, on par with
|
|
1321
1320
|
`pnpm-lock.yaml`.
|
|
@@ -1391,7 +1390,7 @@ projects that prefer manual version control and want provenance guarantees.
|
|
|
1391
1390
|
|
|
1392
1391
|
> **When to prefer this over Changesets (LLM-era note):** For a **single published
|
|
1393
1392
|
> package**, Changesets’ main wins (multi-package coordination, per-PR changelog
|
|
1394
|
-
> accumulation) mostly evaporate while its ceremony stays
|
|
1393
|
+
> accumulation) mostly evaporate while its ceremony stays—and Bun adds the extra
|
|
1395
1394
|
> `workspace:*` workarounds above.
|
|
1396
1395
|
> When releases are cut by an agent/maintainer who assembles notes from clean
|
|
1397
1396
|
> conventional commits at release time (see a release-notes template), tag-triggered
|
|
@@ -1473,11 +1472,11 @@ jobs:
|
|
|
1473
1472
|
|
|
1474
1473
|
**Release process with tag-triggered workflow**:
|
|
1475
1474
|
|
|
1476
|
-
1. `bun run changeset
|
|
1475
|
+
1. `bun run changeset`—create changeset
|
|
1477
1476
|
2. Commit the changeset file
|
|
1478
|
-
3. `bun run changeset version
|
|
1477
|
+
3. `bun run changeset version`—bumps versions, updates CHANGELOG
|
|
1479
1478
|
4. Commit the version bump
|
|
1480
|
-
5. `git tag vX.X.X && git push --tags
|
|
1479
|
+
5. `git tag vX.X.X && git push --tags`—triggers the release workflow
|
|
1481
1480
|
|
|
1482
1481
|
**Key advantages over Changesets Action**:
|
|
1483
1482
|
|
|
@@ -1514,7 +1513,7 @@ timing. The Changesets Action remains simpler for projects that prefer full auto
|
|
|
1514
1513
|
|
|
1515
1514
|
* * *
|
|
1516
1515
|
|
|
1517
|
-
### 11. Git Hooks
|
|
1516
|
+
### 11. Git Hooks and Local Validation
|
|
1518
1517
|
|
|
1519
1518
|
#### Lefthook with Biome
|
|
1520
1519
|
|
|
@@ -1724,7 +1723,7 @@ This is a unique advantage that the pnpm ecosystem does not offer natively.
|
|
|
1724
1723
|
|
|
1725
1724
|
npm-check-updates (`ncu`) works with Bun.
|
|
1726
1725
|
The main difference is using `bun install` instead of `pnpm install` after updates.
|
|
1727
|
-
**All upgrade commands must honor the 14-day package-age rule
|
|
1726
|
+
**All upgrade commands must honor the 14-day package-age rule**—see
|
|
1728
1727
|
[Supply-Chain Mitigation](#supply-chain-mitigation) for the rationale and exception
|
|
1729
1728
|
process. `ncu` v22+ ships a native `--cooldown` flag that enforces this directly.
|
|
1730
1729
|
|
|
@@ -1795,7 +1794,7 @@ Distribution options are identical:
|
|
|
1795
1794
|
|
|
1796
1795
|
1. **GitHub Packages** (recommended for teams)
|
|
1797
1796
|
|
|
1798
|
-
2. **Direct GitHub install**: `bun add github:org/repo
|
|
1797
|
+
2. **Direct GitHub install**: `bun add github:org/repo`—Bun has limited support for
|
|
1799
1798
|
monorepo subdirectory installs (lagging behind pnpm)
|
|
1800
1799
|
|
|
1801
1800
|
3. **Local linking**: `bun link` works similarly to `pnpm link`
|
|
@@ -1825,13 +1824,13 @@ in core library code if the library needs to work in Node.js environments.
|
|
|
1825
1824
|
Supply-chain hardening applies to **every repo, not just new monorepos**, so the full
|
|
1826
1825
|
policy and hands-on enforcement now live in a standalone guideline:
|
|
1827
1826
|
**`tbd guidelines supply-chain-hardening`**. It covers the cross-ecosystem 14-day
|
|
1828
|
-
cool-off plus the Node/pnpm/Bun specifics
|
|
1827
|
+
cool-off plus the Node/pnpm/Bun specifics—lifecycle-script allowlists, lockfile
|
|
1829
1828
|
discipline, `npm-check-updates --cooldown 14`, the CI audit gate, and the
|
|
1830
1829
|
`check-package-age` pre-push guard.
|
|
1831
1830
|
Deeper background and the named-incident watch list:
|
|
1832
1831
|
<https://github.com/jlevy/supply-chain-hardening>.
|
|
1833
1832
|
|
|
1834
|
-
**Bun specifics**: Bun blocks lifecycle scripts by default
|
|
1833
|
+
**Bun specifics**: Bun blocks lifecycle scripts by default—extend the allowlist via
|
|
1835
1834
|
`trustedDependencies` in `package.json`, run `bun audit` in CI with
|
|
1836
1835
|
`bun install --frozen-lockfile`, and commit `bun.lock`. Bun has no native release-age
|
|
1837
1836
|
gate yet, so enforce the 14-day cool-off with `bunx npm-check-updates --cooldown 14`.
|
|
@@ -1911,13 +1910,13 @@ gate yet, so enforce the 14-day cool-off with `bunx npm-check-updates --cooldown
|
|
|
1911
1910
|
7. **Use `bun publish` per package** instead of `changeset publish` to ensure proper
|
|
1912
1911
|
workspace resolution.
|
|
1913
1912
|
|
|
1914
|
-
8. **Run CLI from source with `bun`** directly
|
|
1913
|
+
8. **Run CLI from source with `bun`** directly—no need for `tsx` or any TypeScript
|
|
1915
1914
|
execution wrapper.
|
|
1916
1915
|
|
|
1917
1916
|
9. **Consider `bun --compile`** for distributing CLI tools as standalone executables,
|
|
1918
1917
|
especially for users who don’t have Node.js or Bun installed.
|
|
1919
1918
|
|
|
1920
|
-
10. **Use `bun test`** for testing
|
|
1919
|
+
10. **Use `bun test`** for testing—fake timers are now supported (v1.3.4+). Switch to
|
|
1921
1920
|
Vitest only if you need test isolation, browser mode, or sharding.
|
|
1922
1921
|
|
|
1923
1922
|
11. **Keep the root `package.json` private** with `"private": true` and only workspace
|
|
@@ -1934,23 +1933,23 @@ gate yet, so enforce the 14-day cool-off with `bunx npm-check-updates --cooldown
|
|
|
1934
1933
|
15. **Add the `"bun"` export condition** to let Bun consumers import TypeScript source
|
|
1935
1934
|
directly, bypassing compiled output.
|
|
1936
1935
|
|
|
1937
|
-
16. **Use dynamic git-based versioning** for dev builds
|
|
1936
|
+
16. **Use dynamic git-based versioning** for dev builds—the pattern works identically
|
|
1938
1937
|
with Bun, and `bun` replaces `tsx` for script execution.
|
|
1939
1938
|
|
|
1940
|
-
17. **Use `biome ci`** in CI workflows instead of `biome check
|
|
1939
|
+
17. **Use `biome ci`** in CI workflows instead of `biome check`—it’s stricter and
|
|
1941
1940
|
produces cleaner output for CI logs.
|
|
1942
1941
|
|
|
1943
1942
|
18. **Consider ESM-only output** for Bun-native CLI tools and packages targeting modern
|
|
1944
1943
|
Node.js (>=22). Only add CJS if specific consumers require it.
|
|
1945
1944
|
|
|
1946
|
-
19. **Use flowmark** for Markdown formatting in pre-commit hooks
|
|
1945
|
+
19. **Use flowmark** for Markdown formatting in pre-commit hooks—Biome does not format
|
|
1947
1946
|
Markdown, and consistent Markdown formatting improves documentation quality.
|
|
1948
1947
|
|
|
1949
|
-
20. **Add golden/CLI tests** with `tryscript` alongside `bun test` for CLI tools
|
|
1948
|
+
20. **Add golden/CLI tests** with `tryscript` alongside `bun test` for CLI tools—they
|
|
1950
1949
|
catch regressions in help text, output formatting, and argument parsing.
|
|
1951
1950
|
|
|
1952
1951
|
21. **Consider tag-triggered OIDC releases** as an alternative to the Changesets GitHub
|
|
1953
|
-
Action
|
|
1952
|
+
Action—they provide npm provenance attestation and automatic GitHub Releases.
|
|
1954
1953
|
|
|
1955
1954
|
* * *
|
|
1956
1955
|
|
|
@@ -2000,7 +1999,7 @@ gate yet, so enforce the 14-day cool-off with `bunx npm-check-updates --cooldown
|
|
|
2000
1999
|
`@typescript/native-preview` (binary `tsgo`). Claims ~10x type-check speed and ~3x
|
|
2001
2000
|
less memory; passes 95%+ of the test suite.
|
|
2002
2001
|
Available in Visual Studio 2026 18.6 Insiders by default.
|
|
2003
|
-
**Do not adopt for production builds yet
|
|
2002
|
+
**Do not adopt for production builds yet**—wait for stable (expected mid-to-late
|
|
2004
2003
|
2026). May change the DTS generation landscape for Bunup and tsdown once stable.
|
|
2005
2004
|
|
|
2006
2005
|
9. **Bunup maturity**: Bunup is iterating rapidly (0.16.31 as of May 2026, up from 0.4.x
|
|
@@ -2024,7 +2023,7 @@ gate yet, so enforce the 14-day cool-off with `bunx npm-check-updates --cooldown
|
|
|
2024
2023
|
`--audit-level=<low|moderate|high|critical>` and `--json` output.
|
|
2025
2024
|
Should be a required CI check (see
|
|
2026
2025
|
[Supply-Chain Mitigation](#supply-chain-mitigation)). A `bun audit signatures`
|
|
2027
|
-
equivalent to npm’s signature verification has not yet shipped
|
|
2026
|
+
equivalent to npm’s signature verification has not yet shipped—monitor.
|
|
2028
2027
|
|
|
2029
2028
|
* * *
|
|
2030
2029
|
|
|
@@ -2112,7 +2111,7 @@ testing features.
|
|
|
2112
2111
|
|
|
2113
2112
|
- [publint Documentation](https://publint.dev/docs/)
|
|
2114
2113
|
|
|
2115
|
-
### Guides
|
|
2114
|
+
### Guides and Articles
|
|
2116
2115
|
|
|
2117
2116
|
- [Building a TypeScript Library in 2026 with Bunup](https://dev.to/arshadyaseen/building-a-typescript-library-in-2026-with-bunup-3bmg)
|
|
2118
2117
|
|
|
@@ -2304,10 +2303,10 @@ testing features.
|
|
|
2304
2303
|
[Supply-Chain Mitigation](#supply-chain-mitigation) policy.
|
|
2305
2304
|
Newer releases may exist (`@biomejs/biome` 2.4.15, `lefthook` 2.1.8,
|
|
2306
2305
|
`npm-check-updates` 22.2.0) but were too fresh at the time of this document update.
|
|
2307
|
-
- `"check:ci": "biome ci ."` uses `biome ci` (stricter than `biome check
|
|
2306
|
+
- `"check:ci": "biome ci ."` uses `biome ci` (stricter than `biome check`—errors on
|
|
2308
2307
|
formatting issues)
|
|
2309
2308
|
- `bun run --filter '*' <script>` delegates to each workspace package
|
|
2310
|
-
- The `@changesets/changelog-github` dependency is optional
|
|
2309
|
+
- The `@changesets/changelog-github` dependency is optional—use
|
|
2311
2310
|
`"changelog": "@changesets/cli/changelog"` in `.changeset/config.json` for a simpler
|
|
2312
2311
|
built-in changelog generator
|
|
2313
2312
|
- `npm-check-updates` v22+ is pure ESM. Add `--cooldown 14` to every invocation per the
|
|
@@ -2406,7 +2405,7 @@ testing features.
|
|
|
2406
2405
|
|
|
2407
2406
|
- `"organizeImports": { "enabled": true }` →
|
|
2408
2407
|
`"assist": { "actions": { "source": { "organizeImports": "on" } } }`
|
|
2409
|
-
- `"vcs"` configuration is new
|
|
2408
|
+
- `"vcs"` configuration is new—enables `.gitignore`-based file exclusion
|
|
2410
2409
|
- `"files.ignoreUnknown": true` prevents errors on unsupported file types
|
|
2411
2410
|
- `"files.includes"` with negation patterns replaces the old `"files.ignore"` array
|
|
2412
2411
|
(both still work, but `includes` with negation is more flexible)
|
|
@@ -2629,7 +2628,7 @@ export default defineConfig([
|
|
|
2629
2628
|
}
|
|
2630
2629
|
```
|
|
2631
2630
|
|
|
2632
|
-
### Appendix G: Case Study
|
|
2631
|
+
### Appendix G: Case Study—craft-agents-oss (Real-World Bun Monorepo)
|
|
2633
2632
|
|
|
2634
2633
|
**Repository**:
|
|
2635
2634
|
[lukilabs/craft-agents-oss](https://github.com/lukilabs/craft-agents-oss) (Apache 2.0,
|
|
@@ -2678,7 +2677,7 @@ craft-agents-oss/
|
|
|
2678
2677
|
|
|
2679
2678
|
**1. Source-Level Exports (No Build Step for Internal Consumption)**
|
|
2680
2679
|
|
|
2681
|
-
All internal packages export TypeScript source directly
|
|
2680
|
+
All internal packages export TypeScript source directly—no `dist/` directory:
|
|
2682
2681
|
|
|
2683
2682
|
```json
|
|
2684
2683
|
// packages/shared/package.json — 27 subpath exports
|
|
@@ -2745,10 +2744,10 @@ Changesets entirely for a private monorepo that distributes as a single Electron
|
|
|
2745
2744
|
|
|
2746
2745
|
Instead of Biome, this project uses ESLint with 4 custom rules:
|
|
2747
2746
|
|
|
2748
|
-
- `no-direct-navigation-state
|
|
2749
|
-
- `no-localstorage
|
|
2750
|
-
- `no-direct-platform-check
|
|
2751
|
-
- `no-hardcoded-path-separator
|
|
2747
|
+
- `no-direct-navigation-state`—enforces navigation state abstraction
|
|
2748
|
+
- `no-localstorage`—prevents browser localStorage usage (Bun incompatible)
|
|
2749
|
+
- `no-direct-platform-check`—enforces platform detection abstraction
|
|
2750
|
+
- `no-hardcoded-path-separator`—enforces cross-platform paths
|
|
2752
2751
|
|
|
2753
2752
|
This demonstrates that ESLint’s custom rule ecosystem remains valuable for
|
|
2754
2753
|
domain-specific architectural constraints that Biome cannot yet replicate.
|
|
@@ -2797,7 +2796,7 @@ The source-level exports pattern and TypeScript build scripts are directly appli
|
|
|
2797
2796
|
The hybrid esbuild/Vite approach is more relevant for Electron/full-stack apps than for
|
|
2798
2797
|
pure library packages where Bunup would be simpler.
|
|
2799
2798
|
|
|
2800
|
-
### Appendix H: Case Study
|
|
2799
|
+
### Appendix H: Case Study—clam (Published Bun CLI Monorepo)
|
|
2801
2800
|
|
|
2802
2801
|
**Repository**: [jlevy/clam](https://github.com/jlevy/clam) (MIT, shell-like interface
|
|
2803
2802
|
for Claude Code and other agents)
|
|
@@ -2866,7 +2865,7 @@ export default defineConfig([
|
|
|
2866
2865
|
```
|
|
2867
2866
|
|
|
2868
2867
|
Separate configs for library (with DTS) and CLI binary (with shebang, dependency
|
|
2869
|
-
bundling, no DTS). ESM-only output
|
|
2868
|
+
bundling, no DTS). ESM-only output—no CJS.
|
|
2870
2869
|
|
|
2871
2870
|
**2. Tag-Triggered OIDC Release with npm Provenance**
|
|
2872
2871
|
|
|
@@ -2897,7 +2896,7 @@ new Biome 2.x `"assist"` API for import organization.
|
|
|
2897
2896
|
The base config enables `isolatedDeclarations: true` (as recommended), but the package
|
|
2898
2897
|
overrides it to `false` because its types are complex enough to require the full
|
|
2899
2898
|
TypeScript compiler for DTS generation.
|
|
2900
|
-
This is a valid pattern
|
|
2899
|
+
This is a valid pattern—enable in the base, override in packages that need it.
|
|
2901
2900
|
|
|
2902
2901
|
#### What This Repo Uses
|
|
2903
2902
|
|