Scaffolding a Coding Agent Monorepo with npm Workspaces and Strict TypeScript
This article is the second in the "Developing a Coding Agent from Scratch" series, mainly introducing how to use npm to set up a monorepo project and manage multiple sub-projects. If you are also interested in Agent development, feel free to like, bookmark, and follow. Column: Developing a Coding Agent from Scratch - 东方小月's Column - Juejin
What is a monorepo project
A monorepo project manages multiple sub-projects within a single code repository. Each sub-project is an independent npm package, and they coordinate through npm's dependency management mechanism. In npm workspaces, local packages still declare dependencies via package.json; the difference is that during installation, npm links the dependency to the local workspace instead of downloading it from the npm registry.
For example: the agent package needs to reference the ai package.
The root directory has declared:
{
"workspaces": ["packages/*"]
}
packages/ai/package.json:
{
"name": "@di-code/ai",
"version": "0.0.0"
}
Declare in packages/agent/package.json:
{
"name": "@di-code/agent",
"version": "0.0.0",
"dependencies": {
"@di-code/ai": "0.0.0"
}
}
Then install from the root: npm install
npm will create a local link similar to the following:
node_modules/@di-code/ai
-> D:\pi\di-code\packages\ai
On Windows, this might actually be a junction (directory junction), but the effect is the same as a symbolic link, without copying the source code. Thus, in agent, you can import it like a regular npm package:
import type { Message } from "@di-code/ai";
Do not use cross-directory relative imports:
import type { Message } from "../../ai/src/types.js";
Environment and Directory
Next, we enter the actual development of the project. First, you need to check if your environment meets the requirements. The node version should be at least 22.19.0.
Create a new project directory. I chose the name di-code here, and initialize the root package.json:
{
"name": "di-code",
"version": "0.0.0",
"private": true,
"type": "module",
"workspaces": ["packages/*"],
"scripts": {
"build": "npm run build --workspace @di-code/tui && npm run build --workspace @di-code/ai && npm run build --workspace @di-code/agent && npm run build --workspace @di-code/coding-agent",
"check": "biome check . && tsc --noEmit -p tsconfig.json",
"test": "npm run test --workspaces --if-present"
},
"devDependencies": {
"@biomejs/biome": "2.3.5",
"@types/node": "22.19.19",
"typescript": "5.9.3",
"vitest": "4.1.9"
},
"engines": {
"node": ">=22.19.0"
}
}
Key field descriptions:
private: trueprevents accidental publishing of the root aggregate package.type: moduleestablishes ESM semantics for root scripts.workspacesonly matches the four direct sub-packages, preventing future examples or temporary directories from accidentally becoming workspaces.- The explicit order in
buildcorresponds to the future dependency direction. checkruns Biome first; TypeScript only runs if Biome succeeds.testdistributes the test command to all workspaces that have defined atestscript.- All devDependencies use exact versions, without
^or~.
Then create the packages directory to store all sub-projects. Inside this directory, create four sub-directories: ai, agent, coding-agent, tui.
Create a src directory inside each sub-directory to store the source code.
TypeScript Configuration
Create a tsconfig.base.json file in the root directory:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"noEmitOnError": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"inlineSources": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["node"]
}
}
Partial option descriptions:
targetdetermines the syntax baseline of the output JavaScript; ES2022 is available on the minimum Node 22 runtime.moduleandmoduleResolutionboth useNodeNext, allowing TypeScript to determine package boundaries according to modern Node.js ESM/CJS rules. Do not change one toNodeNextand leave the other as the oldnode.strictis a project constraint and must not be turned off later to eliminate errors.verbatimModuleSyntaxrequires import/export syntax to be consistent with the final module semantics and encourages the use ofimport typefor type imports.noEmitOnErrorprevents leaving seemingly usable build artifacts when there are type errors.declarationanddeclarationMapgenerate.d.tsand mappings for workspace public types.sourceMapandinlineSourcespreserve source location for later debugging.skipLibCheckonly skips checks inside third-party declaration files, not the project's own source code checks.types: ["node"]explicitly introduces Node.js global types, preventing other@types/*packages in the environment from accidentally polluting the global namespace.
Then write the TS configuration file tsconfig.json in the root directory:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"noEmit": true
},
"include": ["packages/*/src/**/*.ts", "packages/*/test/**/*.ts"],
"exclude": ["**/node_modules/**", "**/dist/**"]
}
The responsibility of the root configuration is repository-wide checking, not building:
includecovers both source code and future tests.noEmitensuresnpm run checkdoes not modify the workspace.distis excluded to avoid checking generated files and reporting duplicate issues.
Writing the four workspace package.json files
Create package.json files in the four directories ai, agent, coding-agent, tui. Their scripts and export shapes remain consistent, with different names and descriptions.
packages/ai/package.json
{
"name": "@di-code/ai",
"version": "0.0.0",
"description": "Provider-neutral AI contracts and adapters for di-code",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run --passWithNoTests"
},
"engines": {
"node": ">=22.19.0"
}
}
packages/agent/package.json
{
"name": "@di-code/agent",
"version": "0.0.0",
"description": "Core agent state and loop for di-code",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run --passWithNoTests"
},
"engines": {
"node": ">=22.19.0"
}
}
packages/coding-agent/package.json
{
"name": "@di-code/coding-agent",
"version": "0.0.0",
"description": "Coding agent product layer for di-code",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run --passWithNoTests"
},
"engines": {
"node": ">=22.19.0"
}
}
packages/tui/package.json
{
"name": "@di-code/tui",
"version": "0.0.0",
"description": "Reusable terminal UI library for di-code",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run --passWithNoTests"
},
"engines": {
"node": ">=22.19.0"
}
}
mainpoints to the runtime ESM entry.typespoints to the TypeScript declaration entry.exportsrestricts the package's public entry to the root entry, preventing future callers from depending on internal file paths.
Writing workspace build configurations
Create tsconfig.build.json with the same content in the following four locations:
packages\ai\tsconfig.build.json
packages\agent\tsconfig.build.json
packages\coding-agent\tsconfig.build.json
packages\tui\tsconfig.build.json
The content for each is:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "test"]
}
extendsallows the four packages to inherit the exact same strict mode and ESM semantics.rootDirtells the compiler that the source root issrc.outDirkeeps the output within each respective package, preventing mixing different packages into a rootdist.- build only includes
src; tests do not enter the published artifact.
Entry Creation
Write one line in each of the following four files:
packages\ai\src\index.ts
packages\agent\src\index.ts
packages\coding-agent\src\index.ts
packages\tui\src\index.ts
Content:
export {};
Configuring the Biome Formatting Tool
Biome is a code quality tool for JavaScript, TypeScript, JSON, and other files, simply put, it's ESLint + Prettier.
Write in di-code\biome.json:
{
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
"files": {
"includes": [
"package.json",
"biome.json",
"tsconfig*.json",
"packages/*/package.json",
"packages/*/tsconfig*.json",
"packages/*/src/**/*.ts",
"packages/*/test/**/*.ts"
]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"lineWidth": 120
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}
Writing .gitignore
Create a .gitignore in the root directory and write:
# Dependencies
node_modules/
# Build and test output
dist/
coverage/
*.tsbuildinfo
# Logs
*.log
npm-debug.log*
# Environment and credentials
.env
.env.*
!.env.example
# Editors and operating systems
.vscode/
.idea/
.DS_Store
Thumbs.db
None of these need to be committed to the code repository.
Installing Dependencies and Building
At this point, we have basically completed the basic monorepo configuration. Next, we can start installing dependencies and building the project.
npm install
npm run build
After the build is complete, we can see a dist directory under each package, containing the compiled code.
Finally, run a check to ensure code quality.
npm run check
If Biome only reports format differences, you can execute
npx biome check --write package.json tsconfig.base.json tsconfig.json biome.json packages
to fix them, then run npm run check again to ensure the format meets the requirements.
At this point, we have completed the basic monorepo configuration. The git branch for this chapter is Monorepo Initialization.