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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ on:
pull_request:

jobs:
core:
name: core (node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['22.13.x', '24.x', '26.x']
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}

- name: Install from lockfile
run: npm ci

- name: Check workspace versions
run: npm run check:workspace-versions

- name: Typecheck core
run: npm run typecheck --workspace=@codeburn/core

- name: Test core
run: npm test --workspace=@codeburn/core

- name: Build core
run: npm run build --workspace=@codeburn/core

- name: Verify every export target exists
run: npm run verify-dist --workspace=@codeburn/core

- name: Verify package contents
run: npm pack --workspace=@codeburn/core --dry-run

semgrep:
runs-on: ubuntu-latest
steps:
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeburn-monorepo",
"version": "0.9.19",
"version": "0.9.20",
"description": "CodeBurn monorepo — CLI (packages/cli) + @codeburn/core (packages/core)",
"private": true,
"type": "module",
Expand All @@ -13,7 +13,8 @@
"build:cli": "npm run build:cli -w codeburn",
"build:dash": "npm run build:dash -w codeburn",
"dev": "npm run dev -w codeburn",
"test": "npm run test -w codeburn"
"test": "npm run test -w codeburn",
"check:workspace-versions": "node scripts/check-workspace-versions.mjs"
},
"engines": {
"node": ">=22.13.0"
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeburn",
"version": "0.9.19",
"version": "0.9.20",
"description": "See where your AI spend goes - by task, tool, model, and project",
"type": "module",
"main": "./dist/cli.js",
Expand Down Expand Up @@ -50,7 +50,7 @@
},
"homepage": "https://github.com/getagentseal/codeburn#readme",
"dependencies": {
"@codeburn/core": "*",
"@codeburn/core": "0.9.20",
"@modelcontextprotocol/sdk": "^1.29.0",
"bonjour-service": "^1.4.1",
"chalk": "^5.4.1",
Expand Down
7 changes: 5 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.9.20",
"description": "Pure decode/detect engine for CodeBurn: provider session-log decoding, content-minimized observations, and detector contracts. No I/O, no pricing \u2014 hosts supply both.",
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -177,10 +178,12 @@
"schemas"
],
"scripts": {
"build": "tsup",
"build": "tsup && tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"emit-schemas": "tsx scripts/emit-schemas.mts"
"emit-schemas": "tsx scripts/emit-schemas.mts",
"verify-dist": "node scripts/verify-dist.mjs",
"prepublishOnly": "npm run build && npm run verify-dist"
},
"engines": {
"node": ">=22.13.0"
Expand Down
54 changes: 54 additions & 0 deletions packages/core/scripts/verify-dist.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env node
// Refuses to let a half-built dist reach npm.
//
// `npm run build` is `tsup && tsc`. tsup runs with clean:true, so it wipes dist
// and writes JavaScript; if tsc then fails, dist holds .js with no declarations.
// The build exits non-zero, but nothing rebuilds at publish time, so a later
// `npm publish` would ship a package whose every `types` target 404s. Verified:
// removing the declarations and running `npm pack --dry-run` succeeds with
// 41/41 exports subpaths pointing at files that are not in the tarball.
//
// This script is the assertion that closes that gap. It runs from
// `prepublishOnly` (after the build) and in CI, so it is exercised on every
// push rather than only on the rare publish.
import { existsSync, readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
const pkg = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'))

const problems = []
let checked = 0

for (const [subpath, entry] of Object.entries(pkg.exports ?? {})) {
for (const condition of ['import', 'types']) {
const relative = entry?.[condition]
if (!relative) {
problems.push(`exports["${subpath}"] declares no "${condition}" target`)
continue
}
checked++
if (!existsSync(join(pkgRoot, relative))) {
problems.push(`exports["${subpath}"].${condition} -> ${relative} does not exist`)
}
}
}

// `files` decides what actually ships; a target outside it is unreachable to a
// consumer even when it exists on disk here.
const shipped = pkg.files ?? []
if (!shipped.includes('dist')) {
problems.push('package.json#files does not include "dist", so no export target would ship')
}

if (problems.length > 0) {
console.error(`dist verification failed (${problems.length} problem(s)):`)
for (const problem of problems) console.error(` - ${problem}`)
console.error('\nRun `npm run build --workspace=@codeburn/core` and re-check.')
process.exit(1)
}

console.log(
`dist verified: ${checked} export targets across ${Object.keys(pkg.exports ?? {}).length} subpaths all present`,
)
17 changes: 17 additions & 0 deletions packages/core/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
// No declarationMap: `files` ships dist and schemas but not src, so the
// emitted .d.ts.map would point at sources the consumer never receives.
// That is 151 files and ~135 kB of maps that resolve to nothing.
"declarationMap": false,
"emitDeclarationOnly": true,
"rootDir": "src",
"outDir": "dist",
"sourceMap": false,
"noEmit": false
},
"include": ["src/**/*"],
"exclude": ["tests/**/*", "scripts/**/*", "dist", "node_modules"]
}
5 changes: 4 additions & 1 deletion packages/core/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ export default defineConfig({
clean: true,
splitting: false,
sourcemap: true,
dts: true,
// Declarations come from `tsc -p tsconfig.build.json` instead. tsup's dts
// worker bundles types for all 41 entries in one pass and exhausts the heap
// (ERR_WORKER_OUT_OF_MEMORY) on Node 22 through 26.
dts: false,
})
89 changes: 89 additions & 0 deletions scripts/check-workspace-versions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env node
// Fails when the monorepo root, the CLI, and @codeburn/core drift apart.
// The CLI must depend on the exact core version it ships with, otherwise a
// published CLI can resolve a core it was never tested against.
//
// package-lock.json is checked too. Comparing only the manifests leaves the
// gate blind to the case it exists for: all three manifests can agree while the
// lockfile still records a stale workspace version, and `npm ci` does not
// reject that (verified on npm 10.9.2, 11.12.1, 11.16.0, 11.17.0). A gate that
// cannot catch its own failure mode is decoration.
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')

const read = (relative) => {
const path = join(repoRoot, relative)
try {
return JSON.parse(readFileSync(path, 'utf8'))
} catch (error) {
throw new Error(`cannot read ${relative}: ${error.message}`)
}
}

const root = read('package.json')
const cli = read('packages/cli/package.json')
const core = read('packages/core/package.json')
const lock = read('package-lock.json')

const coreDep = cli.dependencies?.['@codeburn/core']

const problems = []

if (root.version !== cli.version) {
problems.push(`root version ${root.version} !== cli version ${cli.version}`)
}
if (root.version !== core.version) {
problems.push(`root version ${root.version} !== core version ${core.version}`)
}
if (coreDep !== core.version) {
problems.push(
`packages/cli depends on "@codeburn/core": ${JSON.stringify(coreDep)}, expected the exact core version "${core.version}"`,
)
}

// --- lockfile agreement -----------------------------------------------------

const lockEntry = (key) => lock.packages?.[key]

if (!lock.packages) {
problems.push('package-lock.json has no "packages" map; expected lockfileVersion 2 or 3')
} else {
const expected = [
['', root.version, 'root'],
['packages/cli', cli.version, 'packages/cli'],
['packages/core', core.version, 'packages/core'],
]
for (const [key, manifestVersion, label] of expected) {
const entry = lockEntry(key)
if (!entry) {
problems.push(`package-lock.json is missing an entry for ${label}`)
continue
}
if (entry.version !== manifestVersion) {
problems.push(
`package-lock.json records ${label} at ${entry.version}, but its manifest says ${manifestVersion}`,
)
}
}

const lockedDep = lockEntry('packages/cli')?.dependencies?.['@codeburn/core']
if (lockedDep !== undefined && lockedDep !== coreDep) {
problems.push(
`package-lock.json records the CLI's core dependency as ${JSON.stringify(lockedDep)}, but packages/cli/package.json says ${JSON.stringify(coreDep)}`,
)
}
}

if (problems.length > 0) {
console.error('workspace version check failed:')
for (const problem of problems) console.error(` - ${problem}`)
console.error('\nSet root, packages/cli, and packages/core to the same version,')
console.error('pin the CLI dependency to that exact version, then run:')
console.error(' npm install --package-lock-only --ignore-scripts')
process.exit(1)
}

console.log(`workspace versions and package-lock.json agree at ${root.version}`)
Loading