ProfileForge CLI
What It Does
The coding-agent-mcp-tools repo publishes Agent Environment Profiles — curated MCP tool configurations for 8 coding agents (Claude, Codex, Cursor, Windsor, OpenCode, Cline, Roo Code, Kilo Code) across 3 operating systems (Ubuntu, macOS, Windows) and 2 tech stacks (Node.js + React, PHP + Laravel). Every combination is a discrete set of 9 markdown files. As the matrix grows, manually copying files, updating the README table, and keeping navigation.md in sync becomes error-prone and unsustainable. One missed update silently breaks the published state for every downstream user.
ProfileForge CLI (profile-cli)
ProfileForge CLI automates the entire profile lifecycle in a four-step pipeline:
1. validate — Pre-publish Gate
Reads every source profile from base-profiles/ and runs five checks on each file:
- File Presence: All 9 expected files are present (the setup file name is OS-aware —
system-setup.mdon Ubuntu,macos-setup.mdon macOS,windows-setup.mdon Windows). - Required Content: Required content keywords appear per file (e.g.,
mcp.jsonin every setup file regardless of OS). - OS-Specific Keywords: OS-specific keywords are present:
- Ubuntu:
bash - macOS:
brewand.zshrc - Windows:
PowerShellandwinget
- Ubuntu:
- Link Resolution: Relative
./links inside each file resolve to real files in the same directory. - Non-Empty: No file is empty.
Every failure across every agent, OS, and file is collected and reported in a single pass before the command exits — not just the first one.
2. publish
- Runs validation inline first; if that fails, the command exits with code 1 and nothing is written.
- On success, copies each file from
base-profiles/toprofiles/. - Smart Skipping: Skips files whose content is byte-for-byte identical (string comparison, not modification time —
git checkoutresets mtime, making timestamp checks unreliable). - Creates missing target subdirectories automatically.
3. update-readme
- Inserts or replaces the stack column in the README profile matrix table.
- State Check: Before writing any link, it checks whether the agent's profiles are actually present in
profiles/for all three OS directories. - Agents not yet published get a
-placeholder. - The table reflects the real state of
profiles/— no assumptions.
4. update-nav
- Adds a shields.io badge entry to
navigation.mdfor the stack. - Duplicate Check: Before inserting, it checks whether an entry for that stack path already exists; if one is found, it skips with no changes.
- Styling: Badge color and emoji are stack-specific:
- Node.js + React: Blue
#38BDF8 - PHP + Laravel: Purple
#8B5CF6
- Node.js + React: Blue
- The badge links to the first auto-detected agent's Ubuntu profile entry point.
All-in-One Command
All four steps are also wired into a single all command:
validate → publish → update-readme → update-nav
How We Built It
1. Runtime: Node.js >=18.0.0, Zero Runtime Dependencies
The tool relies exclusively on the Node standard library. The only imports across all source files are fs, path, and process.
- No External Packages: No
chalk,yargs,commander, orjs-yaml. - Hand-Written Helpers: Every utility, including the YAML parser, is implemented from scratch.
2. Architecture: Thin Command Router & Focused Modules
The architecture centers on a thin command router (src/index.js) that delegates to nine focused modules:
config.jsgenerate.jsvalidate.jspublish.jsupdate-readme.jsupdate-nav.jsstatus.jsparse-yaml.jsutils.js- Entry point:
bin/profile-cli.js
Design Principle: The router holds no business logic; it parses argv, resolves the stack and agent, and calls the appropriate module. Every module is independently testable.
3. Config as Single Source of Truth
config.js defines each stack, serving as the central configuration hub:
- Paths: Source and target paths.
- OS Variants: Definitions for Ubuntu, macOS, and Windows.
- Filenames: The 9 expected filenames (OS-aware:
system-setup.md,macos-setup.md,windows-setup.md). - Validation Rules: Per-file rules (
mustContain,mustContainByOS). - Metadata: README and navigation metadata.
Every other module reads from config. Adding a new stack requires adding just one object in one file.
4. Hand-Written YAML Parser
generate.js reads instructions.yaml template files to scaffold new agent profiles. Rather than pulling in js-yaml, we wrote parse-yaml.js from scratch.
- Capabilities: Handles quoted strings, block arrays, inline arrays, nested objects (up to 4 levels deep), and line comment stripping.
- Limitations: Explicitly does not handle YAML anchors or multi-line strings, as our instruction files do not use them.
5. Spec-Driven Build
Documentation (docs/scope.md, docs/prd.md, and docs/spec.md) was written before any implementation.
- Precision: The spec defined exact function signatures, return shapes, pipeline order, error messages, and exit codes.
- Decision Making: The "open questions" section of the PRD forced the pipeline ordering decision (
validate→publish→update-readme→update-nav) to be resolved on paper beforerunAll()was written. - Outcome: The wrong order (e.g.,
validate→readme→nav→publish) was caught and corrected during the spec pass, not during debugging.
Challenges We Ran Into
1. Agent Detection That Stays Correct as the Repo Grows
Any hardcoded agent list becomes a maintenance liability the moment a contributor adds a new agent without updating the CLI.
- Solution:
detectAgents(stackKey)inconfig.jsreadsbase-profiles/<stack>/for each OS variant, collects the agent folders per OS, and returns only the intersection — agents that have a folder in every OS variant. - Behavior: An agent with incomplete coverage is excluded with a warning written to
stderr, naming exactly which OS directories it is missing from. - Benefit: No config changes are required when new agents are added.
2. OS-Specific Validation Without Conditional Branching Per File
Different OS setup files require different keywords:
- Ubuntu (
system-setup.md): Must containbashandmcp.json. - macOS (
macos-setup.md): Must containbrew,.zshrc, andmcp.json. - Windows (
windows-setup.md): Must containPowerShell,winget, andmcp.json.
Putting if (os === 'ubuntu') blocks inside validate.js would mean touching the validator every time a rule changes.
- Solution: The
mustContainByOSshape inconfig.jscarries the OS-keyed keyword arrays. - Implementation:
validate.jsapplies them with a single lookup —rules.mustContainByOS[os]— without knowing anything about which OS requires what.
3. Idempotent README and Nav Updates
Running update-readme twice must produce the same result — no duplicate columns.
update-readme.js: Searches the existing table for the stack's header string before writing.- If found: It updates the existing cells in place.
- If not found: It appends a new column.
update-nav.js: Checks whetherprofiles/<stack>already appears innavigation.mdbefore inserting. If it does, it skips with no changes.- Benefit: Both commands are safe to re-run after a manual edit.
4. Content Comparison Instead of Mtime for Publish Skip Logic
A naive implementation would check file modification time (mtime) to decide whether to skip an unchanged file. However, git checkout resets mtime on every clone, meaning every file appears "new" to an mtime check, causing every file to be re-copied on every run.
- Solution: We compare raw content strings (
srcContent === destContent) instead. - Behavior: A file is only copied if its content has actually changed.
- Trade-off: This makes
publishgenuinely idempotent at the cost of reading both files before deciding — an acceptable overhead at this file count.
Accomplishments That We're Proud Of
1. The Validate Gate Is Real, Not Decorative
The validation step rigorously catches issues before any publication occurs:
- Missing Files: Detects if any of the 9 expected files are absent.
- Wrong OS Content: Flags missing OS-specific keywords (e.g.,
bashmissing in Ubuntu profiles,PowerShellmissing in Windows profiles). - Broken Links: Identifies relative links (e.g.,
./docker-setup.md) that point to non-existent files. - Empty Files: Ensures no file is empty.
Every failure is reported with the exact agent, OS, filename, and reason — before a single file is published. The gate holds.
2. One Command Runs the Whole Thing
The profile-cli all <stack> command automates the entire workflow:
- Validates every profile for every agent across all three OS variants.
- Publishes only changed files.
- Updates the README matrix table.
- Updates
navigation.md. - Prints a final summary detailing what was copied, what was skipped, and what to commit next.
What was previously a dozen manual steps that would regularly go out of sync is now a single invocation.
3. status Shows Exactly Where Things Stand
The status command provides a clear view of the repository state, grouping profiles by Stack → Agent → OS. It identifies three states per profile:
- Published: Content matches source.
- Unpublished: Missing from
profiles/. - Out of Sync: Exists but content differs from source.
For anything not yet live, it prints an actionable hint:
Run: profile-cli publish <stack> --agent <agent>
4. Zero Dependencies Shipped and Held
We strictly adhered to the constraint of no external packages (no chalk, no yargs, etc.):
- ANSI Colors: Defined manually in
utils.js. - Argument Parsing: Implemented as a concise 10-line
parseArgs()function. - Portability: The tool runs on any machine with Node >=18 and nothing else.
5. The Spec-Driven Process Worked on a Real Build
The open questions table in docs/prd.md forced us to resolve critical decisions before implementation:
- Pipeline ordering.
- "Out of sync" detection logic.
- README table structure.
These resolved decisions meant runAll() in index.js was written once and not revisited. The spec had enough precision that the build phase was execution, not re-design.
What We Learned
1. Spec Before Code Is Not Ceremony — It Surfaces Real Decisions
The pipeline ordering question (validate → publish → update-readme → update-nav vs. alternatives discussed in docs/prd.md) would have been resolved arbitrarily by whoever wrote the code first if we hadn't flagged it explicitly.
- Outcome: Resolving it in the spec, with reasoning documented, meant no revisiting it mid-build.
- Lesson: Explicit decision-making in the design phase prevents architectural drift during implementation.
2. Dynamic Detection Beats Configuration for Contributor-Facing Tools
A static ALL_AGENTS list exists in config.js and is used by status to show which agents are not yet in base-profiles/ at all, but the pipeline never uses it to decide which agents to process. Instead, detectAgents() determines what actually runs.
- Why It Matters:
- Static Approach: If a contributor adds a new agent folder but forgets to update a config list, the tool silently does nothing.
- Dynamic Approach: The tool automatically picks up new agent folders without configuration changes.
3. Read-Only Validation Is the Correct Design
Keeping validate.js strictly read-only — calling no write APIs and taking no side effects — provided significant flexibility:
- It can be run at any point without risk: before publish, as a standalone audit, or as a future CI step.
- Lesson: Any validation approach that modifies files to "fix" them loses the property of being a safe, predictable gate.
What's Next for ProfileForge CLI
--dry-runflag onpublishandall— preview exactly which files would be copied or skipped without touching the filesystem. Specified indocs/prd.mdunder "What We'd Add With More Time."diff <stack> [--agent <name>]— human-readable diff between source profile and published profile before deciding to publish. Also from the PRD.- More stacks — Python/Django, Ruby on Rails, Go follow the same agent × OS × stack matrix and would plug into the existing config shape without changes to any handler module.
- Interactive
initflow — guided prompts for contributors creating a new agent's profiles from scratch: stack selection, OS coverage confirmation, file scaffolding, and immediate validate run. - GitHub Issues from validation failures — auto-create a labelled issue for each validate failure so missing or broken profiles become tracked work items, not silent gaps.
npx profile-cli— publish to npm so maintainers can run the tool without cloning the repo.
Built With
- claude-code
- github
- kilocode
- node.js
- powershell
- shell-script
- typescript


Log in or sign up for Devpost to join the conversation.