Imported from JimmyPaolini/codebase (
packages/ic-suite/conformetry/conformetry-cli/AGENTS.md). Install upstream withnpx skills add JimmyPaolini/codebase --skill conformetry-cli. Copyright stays with the author.
ConformetryCli: NestJS Command-Line Application
Quick Start
Type: Node.js CLI application (NestJS + nest-commander)
Purpose:
Run Locally
cp .env.default .env # Fill in required environment variables
nx run conformetry-cli:start
Architecture Overview
Tech Stack
- Framework: NestJS (modules, dependency injection, providers)
- CLI runner:
nest-commander(CommandRunner+@Command()decorator) - Env validation:
@nestjs/config+zod(environmentSchemain.constants.ts) - Logging:
@codebase/logger— apino-backedLoggerService(Scope.TRANSIENT) - Language: Strict TypeScript
Execution Flow
src/main.ts
└─ CommandFactory.run(MainModule)
└─ domain command modules ← add under src/modules/
Directory Layout
src/
main.ts # Bootstrap — do not modify
main.module.ts # Root NestJS module (imports ConfigModule, LoggerModule)
constants.ts # Zod environmentSchema for env validation
modules/
<domain>/ # Add feature modules here
<domain>.module.ts
<domain>.command.ts
<domain>.service.ts
<domain>.types.ts
<domain>.constants.ts
<domain>.<tier>.test.ts
testing/ # Shared test utilities
Development
Adding Business Logic
- Add domain command modules — create
src/modules/<domain>/with a NestJS module, command, service, types, and constants. - Register in root module — import the new module in
main.module.ts. - Validate env vars — extend
environmentSchemainconstants.tswith all required environment variables.
Logging
LoggerService and LoggerModule come from @codebase/logger — this project does not define its own logger. Add "@codebase/logger": "workspace:*" to dependencies, then import LoggerModule once in the root module; it is @Global(), so feature modules inject LoggerService without importing it.
LoggerService is Scope.TRANSIENT — each injecting class gets its own instance. Always call setContext in the constructor:
constructor(private readonly logger: LoggerService) {
super();
this.logger.setContext(MyService.name);
}
Outputs structured JSON in production (NODE_ENV=production) and pretty-printed logs in development.
Key Commands
Always prefer running tasks through Nx rather than calling the underlying tools directly.
nx run conformetry-cli:start # Run the command-line application
nx run conformetry-cli:typecheck-code,lint-code,format-code,deprecate-code,guard-code # Every static check, in one graph
nx run conformetry-cli:typecheck # tsc --noEmit
nx run conformetry-cli:oxfmt # Formatting
Testing
Follow the codebase's strict three-tier testing strategy. Co-locate test files with the source they test.
nx run conformetry-cli:vitest:unit # Fast (<100ms) — pure logic, mocked DI
nx run conformetry-cli:vitest:integration # Moderate (1-2s) — real database/API I/O
nx run conformetry-cli:vitest:end-to-end # Slow (30-60s) — full CLI execution
| Tier | File pattern | What to test |
|---|---|---|
| Unit | *.unit.test.ts |
Pure functions, service methods with mocked deps |
| Integration | *.integration.test.ts |
Database queries, external API clients |
| End-to-end | *.end-to-end.test.ts |
Full CommandFactory.run() execution |
See the testing-strategy skill and testing-mocks skill for patterns and mock conventions.
Writing Modules
Use the generator to scaffold new domain modules, then implement the service:
nx g conformetry:nestjs-service-module --name=<domain>
This creates five files in src/modules/<domain>/:
| File | Purpose |
|---|---|
<domain>.module.ts |
Declares providers, imports, and exports |
<domain>.service.ts |
Business logic — the only place you write domain code |
<domain>.constants.ts |
Regex, enums, static config — never inline magic values |
<domain>.types.ts |
TypeScript types scoped to this module |
<domain>.service.unit.test.ts |
Unit tests bootstrapped with Test.createTestingModule |
Module file
Register the service in both providers and exports so consumers can inject it:
@Module({
controllers: [],
exports: [MyDomainService],
imports: [TypeOrmModule.forFeature([MyEntity]), LoggerModule],
providers: [MyDomainService],
})
export class MyDomainModule {}
Add a JSDoc comment on the module class describing what domain it owns.
Service file
Follow the section-comment layout from the template — it keeps large services scannable:
@Injectable()
export class MyDomainService {
// 🏗 Dependency Injection
constructor(
@InjectRepository(MyEntity)
private readonly repo: Repository<MyEntity>,
private readonly logger: LoggerService,
) {
this.logger.setContext(MyDomainService.name);
}
// 🔐 Private Fields
// 🔑 Public Fields
// 🔏 Private Methods
// 🌎 Public Methods
}
Key rules:
- Call
setContextin every constructor — always useMyClass.name, never a string literal. - Inject
LoggerServiceas the last constructor parameter (after repository/domain deps). - Private first — keep internal helpers in the
🔏 Private Methodssection, expose only what callers need under🌎 Public Methods. readonlyeverything in the constructor — all injected deps must beprivate readonly.- One service per module — if a service grows too large, extract a sub-domain into its own module.
Constants file
Move all inline values to .constants.ts to keep services readable:
// ♟️ Constants
export const MY_SKIP_REGEX = /(alternative)|(archaic)|(synonym)/i;
export const DEFAULT_PAGE_SIZE = 100;
Types file
Put all module-local TypeScript types and interfaces in .types.ts:
// 🏷️ Types
export interface ParsedEntry {
word: string;
partOfSpeech: string;
}
Do not re-export types from index.ts unless they are part of the public API consumed by other modules.
Registering in the root module
After generating a module, import it in main.module.ts:
@Module({
imports: [
ConfigModule.forRoot({ ... }),
LoggerModule,
MyDomainModule, // ← add here
],
providers: [],
})
export class MainModule {}
Conformetry validation
Conformetry validation measures generated and existing module structures against the templates they came from. It runs for one project, or for every project at once.
pnpm nx run-many --targets=conformetry-validate
Best Practices
- Never put business logic in
main.ts— it bootstrapsCommandFactoryonly. - One command per class — split sub-commands into separate
CommandRunnersubclasses. - Validate at the boundary — all env vars must be declared in
environmentSchema; access viaConfigService, notprocess.env. - Type imports — use
import { type Foo }for type-only imports (enforced by ESLint). - No
anytypes — useunknownor proper typing; strict mode is enabled.
See the write-typescript skill for strict mode patterns.
Troubleshooting
- Command not found at runtime — ensure the command class is listed in
providersof its module and the module is imported by the root module. - Dependency injection failure — verify the service is
@Injectable(), exported from its module, and that module is imported by the consuming module. - Unrecognized CLI flag — check that
@Option()decorators in the command class exactly match the flag names passed. - Env var validation error on startup — add the missing variable to
environmentSchemainsrc/constants.tsand to.env.default.
See the triage-integration skill for lint and git hook failures.
Key Files
- src/main.ts: Application bootstrap
- src/main.module.ts: Root NestJS module
- src/constants.ts:
environmentSchema(Zod) @codebase/logger(packages/logger): shared pino-backedLoggerServiceandLoggerModule- project.json: Nx targets (
develop,build,test,lint,typecheck,format) - .env.default: Environment variable template