Skip to main content
luke@terminal:/$ ls
luke@terminal:/blog$ cat projex-cli-config-editor.md

I Got Tired of Editing the Projex Config File So I Built a CLI

2026-04-16 2026-04-17 4 min read

Projex is my internal tool for managing the projects page on this site. It stores all project data in projex.config.ts. That's by design. Type-safe, validated at build time, lives in the repo alongside the code. Works well.

Until you want to add a learning entry to a project.

Then you're opening a TypeScript file, finding the right project in the array, counting commas to make sure you don't mess up the object syntax, adding a new entry to the struggles array, making sure the date format is right, closing the brackets properly. All for something that should be one command.

I built this library. I wrote the config schema. And even I was getting it wrong. I'd actually written about Projex before - you can see it on my projects page. The dev.to integration broke immediately the first time I tried it. Same theme: I built it, but I wasn't actually using it like a real user would.


The Tipping Point

It was something small. I wanted to add a timeline entry to a project. Opened projex.config.ts, scrolled to the right project, added the entry. Saved. Build passed. Checked the site.

The entry was on the wrong project.

I'd pasted it inside the wrong object in the array. Easy mistake when your config is a nested TypeScript data structure and you're editing it like a text file. No validation catches it because the types are fine. The entry is valid. It's just... in the wrong place.

That was the moment I thought: there has to be a better way.


The CLI

I decided to build a CLI. Not because CLIs are fun to build. Because the alternative was continuing to hand-edit a TypeScript file every time I wanted to update my projects page.

The scope was straightforward. I needed commands for everything I was doing manually:

  • Add and remove projects
  • Add and remove learning entries, timeline entries, posts
  • Edit project fields
  • List what's in the config

The tricky part was that projex.config.ts is a real TypeScript file. Not JSON. Not YAML. It uses defineProjects(), has imports, can have comments. I needed to parse and modify it without destroying the structure.

I Googled around for TypeScript AST manipulation and found ts-morph. It reads the file as a tree of nodes rather than raw text. Find a project by ID, add an entry to an array, change a property value. All without touching the surrounding code, comments, or formatting.

I'd seen ts-morph mentioned before when I was working on something similar. It felt like the right tool for this specific job. The other option was just string manipulation, which seemed brittle.

The actual manipulation was less painful than I expected. I figured I'd have to write a lot of traversal code to navigate the nested structure. But ts-morph's API handles array operations cleanly. Adding to an array, removing from an array, finding nodes by specific properties. It just worked.


The Bugs I Found By Testing More

The first version worked. But there were edge cases I didn't catch until I sat down and wrote tests for scenarios that seemed unlikely.

The remove commands showed entries as #0, #1, #2 in the interactive prompt. Useless. You'd have to know which index corresponded to which entry. I changed them to show the actual content. Learning entries show [challenge] Struggled with state management.... Timeline entries show 2026-04-15 - v1.0 released. Posts show the title and date. Obvious in retrospect.

Then there was a subtler bug. The code that reads entries from the config filters for object literals in the array. If someone had a spread element in there, like [...sharedEntries, { type: 'challenge', text: 'actual entry' }], the filtered list would skip the spread. But the index reported to the user would be wrong. They'd pick what looked like entry 0, but the actual array index was 1. The wrong entry would get deleted silently.

Fixed that by tracking original indices through the parsed structure instead of using the filtered list position. Added an integration test with a spread element to prove it works.

The edit command had its own issue. It let you set any field on any project type with just a warning. --channel-id on a GitHub project? Sure, warned and proceeded. That's not a warning situation. That's a "you're doing something wrong" situation. Changed it to error and exit.


The --unset Flag

The one feature I didn't originally plan for was removing fields. The CLI could add, edit, and remove projects and entries. But if you accidentally set a field that shouldn't be there, you were back to editing the config file manually.

That defeated the purpose.

So I added --unset. projex edit project my-project --unset description removes the field entirely. Protected fields like id, type, and the array fields can't be removed. It can't be combined with other edit flags either, because that would be ambiguous.

Simple feature. But without it, the CLI wasn't complete. You'd still need to touch the config file for at least one class of changes.


Where Things Stand

881 tests. 55 test files. The integration tests create actual temporary config files and exercise the real parsing logic, not just mocked versions of it.

The CLI handles init, add, edit, remove, and list. Interactive mode when you don't provide flags, non-interactive mode with flags for scripting. Type-specific field validation that errors instead of warns. Descriptive labels on remove prompts instead of index numbers.

I've been using it for a day and it's already changed how I interact with my projects page. Adding a learning entry went from "open file, find project, count commas, hope for the best" to:

projex add learning projex --type challenge --text "Config file parsing is surprisingly tricky"