@code-yeongyu/senpi-codemode 2026.10.31-1 → 2026.10.31-2

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -4
  4. package/src/extension/host-facts.ts +81 -3
  5. package/src/extension/skill-contribution.ts +6 -6
  6. package/src/prompt/eval-prompt-template.ts +1 -1
  7. package/src/prompt/eval-prompt.ts +4 -2
  8. package/src/skill/bun/SKILL.md +53 -0
  9. package/src/skill/{bun-1-4 → bun}/references/breaking-changes.md +26 -4
  10. package/src/skill/{bun-1-4 → bun}/references/build.md +32 -6
  11. package/src/skill/bun/references/dev-tooling.md +67 -0
  12. package/src/skill/bun/references/http-networking.md +157 -0
  13. package/src/skill/bun/references/install.md +137 -0
  14. package/src/skill/{bun-1-4 → bun}/references/node-compat-platforms.md +16 -2
  15. package/src/skill/{bun-1-4 → bun}/references/performance.md +9 -3
  16. package/src/skill/{bun-1-4 → bun}/references/runtime-apis.md +38 -13
  17. package/src/skill/bun/references/security.md +74 -0
  18. package/src/skill/bun/references/session-recipes.md +71 -0
  19. package/src/skill/bun/references/test.md +132 -0
  20. package/src/tool/detached-cell-manager.ts +50 -3
  21. package/src/tool/eval-argument-validator.ts +59 -0
  22. package/src/tool/eval-request.ts +15 -9
  23. package/src/tool/eval-tool-options.ts +1 -1
  24. package/src/tool/eval-tool.ts +2 -0
  25. package/src/skill/bun-1-4/SKILL.md +0 -45
  26. package/src/skill/bun-1-4/references/dev-tooling.md +0 -34
  27. package/src/skill/bun-1-4/references/http-networking.md +0 -113
  28. package/src/skill/bun-1-4/references/install.md +0 -93
  29. package/src/skill/bun-1-4/references/security.md +0 -52
  30. package/src/skill/bun-1-4/references/test.md +0 -103
@@ -0,0 +1,132 @@
1
+ # bun test in 1.4.x
2
+
3
+ Composable CI recipe:
4
+
5
+ ```bash
6
+ bun test --changed=main # only what your branch touches
7
+ bun test --parallel --timings=timings.json --update-timings
8
+ bun test --parallel --shard=1/3 --timings=timings.json # in CI, per machine
9
+ bun test --check # type-check test files + imports first; no tests run on a type error [v1.4.3]
10
+ ```
11
+
12
+ ## Traps — each one cost a past session hours
13
+
14
+ | Trap | Fact | Fix |
15
+ |---|---|---|
16
+ | Timeout reaches only the first file | `setDefaultTimeout()` in a preload is file-scoped (first file only); bunfig `[test] timeout` is silently ignored (bun#7789). Only slow hosts push tests past 5 s, so it looks like a Windows flake (2026-10-20) | `bun test --timeout 30000` on the command line, or a per-test third argument (`0` = no limit) |
17
+ | Zombie killer takes your DB with it | When a test times out, Bun SIGTERMs every child the test process spawned, including an embedded Postgres started in `beforeAll`; every later test in the file fails with "shutting down" and the failing set changes with host load (2026-10-23) | raise the budget for DB suites, or start the server outside the test process |
18
+ | Path without `./` is a name filter | `bun test path/file.test.ts` matches files whose path CONTAINS that string; `bun test ./path/file.test.ts` runs exactly that file | start exact paths with `./` or `/`; `-t <regex>` filters by test name |
19
+ | `expect(p).rejects` hangs after a Worker reply | `await expect(p).rejects…` reached from the continuation of an awaited Worker message deadlocks (oven-sh/bun#37189, still on 1.4.3) | `const err = await p.then(() => undefined, (e) => e)` then assert on `err` |
20
+ | `mock.module()` reaches every importer | mocking a re-exporting entry replaces the function for modules importing it by relative path too; one file's `mock.module` of a shared logger breaks another file's `spyOn` in the same run (2026-10-26/27) | copy the real namespace first (`{ ...(await import(x)) }`), keep such tests in their own process (`bun test ./x.isolated.test.ts`) or run `--isolate` |
21
+ | Preload patch of a builtin is invisible | a preload's `require("node:os").x = …` reaches product `import * as os` only if nothing ESM-imported that builtin before the patch; Bun snapshots the namespace at first ESM import and `module.syncBuiltinESMExports()` does not help (2026-10-29) | patch before any ESM import of the builtin, or use the product's env seam |
22
+ | Respawned `"bun"` is not this Bun | PATH can resolve to a mise shim or a stray older `bun` in a global `.bin`; the test's `calls.log` ENOENT hid a `--version` exit 1 (2026-09-28, 10-29) | spawn `process.execPath`; surface the child's stderr when a stub log is missing |
23
+ | Env-less child misses runtime env | `Bun.spawn` without `env` gives the child the STARTUP environment | `env: process.env` — see [session-recipes](session-recipes.md) |
24
+ | Timed-out test keeps running | the per-test timeout marks the test failed but its body keeps executing; a body awaiting a never-settling promise is not ended at all, and `onTestFinished` runs while it is still awaiting (2026-10-12/16) | own async work in a hook registered before fixture removal; wrap a never-settles RED in an outer `timeout -k 5 <s>` |
25
+ | Shared-helper hooks bind once | an `afterEach`/`afterAll` at module scope of a helper several files import binds only to the FIRST importer; 16 files skipped cleanup and left 106 temp dirs (2026-10-10) | register per call with `onTestFinished(() => …)` inside the helper |
26
+ | `bun run --cwd pkg test file` runs everything | bun appends your positional AFTER the script's own (`vitest run test/ file`), so the whole suite runs (2026-09-21); `bun test` on a vitest-based package hung 12 h (2026-10-27) | invoke the package's runner directly for one file; check which runner a package uses before `bun test` |
27
+ | Root `pathIgnorePatterns` hides a package | a root bunfig pattern silently excluded a workspace's suite; the run was green with 0 of its files (2026-09-27) | run the package's own script or explicit paths and verify the discovered file count |
28
+ | Duplicate package.json key | every child prints `Duplicate key` on stderr, failing stderr-asserting tests (2026-10-24) | fix the JSON |
29
+
30
+ Flake tools: `bun test --rerun-each 20 ./x.test.ts` runs a file N times in one process (order/leak bugs); `--randomize --seed N` reproduces order-dependent failures; `--only-failures` re-runs what failed last time; `test.failing("name", fn)` inverts a known-red test so it alerts when it starts passing; `--bail` stops at the first failure (also stops every `--parallel` worker).
31
+
32
+ ## --parallel: worker processes [v1.3.13, improved v1.4.0]
33
+
34
+ Blog: [#bun-test-parallel](https://bun.com/blog/bun-v1.4#bun-test-parallel)
35
+
36
+ ```bash
37
+ bun test --parallel # N = CPU count
38
+ bun test --parallel=4
39
+ ```
40
+
41
+ - Files go to whichever worker frees up next. Coverage and JUnit output merged across workers; `--bail` stops every worker.
42
+ - `--parallel` implies `--isolate`; `--no-isolate` opts out.
43
+ - Workers expose 1-indexed `JEST_WORKER_ID` / `BUN_TEST_WORKER_ID`, so Jest setups keying DBs/ports off worker id work unchanged.
44
+ - Preload scripts with top-level `await` complete before any worker runs tests.
45
+
46
+ ## --isolate: fresh global per file [v1.3.13, hardened v1.4.0]
47
+
48
+ Blog: [#bun-test-isolate](https://bun.com/blog/bun-v1.4#bun-test-isolate)
49
+
50
+ ```bash
51
+ bun test --isolate
52
+ ```
53
+
54
+ Between files, Bun creates a new `globalThis`, clears ESM/CJS module registries, closes leaked servers/sockets/watchers/subprocesses, cancels timers, restores fake timers, and re-runs `--preload`. Transpiled source/bytecode stay cached process-wide, so only top-level code re-runs. This is how Jest/Vitest behave by default — use it to kill "passes alone, fails in the full suite" bugs. v1.4.0 fixed the 1.3.13 stability issues (fake-timer leaks, module-scope subprocess leaks, `process.chdir` bleed, N-API across files, debugger breakpoints); v1.4.1 stopped files that call `mock()`, `spyOn()`, `mock.module()` or `Bun.plugin()` from keeping their module graph alive (80 mocked files: 1591 MB → 215 MB) and stopped `TZ`, proxy and TLS settings from leaking into later files.
55
+
56
+ ## --shard: split across CI machines [v1.3.13]
57
+
58
+ Blog: [#bun-test-shard](https://bun.com/blog/bun-v1.4#bun-test-shard)
59
+
60
+ ```bash
61
+ bun test --shard=1/3 # deterministic, round-robin, 1-based (matches Jest/Vitest/Playwright)
62
+ ```
63
+
64
+ Works with `--changed` and `--randomize`; an empty shard exits 0.
65
+
66
+ ## --timings: balance by wall time [v1.4.0]
67
+
68
+ Blog: [#bun-test-timings](https://bun.com/blog/bun-v1.4#bun-test-timings)
69
+
70
+ ```bash
71
+ bun test --timings=timings.json --update-timings # record per-file durations
72
+ bun test --shard=1/3 --timings=timings.json # shards cut by equal time, not file count
73
+ bun test --parallel --timings=timings.json # workers start slowest file first (LPT scheduling)
74
+ ```
75
+
76
+ The timings file is written slowest-first, so it doubles as a slow-test report. Files sharing imports stay together (warm module cache).
77
+
78
+ ## --changed: only affected tests [v1.3.13]
79
+
80
+ Blog: [#bun-test-changed](https://bun.com/blog/bun-v1.4#bun-test-changed)
81
+
82
+ ```bash
83
+ bun test --changed # uncommitted (unstaged + staged + untracked)
84
+ bun test --changed=main # diff against branch/commit/tag
85
+ bun test --changed --watch # re-filters on every restart
86
+ ```
87
+
88
+ Bun scans test imports, asks git what changed, walks the import graph backwards. tsconfig `paths` aliases (`@/*`) work. Vitest-compatible flag.
89
+
90
+ ## --retry and repeats [v1.3.3]
91
+
92
+ Blog: [#bun-test-retry](https://bun.com/blog/bun-v1.4#bun-test-retry)
93
+
94
+ ```ts
95
+ test("flaky network call", async () => { await fetch("https://example.com"); }, { retry: 5 });
96
+ test("stress", () => { if (Math.random() < 0.1) throw new Error("uh oh!"); }, { repeats: 20 });
97
+ ```
98
+
99
+ `bun test --retry <N>` sets a suite-wide default. NOTE: prefer fixing the flake (`--rerun-each`, `--randomize --seed`, `--isolate` find it); retry is a containment tool.
100
+
101
+ ## jest.useFakeTimers() [v1.3.4, improved v1.4.0]
102
+
103
+ Blog: [#jest-usefaketimers](https://bun.com/blog/bun-v1.4#jest-usefaketimers)
104
+
105
+ ```ts
106
+ import { jest, test, expect } from "bun:test";
107
+ test("debounce", () => {
108
+ jest.useFakeTimers();
109
+ let called = 0;
110
+ setTimeout(() => called++, 1000);
111
+ jest.advanceTimersByTime(1000);
112
+ expect(called).toBe(1);
113
+ jest.useRealTimers();
114
+ });
115
+ ```
116
+
117
+ - Controls `setTimeout`, `setInterval`, `Date`. `jest.setSystemTime()` works with `advanceTimersByTime()`.
118
+ - `@testing-library/react`'s `waitFor` detects fake timers and advances instead of sleeping. `Bun.cron` schedules can be driven by the fake clock.
119
+
120
+ ## Also in bun:test
121
+
122
+ - `expectTypeOf<T>()` type assertions (Vitest-compatible); `test.if/skipIf/todoIf(cond)`, `test.each([...])`, `describe.each`; `onTestFinished(fn)` runs after all `afterEach` hooks (serial tests only).
123
+ - `bun test --inspect` / `--inspect-brk` attaches the debugger to the runner; `--reporter=junit --reporter-outfile=x.xml`; inside GitHub Actions, failures are emitted as annotations automatically.
124
+ - `--coverage-reporter=lcov` implies `--coverage` [v1.4.3]. v1.4.4 stopped counting skipped branches (`||`/`??`/`?.` right sides, code after an `await` that never resumed) and implicit constructors as covered — `% Lines`/`% Funcs` can drop after upgrading, so re-check `coverageThreshold`.
125
+ - `--update-snapshots` keeps the old file when a run ends early (`--bail`) [v1.4.4].
126
+
127
+ ## Behavior changes in 1.4.x (test-related)
128
+
129
+ - `jest.resetAllMocks()` now drops implementations too (matches Jest); use `clearAllMocks()` for history-only.
130
+ - `expect().toContain()` compares with `===` not `Object.is` (`[NaN]` no longer contains `NaN`).
131
+ - `toEqual()` compares `Temporal` objects by value.
132
+ - Coverage percentages are stricter in v1.4.4 (above).
@@ -38,12 +38,52 @@ export type {
38
38
  export class EvalCellNotFoundError extends Error {
39
39
  readonly code = "eval_cell_not_found";
40
40
 
41
- constructor(cellId: string) {
42
- super(`Unknown detached eval cell "${cellId}"`);
41
+ constructor(cellId: string, suggestion?: string) {
42
+ super(
43
+ suggestion === undefined
44
+ ? `Unknown detached eval cell "${cellId}"`
45
+ : `Unknown detached eval cell "${cellId}"; did you mean "${suggestion}"?`,
46
+ );
43
47
  this.name = "EvalCellNotFoundError";
44
48
  }
45
49
  }
46
50
 
51
+ function editDistance(a: string, b: string): number {
52
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
53
+ for (let i = 1; i <= a.length; i++) {
54
+ const current = [i];
55
+ for (let j = 1; j <= b.length; j++) {
56
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
57
+ current.push(Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution));
58
+ }
59
+ previous = current;
60
+ }
61
+ return previous[b.length] ?? 0;
62
+ }
63
+
64
+ /**
65
+ * The one known id within a quarter of the mistyped id's length (at least 1 edit), or undefined when
66
+ * none is that close or two or more tie for closest, so a suggestion never picks between candidates.
67
+ */
68
+ function closestCellId(cellId: string, known: Iterable<string>): string | undefined {
69
+ const limit = Math.max(1, Math.floor(cellId.length / 4));
70
+ let best: string | undefined;
71
+ let bestDistance = Number.POSITIVE_INFINITY;
72
+ let tied = false;
73
+ for (const candidate of known) {
74
+ const distance = editDistance(cellId, candidate);
75
+ if (distance > limit) continue;
76
+ if (distance < bestDistance) {
77
+ best = candidate;
78
+ bestDistance = distance;
79
+ tied = false;
80
+ } else if (distance === bestDistance) {
81
+ tied = true;
82
+ }
83
+ }
84
+ return tied ? undefined : best;
85
+ }
86
+
47
87
  export class EvalDetachedCellManager {
48
88
  readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
49
89
  readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
@@ -317,7 +357,14 @@ export class EvalDetachedCellManager {
317
357
 
318
358
  #terminal(cellId: string): EvalDetachedCellSnapshot {
319
359
  const snapshot = this.#terminalSnapshots.get(cellId);
320
- if (snapshot === undefined) throw new EvalCellNotFoundError(cellId);
360
+ if (snapshot === undefined) {
361
+ const known = new Set([
362
+ ...this.#cells.keys(),
363
+ ...this.#interrupting.keys(),
364
+ ...this.#terminalSnapshots.list().map((entry) => entry.cellId),
365
+ ]);
366
+ throw new EvalCellNotFoundError(cellId, closestCellId(cellId, known));
367
+ }
321
368
  return snapshot;
322
369
  }
323
370
  }
@@ -0,0 +1,59 @@
1
+ import { type JsonObject, registerToolArgumentValidator, validateToolArguments } from "@earendil-works/pi-ai";
2
+ import type { TSchema } from "typebox";
3
+ import { type EvalRunField, evalRunFieldRequiredMessage, normalizeEvalSummary } from "./eval-request.ts";
4
+ import type { EvalLanguage } from "./types.ts";
5
+
6
+ type EvalArgumentValidator = Parameters<typeof registerToolArgumentValidator>[1];
7
+ type EvalArgumentError = NonNullable<EvalArgumentValidator["errors"]>[number];
8
+
9
+ const RUN_FIELDS: readonly EvalRunField[] = ["language", "code", "summary"];
10
+
11
+ /**
12
+ * Owns eval argument validation (senpi#5138). A run call (action omitted or "run") missing a run field fails with
13
+ * one teaching error per missing field, before the generic anyOf validation whose per-branch dump a model cannot act
14
+ * on. Every other call goes through the generic validation and coercion unchanged: a failure there rethrows the generic
15
+ * path's own error, so its message (and the `Received arguments` block) is byte-for-byte what an unregistered schema gets.
16
+ */
17
+ export function registerEvalArgumentValidator(schema: TSchema, languages: readonly EvalLanguage[]): void {
18
+ // The same schema under another identity, so the generic path validates it instead of calling back here.
19
+ const generic: TSchema = Object.create(Object.getPrototypeOf(schema), Object.getOwnPropertyDescriptors(schema));
20
+ const validate: EvalArgumentValidator = (value) => {
21
+ const missing = missingRunFieldErrors(value, languages);
22
+ if (missing.length > 0) {
23
+ validate.errors = missing;
24
+ return false;
25
+ }
26
+ // Throws the generic validation error unchanged when the call is invalid for any other reason.
27
+ const coerced: unknown = validateToolArguments(
28
+ { name: "eval", description: "", parameters: generic },
29
+ { type: "toolCall", id: "", name: "eval", arguments: value as JsonObject },
30
+ );
31
+ // A registered validator's input is what the caller returns, so the generic coercion lands in place.
32
+ if (isRecord(value) && isRecord(coerced)) {
33
+ for (const key of Object.keys(value)) delete value[key];
34
+ Object.assign(value, coerced);
35
+ }
36
+ validate.errors = null;
37
+ return true;
38
+ };
39
+ registerToolArgumentValidator(schema, validate);
40
+ }
41
+
42
+ function missingRunFieldErrors(value: unknown, languages: readonly EvalLanguage[]): EvalArgumentError[] {
43
+ if (!isRecord(value) || (value.action !== undefined && value.action !== "run")) return [];
44
+ return RUN_FIELDS.filter((field) => isMissing(field, value[field])).map((field) => ({
45
+ instancePath: "",
46
+ keyword: "required",
47
+ message: evalRunFieldRequiredMessage(field, languages),
48
+ params: { missingProperty: field },
49
+ }));
50
+ }
51
+
52
+ function isMissing(field: EvalRunField, value: unknown): boolean {
53
+ if (value === undefined || value === null) return true;
54
+ return field === "summary" && typeof value === "string" && normalizeEvalSummary(value) === undefined;
55
+ }
56
+
57
+ function isRecord(value: unknown): value is Record<string, unknown> {
58
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59
+ }
@@ -40,16 +40,12 @@ export function parseEvalRequest(
40
40
  }
41
41
  if (params.action !== undefined && params.action !== "run")
42
42
  throw new TypeError(`Unknown eval action "${String(params.action)}"`);
43
- if (params.language === undefined) throw new TypeError(evalRunRequiresLanguageMessage(enabledLanguages));
43
+ if (params.language === undefined) throw new TypeError(evalRunFieldRequiredMessage("language", enabledLanguages));
44
44
  if (!isEvalLanguage(params.language))
45
45
  throw new TypeError(`eval run language must be one of: ${enabledLanguages.join(", ")}`);
46
- if (typeof params.code !== "string")
47
- throw new TypeError("eval run requires code: the cell body to execute, verbatim");
46
+ if (typeof params.code !== "string") throw new TypeError(evalRunFieldRequiredMessage("code", enabledLanguages));
48
47
  const summary = normalizeEvalSummary(params.summary);
49
- if (summary === undefined)
50
- throw new TypeError(
51
- "eval run requires summary (one line in the user's language): what you are working on and for what purpose",
52
- );
48
+ if (summary === undefined) throw new TypeError(evalRunFieldRequiredMessage("summary", enabledLanguages));
53
49
  if (params.on_timeout !== undefined && params.on_timeout !== "detach" && params.on_timeout !== "error")
54
50
  throw new TypeError(`Unknown eval on_timeout value "${String(params.on_timeout)}"`);
55
51
  if (params.isolate !== undefined && params.isolate !== false) {
@@ -90,6 +86,16 @@ function isEvalLanguage(value: unknown): value is EvalToolInput["language"] {
90
86
  return value === "py" || value === "js" || value === "rb" || value === "jl";
91
87
  }
92
88
 
93
- function evalRunRequiresLanguageMessage(languages: readonly EvalLanguage[]): string {
94
- return `eval run requires language: one of ${languages.map((language) => `"${language}"`).join(", ")}`;
89
+ export type EvalRunField = "language" | "code" | "summary";
90
+
91
+ /** The teaching error for a run call missing one of its fields, shared by argument validation and execution. */
92
+ export function evalRunFieldRequiredMessage(field: EvalRunField, languages: readonly EvalLanguage[]): string {
93
+ switch (field) {
94
+ case "language":
95
+ return `eval run requires language: one of ${languages.map((language) => `"${language}"`).join(", ")}`;
96
+ case "code":
97
+ return "eval run requires code: the cell body to execute, verbatim";
98
+ case "summary":
99
+ return "eval run requires summary (one line in the user's language): what you are working on and for what purpose";
100
+ }
95
101
  }
@@ -67,7 +67,7 @@ export interface CreateEvalToolOptions {
67
67
  readonly hostLine?: string;
68
68
  /** Display identity of each language's runtime, shown in headers and details; `js` also selects the prompt's runtime line. */
69
69
  readonly runtimes?: EvalRuntimes;
70
- /** Absolute path of the active bun-1-4 skill; the prompt names it as MUST READ on a bun kernel. */
70
+ /** Absolute path of the active bun skill; the prompt names it as MUST READ on a bun kernel. */
71
71
  readonly bunSkillPath?: string;
72
72
  /** Kernel globals of the tools active when a cell is submitted; read once per cell. */
73
73
  readonly kernelPreludes?: () => readonly KernelPreludeContribution[];
@@ -8,6 +8,7 @@ import {
8
8
  } from "../config/settings.ts";
9
9
  import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
10
10
  import { EvalDetachedCellManager } from "./detached-cell-manager.ts";
11
+ import { registerEvalArgumentValidator } from "./eval-argument-validator.ts";
11
12
  import { executeEvalControl } from "./detached-eval-result.ts";
12
13
  import {
13
14
  EvalIsolateInvalidError,
@@ -66,6 +67,7 @@ export function createEvalTool(options: CreateEvalToolOptions) {
66
67
  ...(options.promptKernelPreludes === undefined ? {} : { kernelPreludes: options.promptKernelPreludes }),
67
68
  });
68
69
  const languages = enabledLanguageList(options.enabledLanguages);
70
+ registerEvalArgumentValidator(parameters, languages);
69
71
  const cellManager =
70
72
  options.cellManager ??
71
73
  new EvalDetachedCellManager({
@@ -1,45 +0,0 @@
1
- ---
2
- name: bun-1-4
3
- description: "Read before a js eval cell that installs a package, spawns a server or PTY, or starts a long run, and before any bun -e, script, server, CLI, test, bundle, or package-management work: this session's eval js kernel runs Bun 1.4+ (this skill is present only when it does). Bun 1.4 ships builtins that replace 15+ npm deps — check here BEFORE installing sharp, puppeteer/playwright (scraping), marked, node-cron, node-pty, concurrently, serve-static, tar, json5, fast-xml-parser, string-width. Triggers: eval js, bun, Bun.serve, bun test, bun build, bun install, bun run, image resize, headless browser, markdown render, cron, PTY."
4
- ---
5
-
6
- # Bun 1.4 — Use the Builtins First
7
-
8
- Bun 1.4 (2026) ships builtins that replace most utility npm packages. **Before adding a dependency or writing a workaround, check the capability map below — if Bun ships it, use the builtin.** This skill is loaded because this session's `eval` js kernel runs Bun >= 1.4; on other machines or pinned projects, confirm with `bun --version` ([v1.3.x]/[v1.4.0] tags in the references give exact minimums). Release post: <https://bun.com/blog/bun-v1.4>
9
-
10
- ## Capability map — what you can do now
11
-
12
- | You want to... | Use | Replaces | Ref = `references/<name>.md` |
13
- |---|---|---|---|
14
- | Resize/convert/rotate images | `Bun.file(p).image().resize().webp().write()` | sharp | runtime-apis |
15
- | Headless browser: navigate, click, screenshot, evaluate, CDP | `new Bun.WebView()` | puppeteer, playwright (scraping) | runtime-apis |
16
- | Markdown → HTML / React / ANSI | `Bun.markdown.html() / .react() / .render()` | marked, react-markdown | runtime-apis |
17
- | Schedule cron jobs | `Bun.cron()` | node-cron | runtime-apis |
18
- | Drive a PTY (bash, vim, TUIs) from JS | `Bun.spawn([...], { terminal })` | node-pty | runtime-apis |
19
- | Run package scripts concurrently, glob-matched | `bun run --parallel "build:*"` | concurrently, npm-run-all | runtime-apis |
20
- | Parse JSON5 / JSONL / JSONC / XML / TOML; tarballs | `Bun.JSON5/.JSONL/.JSONC/.XML/.TOML`, `Bun.Archive` | json5, ndjson, jsonc-parser, fast-xml-parser, @iarna/toml, tar | runtime-apis |
21
- | ANSI-aware terminal text: width, slice, wrap | `Bun.stringWidth()`, `Bun.sliceAnsi()`, `Bun.wrapAnsi()` | string-width, slice-ansi, wrap-ansi | runtime-apis |
22
- | Call C libraries 3x faster, plain strings back | `bun:ffi` (`buffer_length`, `cstring`) | — | runtime-apis |
23
- | Serve a static dir (ETag/Range/304 handled) | `Bun.serve({ routes: { "/x/*": { dir } } })` | express.static, serve-static, sirv | http-networking |
24
- | HTTP/3 server; `fetch` protocol/compress/proxy | `http3: true`, `fetch(url, { protocol, compress, proxy })` | — | http-networking |
25
- | Tests across CPU workers / CI shards / changed files / flaky retry | `bun test --parallel --shard=1/3 --changed=main --retry` | jest/vitest infra | test |
26
- | Kill "passes alone, fails in suite" bugs | `bun test --isolate` | vitest default behavior | test |
27
- | React Compiler auto-memoization | `bun build --react-compiler` | babel-plugin-react-compiler | build |
28
- | Embed assets/dirs into a single-file executable | `bun build --compile --asset ./public` | pkg hacks | build |
29
- | Compile-time feature flags, in-memory bundling, metafile | `bun:bundle feature()`, `files:{}`, `metafile: true` | esbuild define/plugins | build |
30
- | Diff package versions, fix vulns, dedupe/prune, license audit | `bun pm diff`, `bun audit fix`, `bun dedupe`, `bun prune`, `bun pm licenses` | npm-diff, npm audit fix | install |
31
- | 7x faster CI installs (global virtual store) | `linker = "isolated"` in bunfig.toml | pnpm store | install |
32
- | Profile CPU/heap/bundle as grep-able Markdown | `bun --cpu-prof-md`, `--heap-prof-md`, `bun build --metafile-md` | Chrome DevTools round-trip | dev-tooling |
33
- | Native REPL; render Markdown in terminal | `bun repl`, `bun ./README.md` | node repl, glow | runtime-apis |
34
- | Run Playwright, vitest, Next.js 16, OpenTelemetry, dd-trace | they now just work | Node.js | node-compat-platforms |
35
-
36
- Free wins (no code change): 2x lower CPU, 13-48% less server memory, 2-2.5x faster startup — full numbers in [performance](references/performance.md).
37
-
38
- ## Operating rules
39
-
40
- 1. **Eval-first.** The `eval` js kernel runs on Bun (that is why this skill is present): call `Bun.*` APIs directly inside js cells. Reach for `bash` or a spawned `bun <file>` / `bun -e` only for CLI surfaces (`bun test`, `bun build`, `bun pm`) or work that needs a separate process.
41
- 2. **Builtin-first.** The Replaces column above is a ban list for new dependencies, in production code, test helpers, and one-off scripts alike.
42
- 3. **Read the matching reference before using an unfamiliar API** — signatures and caveats (e.g. `Bun.markdown` HTML is unsanitized, HTTP/3 is experimental) live there, not here.
43
- 4. **Upgrading a project to 1.4 or debugging behavior that changed?** Read [breaking-changes](references/breaking-changes.md) first — YAML/TOML strictness, `.xml`/`.css` import semantics, `Bun.$` globbing, fetch header combining, and WebSocket close timing all changed.
44
- 5. **TLS errors after upgrade are usually intentional** — 1.4 tightened certificate verification across `fetch`, `tls.connect`, `Bun.connect`, `RedisClient`. See [security](references/security.md) before loosening anything.
45
- 6. **A long job outlives a tool call only as `bash({ command, run_in_background: true })`** — plain `&` dies with the call's PTY hangup (native backend), `nohup` at best leaves an untracked orphan a timeout still group-kills, and a non-persistent `monitor` kills its watched command at `timeout_ms`; start the job in a background session and only watch it.
@@ -1,34 +0,0 @@
1
- # Bun 1.4 Dev Tooling & Observability
2
-
3
- Blog: [#dev-tooling](https://bun.com/blog/bun-v1.4#dev-tooling), [#observability](https://bun.com/blog/bun-v1.4#observability)
4
-
5
- ## Markdown profilers — built for terminals and LLMs
6
-
7
- ```bash
8
- bun --cpu-prof-md ./app.ts # CPU profile as Markdown: hot functions by self time,
9
- # call tree, who-calls-whom. grep it, paste into a bug or an LLM.
10
- bun --heap-prof-md ./app.ts # heap profile as Markdown: total size, types by retained size,
11
- # largest objects, retention chains. Includes grep recipes in the header.
12
- bun build ./src/index.ts --outdir ./dist --metafile-md=./dist/meta.md # bundle-size analysis as Markdown
13
- ```
14
-
15
- - `bun --cpu-prof` / `--heap-prof` still write `.cpuprofile` / V8-compatible `.heapsnapshot` for Chrome DevTools / VS Code.
16
- - `BUN_CPU_PROFILE=1` turns on the CPU profiler for a process you cannot pass flags to (e.g. a framework-spawned worker).
17
- - **Agent workflow: when a Bun process is slow or leaking, run the `-md` variants and read the report directly — no DevTools round-trip.**
18
-
19
- ## Async stack traces [v1.4.0]
20
-
21
- Errors from async native APIs (`fs.promises`, `Bun.file()`, S3, DNS, crypto, `fetch`) point at the `await` in your code, not native frames.
22
-
23
- ## Process lifetime & env flags
24
-
25
- ```bash
26
- bun --no-orphans app.ts # exit when parent dies; SIGKILL every descendant on exit (Linux/macOS/Windows)
27
- bun --no-env-file app.ts # skip automatic .env loading (or env = false in bunfig.toml) — use in prod/CI
28
- ```
29
-
30
- ## APM / tracing works now
31
-
32
- - **`node:inspector`**: a `Session` can start/stop CPU profiles while the app runs (`Profiler.start`/`Profiler.stop`).
33
- - **Datadog**: `dd-trace` traces requests; `@datadog/pprof` profiles continuously (required V8 C++ APIs implemented).
34
- - **OpenTelemetry**: `@opentelemetry/instrumentation-http` and `-fs` export spans; `shimmer` and `require-in-the-middle` can patch bundled code.
@@ -1,113 +0,0 @@
1
- # Bun 1.4 HTTP & Networking
2
-
3
- ## Serve files & folders (replaces express.static, serve-static, sirv) [v1.4.0]
4
-
5
- Docs: <https://bun.com/docs/runtime/http/routing> · Blog: [#serve-files-folders](https://bun.com/blog/bun-v1.4#serve-files-folders)
6
-
7
- ```ts
8
- Bun.serve({
9
- routes: {
10
- "/static/*": { dir: "./public" },
11
- },
12
- });
13
- ```
14
-
15
- - Files stream with `sendfile`; `Content-Type`, `ETag`, `Last-Modified`, `304`, and `Range` handled automatically; `index.html` served for directories.
16
- - Path traversal is blocked: paths normalized, and on Linux files open with `openat2` + `O_RESOLVE_BENEATH` so symlinks can't escape the directory.
17
-
18
- ## Range and conditional requests [v1.3.13, improved v1.4.0]
19
-
20
- Blog: [#range-and-conditional-requests](https://bun.com/blog/bun-v1.4#range-and-conditional-requests)
21
-
22
- ```ts
23
- Bun.serve({
24
- routes: { "/video.mp4": new Response(Bun.file("./video.mp4")) },
25
- });
26
- // curl -H 'Range: bytes=0-1023' → 206 Partial Content
27
- // curl -H 'If-None-Match: "..."' → 304 Not Modified
28
- ```
29
-
30
- Static routes and `Bun.file()` bodies handle `Range` (206), `If-None-Match`/`If-Modified-Since` (304), `If-Match`/`If-Unmodified-Since` (412). Video seeking and resumable downloads work out of the box.
31
-
32
- ## HTTP/3 in Bun.serve() — EXPERIMENTAL [v1.3.14, improved v1.4.0]
33
-
34
- Docs: <https://bun.com/docs/runtime/http/server> · Blog: [#http-3-in-bun-serve-experimental](https://bun.com/blog/bun-v1.4#http-3-in-bun-serve-experimental)
35
-
36
- ```ts
37
- Bun.serve({
38
- port: 443,
39
- tls: { /* cert, key */ },
40
- http3: true, // also listen on UDP/443
41
- // h1: false, // optional: HTTP/3 only
42
- fetch(req) { return new Response("hi"); },
43
- });
44
- ```
45
-
46
- - HTTP/1.1 keeps working over TCP; `Alt-Svc` header advertises H3 so browsers upgrade themselves. 2.7x faster than HTTPS/1.1 on static routes.
47
- - **Do not ship `http3: true` to production yet**: 0-RTT resumption disabled, `server.upgrade()` returns `false` over H3, `unix:` sockets skip the H3 listener.
48
-
49
- ## HTTP/2 & HTTP/3 in fetch() — EXPERIMENTAL [v1.3.14, improved v1.4.0]
50
-
51
- Blog: [#http-2-http-3-in-fetch-experimental](https://bun.com/blog/bun-v1.4#http-2-http-3-in-fetch-experimental)
52
-
53
- ```ts
54
- const [a, b] = await Promise.all([
55
- fetch(url1, { protocol: "http2" }), // concurrent same-origin requests share one connection
56
- fetch(url2, { protocol: "http2" }),
57
- ]);
58
- const res = await fetch(url3, { protocol: "http3" });
59
- ```
60
-
61
- - Redirects, decompression, streaming behave as over HTTP/1.1.
62
- - Global opt-in: `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT=1` or `--experimental-http3-fetch` (Bun remembers per-origin H3 support).
63
-
64
- ## fetch() request compression [v1.4.0]
65
-
66
- Blog: [#fetch-request-compression](https://bun.com/blog/bun-v1.4#fetch-request-compression)
67
-
68
- ```ts
69
- await fetch(url, {
70
- method: "POST",
71
- body: largeJsonString,
72
- compress: "gzip", // or true, "deflate", "br", "zstd", { encoding, level }
73
- });
74
- ```
75
-
76
- Buffered bodies (string, ArrayBuffer, TypedArray, Blob) are compressed and `Content-Encoding`/`Content-Length` set automatically; streaming bodies pass through unchanged.
77
-
78
- ## fetch() proxy headers [v1.3.4]
79
-
80
- Blog: [#fetch-proxy-headers](https://bun.com/blog/bun-v1.4#fetch-proxy-headers)
81
-
82
- ```ts
83
- await fetch(url, {
84
- proxy: {
85
- url: "http://proxy.example.com:8080",
86
- headers: { "Proxy-Authorization": "Bearer token" },
87
- },
88
- });
89
- ```
90
-
91
- ## TLS session resumption + connection reuse [v1.3.10-v1.4.0]
92
-
93
- Blog: [#tls-session-resumption](https://bun.com/blog/bun-v1.4#tls-session-resumption), [#connection-reuse](https://bun.com/blog/bun-v1.4#connection-reuse)
94
-
95
- - Second cold connection to an origin resumes at 1 RTT (32-entry per-origin LRU of BoringSSL client sessions).
96
- - `fetch()` reuses connections through HTTPS proxies and for requests with custom TLS options (client cert, custom CA). No code change needed.
97
-
98
- ## HTML routes: sourcemaps off in production [v1.4.0]
99
-
100
- Blog: [#html-routes-sourcemaps-disabled-in-production](https://bun.com/blog/bun-v1.4#html-routes-sourcemaps-disabled-in-production)
101
-
102
- Production `Bun.serve` no longer serves sourcemaps for HTML routes (dev mode still does). Override in `bunfig.toml`:
103
-
104
- ```toml
105
- [serve.static]
106
- sourcemap = "linked"
107
- ```
108
-
109
- ## Server lifecycle changes worth knowing (v1.4)
110
-
111
- - `server.stop()` now closes idle keep-alive connections immediately, waits for in-flight responses, and resolves when the last connection closes. `server.stop(true)` force-closes stalled ones.
112
- - `server.publish()` / `ws.publish()` return `0` (dropped/no subscribers) or `-1` (backpressure) instead of always the byte count.
113
- - Per-method route objects (`{ GET }`) answer `HEAD` with the GET handler automatically.
@@ -1,93 +0,0 @@
1
- # bun install & package management in 1.4
2
-
3
- ## Global virtual store: up to 7x faster installs [v1.3.14]
4
-
5
- Blog: [#global-virtual-store-up-to-7x-faster-installs](https://bun.com/blog/bun-v1.4#global-virtual-store-up-to-7x-faster-installs)
6
-
7
- ```toml
8
- # bunfig.toml
9
- [install]
10
- linker = "isolated"
11
- ```
12
-
13
- With the isolated linker, packages extract once into Bun's global cache and are symlinked into `node_modules/.bun/` (one `symlink()` per package instead of one `clonefileat()`). Warm-cache CI installs of 1,400 packages: 7x faster. Opt-in for existing projects; **new monorepos default to isolated** (see below).
14
-
15
- ## New subcommands [v1.4.0]
16
-
17
- Blog anchors: [#bun-pm-diff](https://bun.com/blog/bun-v1.4#bun-pm-diff), [#bun-audit-fix](https://bun.com/blog/bun-v1.4#bun-audit-fix), [#bun-dedupe](https://bun.com/blog/bun-v1.4#bun-dedupe), [#bun-prune](https://bun.com/blog/bun-v1.4#bun-prune), [#bun-pm-licenses](https://bun.com/blog/bun-v1.4#bun-pm-licenses)
18
-
19
- ```bash
20
- bun pm diff react # lockfile version → latest; un-minifies before diffing;
21
- bun pm diff react@18.2.0 19.0.0 # summary flags new install scripts + new child_process/fs/net/vm imports
22
- bun pm diff ./vendored-pkg pkg@2.1.0
23
-
24
- bun audit fix # upgrade vulnerable packages to safe versions; --latest allows majors; --dry-run
25
- bun dedupe # collapse duplicate versions in bun.lock; --check fails CI on dupes
26
- bun prune --production # delete node_modules entries not in bun.lock; --production drops devDeps
27
- bun pm licenses --prod --json # dependency license inventory
28
- ```
29
-
30
- Docker pattern: build with devDeps, ship without:
31
-
32
- ```dockerfile
33
- RUN bun install --frozen-lockfile
34
- COPY . .
35
- RUN bun run build
36
- RUN bun prune --production
37
- ```
38
-
39
- ## bun update: transitive + patterns [v1.4.0]
40
-
41
- Blog: [#bun-update-updates-transitive-dependencies](https://bun.com/blog/bun-v1.4#bun-update-updates-transitive-dependencies)
42
-
43
- ```bash
44
- bun update # now updates deps-of-deps too
45
- bun update zod # updates zod everywhere it appears
46
- bun update '@types/*' --latest
47
- ```
48
-
49
- `bun update <name>` errors (exit 1) if nothing depends on it (no longer silently adds).
50
-
51
- ## Monorepo: --filter and --catalog [v1.4.0]
52
-
53
- Blog: [#bun-add-filter](https://bun.com/blog/bun-v1.4#bun-add-filter), [#bun-add-catalog](https://bun.com/blog/bun-v1.4#bun-add-catalog)
54
-
55
- ```bash
56
- bun add zod --filter api # add to one workspace from the repo root
57
- bun run --filter 'web...' build # web + its dependencies; '...web' = its dependents
58
- bun add react --catalog # add to root catalog, write "catalog:" in the workspace
59
- ```
60
-
61
- Plain `bun add x` in a workspace whose default catalog lists `x` writes `catalog:` automatically.
62
-
63
- ## Nested overrides [v1.4.0]
64
-
65
- Blog: [#nested-overrides](https://bun.com/blog/bun-v1.4#nested-overrides)
66
-
67
- ```json
68
- {
69
- "overrides": {
70
- "express": { "qs": "6.13.0" },
71
- "lodash@<4.17.21": "4.17.21"
72
- }
73
- }
74
- ```
75
-
76
- npm nested form, yarn `a/b`, pnpm `a>b` all work; overrides can be version-scoped. Lockfiles using these become `lockfileVersion: 3` (older Bun can't read them).
77
-
78
- ## Supply-chain hardening
79
-
80
- - **Lockfile integrity** [v1.3.10]: `bun.lock` records SHA-512 for GitHub and tarball deps, like npm packages.
81
- - **`trustedDependencies` auto-trust is npm-registry-only** [v1.3.5]: a `git:`/`github:`/`file:` package named `esbuild` gets NO trust from the default list — list it yourself. Names match exactly (not by hash).
82
- - **`nativeDependencies`** [v1.3.2]: for packages shipping prebuilt binaries as per-platform optionalDependencies, Bun links the right binary directly instead of running postinstall. **`ignoreScripts`** skips a package's lifecycle scripts even if trusted.
83
-
84
- ```json
85
- { "nativeDependencies": ["esbuild"], "ignoreScripts": ["sharp"] }
86
- ```
87
-
88
- ## Defaults changed in 1.4 (see also breaking-changes.md)
89
-
90
- - New monorepos default to `linker: "isolated"` (`configVersion: 1` in bun.lock). Existing lockfiles keep hoisted. Pin `linker = "hoisted"` to opt out.
91
- - `bun.lock` is `lockfileVersion: 2`: out-of-registry tarballs need integrity hashes; git entries validated against path traversal. Run `bun install` to migrate.
92
- - A project's `bunfig.toml` now overrides `.npmrc` for the same key.
93
- - `bun init` writes `typescript ^7`; non-TTY `bun init` behaves as `-y`.