Skip to content

feat: add AI-first CLI and AI.md agent manual#3

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/build-ai-first-cli
Draft

feat: add AI-first CLI and AI.md agent manual#3
Copilot wants to merge 3 commits intomainfrom
copilot/build-ai-first-cli

Conversation

Copy link

Copilot AI commented Mar 8, 2026

Implements Phase 1 (CLI code generator) and Phase 2 (AI.md agent manual) to make the package "agent-ready" — usable directly from AI coding assistants and automation pipelines.

CLI (src/cli.ts)

New init command scaffolds a type-safe mapper from two existing TypeScript interface files:

# Write ApiUserMapper.ts next to source file
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts

# Preview without writing (agent-friendly)
npx @kylebrodeur/type-safe-mapping init ./src/types/ApiUser.ts ./src/models/User.ts --stdout

Generated output:

import { MappedServiceBase, MappedType } from '@kylebrodeur/type-safe-mapping';
import type { ApiUser } from './ApiUser.js';

const fieldMapping = {
  // TODO: user_id: 'targetFieldName',  // unresolved — fill in
  first_name: 'firstName',              // auto-matched via camelCase
  last_name: 'lastName',
} as const;

export type User = MappedType<ApiUser, typeof fieldMapping>;

export class ApiUserMapper extends MappedServiceBase<ApiUser, typeof fieldMapping> {
  protected fieldMapping = fieldMapping;
}
  • Zero new runtime dependencies — regex-based TS interface parser using Node built-ins only
  • Auto-matches fields via snake_case → camelCase heuristic; unmatched fields emit // TODO: stubs
  • --stdout lets agents inspect before committing to disk; --out <path> for custom output location
  • Refuses to overwrite existing files by default

AI.md

Root-level agent manual indexed by Cursor, Copilot, Claude, etc. Covers when to use the package, step-by-step agent workflow (scaffold → fill TODOs → tsc --noEmit), full API reference, and common pitfalls.

Supporting changes

  • package.json: bin field (type-safe-mapping → dist/cli.js), AI.md in published files, @types/node in devDependencies
  • tsconfig.json: "types": ["node"] for CLI
  • README.md: new CLI section with flag table and generated output example
  • tests/cli.test.ts: 14 tests covering --stdout, camelCase matching, file write, overwrite guard, --out, and all error paths

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

The CLI's TypeScript parser is intentionally regex-based to preserve the zero-runtime-dependency constraint. It handles the common cases (simple types, unions, generics, optional fields, index signatures) but is not a full AST parser — complex nested types or unusual formatting may produce incomplete field lists. Users are expected to review and complete any // TODO: stubs before running tsc --noEmit.

Original prompt

Review these ideas and give thoughts and feedback along with a proposed plan. I will then review.

This plan focuses on making your package "Agent-Ready" and building a CLI that acts as a bridge between raw code and AI automation.
Phase 1: The "AI-First" CLI
Instead of just a library, you’re building a code generator.

  • The Command: npx @kylebrodeur/type-safe-mapping init
  • The Logic: Use ts-morph or a simple regex to scan two TypeScript interfaces and generate a boilerplate mapping file using your library’s syntax.
  • Agent Feature: Add a --stdout flag. This allows an AI agent to "peek" at the generated code in its terminal before deciding to write it to a file.

Phase 2: Create the AI.md (The "Manual" for Agents)
Create a file named AI.md (or SKILLS.md) in your root. Modern AI tools (Cursor, Windsurf, Claude Code) will index this.

AI Skill: Type-Safe MappingThis package provides a utility for mapping one TypeScript object to another with 100% type safety.

When to use- When converting an API Response (DTO) to a Domain Model.- When you need to ensure that adding a field to an interface breaks the build until the mapper is updated.

Usage for Agents1. Scaffold: Run npx @kylebrodeur/type-safe-mapping init ./src/types/User.ts ./src/dto/UserDTO.ts.

  1. Implementation: Use the createMapper function from the library.
  2. Validation: Ensure the agent runs tsc --noEmit after mapping to verify the types match.

Phase 3: The "Non-Spammy" Promotion Loop
Now that you have an AI-ready tool, your promotion becomes "How to use AI to map types":

  1. The "AI Workflow" Post: Write a post on DEV.to titled "Stop manually writing mappers: How to use [Your Tool] + Cursor/Claude to automate DTOs."
  2. The "Problem/Solution" Reddit Comment: Find threads on r/typescript asking about "AutoMapper alternatives."
  • Draft: "I found most mappers too heavy, so I built a type-safe one. It also has a CLI that plays really well with AI agents if you're using Cursor or Copilot."
  1. The "Daily.dev" Submission: Submit your GitHub repo. Use a thumbnail that shows a terminal window running your new CLI.

Phase 4: Integration with MCP (Advanced)
Build a Model Context Protocol (MCP) server.

  • This allows a user to "plug in" your library to Claude Desktop.
  • The agent will have a tool called generate_mapper that it can call natively without the user even typing a command.

Which of these would you like to start with? I can help write the CLI boilerplate code or the full AI.md content.

Create a new branch.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits March 8, 2026 23:58
Co-authored-by: kylebrodeur <811103+kylebrodeur@users.noreply.github.com>
Co-authored-by: kylebrodeur <811103+kylebrodeur@users.noreply.github.com>
Copilot AI changed the title [WIP] Add AI-First CLI for type-safe mapping package feat: add AI-first CLI and AI.md agent manual Mar 9, 2026
@kylebrodeur kylebrodeur requested a review from Copilot March 9, 2026 00:34
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an “AI-first” CLI code generator and an AI-focused manual to make @kylebrodeur/type-safe-mapping easier to adopt from automation/agent workflows.

Changes:

  • Introduces a new type-safe-mapping CLI (init) that scaffolds mapper boilerplate from TypeScript interfaces (with --stdout / --out).
  • Adds AI.md agent manual and expands README with CLI documentation and examples.
  • Adds Vitest coverage for CLI behavior and updates TS/packaging config for Node-based CLI usage.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/cli.ts Implements the CLI, regex-based interface parsing, field pairing heuristics, and mapper code generation.
tests/cli.test.ts Adds end-to-end CLI tests covering stdout mode, file output, overwrite guard, and error paths.
AI.md Adds an agent-oriented usage manual (workflow + CLI reference + pitfalls).
README.md Documents the new CLI, options, and sample generated output.
package.json Publishes AI.md, adds bin entry, and adds Node types for CLI compilation.
tsconfig.json Includes Node typings needed for the CLI build.
package-lock.json Locks dependency updates required by the new/updated dev toolchain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +132 to +133
export { MappedServiceBase } from './MappedServiceBase';
export type { MappedType, MappingDefinition, ReverseMapping } from './types';
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Exports” snippet shows extensionless internal paths ('./MappedServiceBase', './types'), but this package is ESM/NodeNext and the actual source exports use .js extensions. Consider either showing the real src/index.ts exports (with .js) or replacing this section with consumer-facing import examples from the package entrypoint to avoid misleading copy/paste.

Suggested change
export { MappedServiceBase } from './MappedServiceBase';
export type { MappedType, MappingDefinition, ReverseMapping } from './types';
import {
MappedServiceBase,
type MappedType,
type MappingDefinition,
type ReverseMapping,
} from '@kylebrodeur/type-safe-mapping';

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +113
| Flag | Description |
|------|-------------|
| `--stdout` | Print generated code to stdout instead of writing a file |
| `--out <path>` | Write the output to a specific file path |

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown tables in this section start rows with || (double pipe), which renders as an extra empty column in most markdown renderers. Use single leading pipes (| Flag | Description |, etc.) for valid tables.

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +113
| Problem | Fix |
|---|---|
| TypeScript errors about index signature | Add `[key: string]: unknown` to your source interface |
| Type inference fails | Make sure you used `as const` on the field mapping object |
| Unmapped fields appear in output | Only fields in `fieldMapping` are included — this is intentional |
| CLI says "no interfaces found" | Check that the file contains a `interface Foo { ... }` declaration |
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Common Pitfalls” table rows start with || (double pipe), which creates an extra empty column in standard markdown rendering. Use single leading pipes for proper table formatting.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +101
// Match `fieldName?: type;` or `fieldName: type;`
// This handles simple types, generics, unions, and intersections
const fieldRegex = /^\s*(readonly\s+)?(\w+)(\?)?:\s*([^;]+);?\s*$/gm;
let m: RegExpExecArray | null;

while ((m = fieldRegex.exec(cleaned)) !== null) {
const fieldName = m[2];
const optional = m[3] === '?';
const type = m[4].trim();

// Skip index signatures like [key: string]: unknown
if (/^\[/.test(fieldName)) continue;

fields.push({ name: fieldName, type, optional });
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseFields says it skips index signatures, but the regex only captures \w+ field names, so index signatures ([key: string]: ...) will never match in the first place; the if (/^\[/.test(fieldName)) branch is dead code. Either adjust the regex to also match bracketed keys and keep the skip, or remove the skip check/comment to avoid misleading future maintenance.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +43
// Match `interface Foo {` or `export interface Foo {`
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+[^{]+)?\s*\{/g;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interfaceRegex doesn't match interfaces with generic parameters (e.g. export interface Foo<T> { ... }) because it expects { immediately after the name/optional extends. This contradicts the comment that generics are handled and will cause the CLI to report “No TypeScript interfaces found” on common inputs. Update the regex to allow an optional <...> section (and/or adjust docs/tests accordingly).

Suggested change
// Match `interface Foo {` or `export interface Foo {`
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+[^{]+)?\s*\{/g;
// Match `interface Foo {`, `export interface Foo<T> {`, with optional generics and extends
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)\s*(?:<[^>]+>)?(?:\s+extends\s+[^{]+)?\s*\{/g;

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +33
function runCli(args: string): { stdout: string; stderr: string; code: number } {
try {
const stdout = execSync(`node ${CLI_PATH} ${args}`, {
cwd: TMP_DIR,
encoding: 'utf-8',
});
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runCli builds a shell command string (execSync("node ${CLI_PATH} ${args}")). This is brittle with paths containing spaces (and is harder to make cross-platform). Prefer execFileSync('node', [CLI_PATH, ...parsedArgs], { ... }) (or similar) to avoid quoting issues and injection via the args string.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to +66
"engines": {
"node": ">=18.0.0"
},
"bin": {
"type-safe-mapping": "./dist/cli.js"
},
"devDependencies": {
"@types/node": "^25.3.5",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^10.0.3",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

engines.node allows Node >=18, but the updated devDependencies (notably vitest@4 / eslint@10) declare Node >=20 in the lockfile. Contributors running Node 18 will likely be unable to install/run tests/lint. Consider aligning engines.node with the dev toolchain requirement (or downgrading devDependencies to versions that support Node 18).

Copilot uses AI. Check for mistakes.
// Parse flags
const stdoutFlag = args.includes('--stdout');
const outIndex = args.indexOf('--out');
const outFile = outIndex !== -1 ? args[outIndex + 1] : null;
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--out is parsed as args[outIndex + 1] but there’s no validation that a value is actually present. If a user passes --out at the end, it’s silently ignored and the default output path is used. It would be better to error out when --out is provided without a following path.

Suggested change
const outFile = outIndex !== -1 ? args[outIndex + 1] : null;
let outFile: string | null = null;
if (outIndex !== -1) {
const outValue = args[outIndex + 1];
if (!outValue || outValue.startsWith('--')) {
console.error('Error: --out option requires an output file path.');
printUsage();
process.exit(1);
}
outFile = outValue;
}

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +7

const CLI_PATH = resolve(__dirname, '../dist/cli.js');
const TMP_DIR = resolve(__dirname, '../tmp-cli-test');
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses __dirname, but the repo is ESM ("type": "module" in package.json), where __dirname is not defined. Use import.meta.url + fileURLToPath (or another vitest-provided helper) to derive paths in an ESM-safe way.

Suggested change
const CLI_PATH = resolve(__dirname, '../dist/cli.js');
const TMP_DIR = resolve(__dirname, '../tmp-cli-test');
import { fileURLToPath } from 'node:url';
const CLI_PATH = fileURLToPath(new URL('../dist/cli.js', import.meta.url));
const TMP_DIR = fileURLToPath(new URL('../tmp-cli-test', import.meta.url));

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +85
// 1. Source type — must include an index signature
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
[key: string]: unknown; // ← required
}

// 2. Field mapping with `as const` — external → internal
const userMapping = {
user_id: 'id',
first_name: 'firstName',
last_name: 'lastName',
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section states the source type “must include an index signature” and labels it “required”, but the library’s public constraint is TSource extends Record<string, unknown>, which does not require an index signature (object types with explicit properties satisfy it). This guidance is likely to confuse users and should be corrected (or the library constraint changed if an index signature is truly required).

Suggested change
// 1. Source type — must include an index signature
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
[key: string]: unknown; // ← required
}
// 2. Field mapping with `as const` — external → internal
const userMapping = {
user_id: 'id',
first_name: 'firstName',
last_name: 'lastName',
// 1. Source type — plain object type compatible with Record<string, unknown>
interface ApiUser {
user_id: string;
first_name: string;
last_name: string;
}
// 2. Field mapping with `as const` — external → internal
const userMapping = {
user_id: 'id',
first_name: 'firstName',
lastName: 'lastName',

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants