Project 2: task-nexus (Subcommands and State)
Build a task manager CLI with subcommands, config layering, and persistent local storage.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2 (Intermediate) |
| Time Estimate | 1 week |
| Main Programming Language | Go (Alternatives: Rust, Python) |
| Alternative Programming Languages | Rust, Python |
| Coolness Level | Level 3: Daily driver |
| Business Potential | 2: Micro-SaaS potential |
| Prerequisites | CLI basics, file I/O, JSON |
| Key Topics | Subcommands, config precedence, local storage |
1. Learning Objectives
By completing this project, you will:
- Design a multi-level command tree with verbs and nouns.
- Implement config layering across defaults, files, env, and flags.
- Build a durable local data store using JSON or SQLite.
- Provide stable machine-readable output for scripts.
- Add safe mutation commands with confirmation and undo patterns.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Command Trees and UX Grammar
Fundamentals
A command tree is the structural grammar that defines how users navigate a CLI. It is not just about parsing arguments; it is about making operations discoverable and safe. Subcommands group actions into a hierarchy: task-nexus add, task-nexus list, task-nexus done. This structure reduces cognitive load because the user learns one top-level verb and then explores its children. A good command tree also prevents ambiguous flags that change the meaning of a command silently. In a task manager, subcommands are critical because there are multiple actions (add, list, done, delete) that would otherwise be flags. Clear subcommands make help output readable and support tab-completion.
Deep Dive into the concept
Designing a command tree is similar to designing an API. Each node in the tree represents a concept, and each edge represents a permissible action. The first decision is whether the top-level command is a noun or a verb. For task-nexus, the noun is implied (tasks), so the top-level command can be short and subcommands can be verbs. This is the same reasoning behind git and kubectl. Once you pick that model, you must ensure that subcommands map to the user’s mental model. For example, users expect list to be read-only and delete to be destructive. If delete silently acts like done, you break expectations. The command tree should also reduce the surface of flags. Flags should modify a command, not replace it. If you find yourself adding --delete or --complete, you likely need a subcommand instead.
Help output must reflect the tree. Each node should have its own help page, showing required arguments, optional flags, and examples. The CLI Guidelines recommend keeping usage lines short and adding examples for the top few operations. This makes the tool discoverable without external docs. It also impacts error messages. If a user types task-nexus done without an ID, you must show the correct usage for that subcommand, not the top-level usage. Good parsers (Cobra, Clap) help with this, but you must still define the structure.
A command tree also affects compatibility. Once users adopt a command structure, changes break scripts. Therefore, you should treat the tree as a versioned contract. Adding new subcommands is safe, but renaming or re-parenting existing ones is disruptive. If you must change, provide aliases or deprecations. You should also think about how configuration and output modes apply across the tree. For example, --format json should behave consistently across list and search. That is easier to implement when commands share a common root configuration layer.
Finally, command trees relate to completion scripts. Shell completion exposes your command graph to the shell, making the tool easier to use. This becomes especially important as the number of commands grows. Designing a tree that is predictable and consistent makes completion more useful and reduces support questions.
How this fit on projects
This concept defines the structure of every command in task-nexus. It drives the subcommand list, the help output, and the completion scripts. It also informs your decision on how many global flags to support.
Definitions & key terms
- Command tree: Hierarchical grammar of subcommands.
- Verb-first model: Commands as actions (add, list, delete).
- Global flags: Flags applied to all subcommands.
- Discoverability: Ease with which users find features.
Mental model diagram (ASCII)
task-nexus
add
list
done
delete
config
show
How it works (step-by-step)
- Parse the first token after the binary name to identify the subcommand.
- Load subcommand-specific flags and positionals.
- Validate required args and show subcommand help on error.
- Execute the subcommand handler with shared config and storage.
Minimal concrete example
$ task-nexus add "Write chapter" --project cli
$ task-nexus list --project cli --format json
Common misconceptions
- “Everything can be a flag.” -> Flags that change behavior hide actions.
- “Help is optional.” -> Help output is the primary discovery surface.
- “Changing command names is harmless.” -> It breaks scripts and muscle memory.
Check-your-understanding questions
- Why prefer subcommands over flags for actions?
- What is the risk of renaming subcommands after users adopt them?
- How does a command tree affect shell completion?
Check-your-understanding answers
- Subcommands make actions explicit and reduce ambiguity.
- It breaks scripts and documentation relying on the old names.
- Completion uses the tree to suggest valid commands.
Real-world applications
gitandkubectlrely on command trees for scale.awsCLI groups commands by service, mirroring its API.
Where you will apply it
- See §3.2 Functional Requirements and §5.2 Project Structure.
- Also used in: Project 8: api-forge and Project 9: plug-master.
References
- Command Line Interface Guidelines (clig.dev)
- The Linux Command Line, Ch. 6 and 11
Key insights
A well-designed command tree is a usability feature that reduces errors and improves learning.
Summary
Subcommands are the backbone of scalable CLI design. They turn a flat command into a navigable interface.
Homework/Exercises to practice the concept
- Draft a command tree for a task manager with projects and tags.
- Identify one existing CLI that uses too many flags and redesign it with subcommands.
- Write help output for
task-nexus list.
Solutions to the homework/exercises
- A simple tree:
task-nexus add|list|done|delete|project. - Convert action flags into verbs (e.g.,
--delete->delete). - Include usage, options, and 2 examples.
2.2 Configuration Precedence and XDG Layout
Fundamentals
Configuration layering is how a CLI merges defaults, config files, environment variables, and flags into a single effective configuration. Users expect flags to override environment variables and environment variables to override config files. They also expect tools to store their config and state in the standard directories defined by the XDG Base Directory spec. If you store config in random paths or ignore environment overrides, users lose trust. For a task manager, correct precedence is essential because users will want to script it, set defaults, and override options for a single command.
Deep Dive into the concept
The XDG Base Directory specification defines where user-specific configuration, data, and cache should live. On Linux, XDG_CONFIG_HOME defaults to ~/.config, XDG_DATA_HOME defaults to ~/.local/share, and XDG_CACHE_HOME defaults to ~/.cache. On macOS, there are different conventions (~/Library/Application Support), but many CLI tools still honor XDG for cross-platform consistency. The goal is to avoid scattering files in the home directory. For task-nexus, you should store configuration in ~/.config/task-nexus/config.toml (or similar) and data in ~/.local/share/task-nexus/tasks.json or tasks.db.
Precedence is the mechanism that turns layered config into a single value. A typical order is: defaults < config file < environment variables < flags. This order respects explicit user intent. If a user exports TASK_NEXUS_FORMAT=json, they expect it to apply everywhere unless overridden by --format table. You should also expose a config show command that prints the effective configuration and its source, which is useful for debugging. That command is not just a convenience; it is part of making precedence visible.
Config files introduce schema design. You must decide the file format (TOML, YAML, JSON) and define keys that map to flags. Consistency matters: if your flag is --format, your config key should be format, and the environment variable should be TASK_NEXUS_FORMAT. Mapping rules should be documented and stable. Config also interacts with security. If you allow config to define paths for data files, you must validate those paths to avoid writing to unexpected locations.
Another subtlety is merging. Some config values are scalars (format, default project). Others are lists (default tags). Merging lists can be ambiguous: do env vars override or append? You must define this behavior clearly. For this project, keep it simple: env and flags override completely. Later, you can add explicit --add-tag or --remove-tag options if needed. Determinism matters; the same inputs should always yield the same effective config.
How this fit on projects
This concept controls how task-nexus loads config and how your CLI behaves across commands. It directly influences storage paths, output formatting defaults, and user trust.
Definitions & key terms
- XDG Base Directory: Standard config/data/cache paths.
- Precedence order: Rules for resolving conflicting config values.
- Effective config: The final merged configuration used by the CLI.
- Config schema: The expected keys and value types in a config file.
Mental model diagram (ASCII)
Defaults -> Config file -> Env vars -> Flags -> Effective config
How it works (step-by-step)
- Load defaults embedded in code.
- Read config file from XDG config directory.
- Apply environment variable overrides.
- Apply flag overrides and validate.
- Use the effective config for all commands.
Minimal concrete example
$ export TASK_NEXUS_FORMAT=json
$ task-nexus list
# outputs JSON unless overridden by --format table
Common misconceptions
- “Config files should override flags.” -> Flags are the most explicit intent.
- “XDG does not matter.” -> Users expect tools to follow OS conventions.
- “Env vars are only for secrets.” -> They are also used for defaults.
Check-your-understanding questions
- Why should flags override environment variables?
- Where should the task database live by default?
- How does
config showhelp debugging?
Check-your-understanding answers
- Flags are explicit per-command intent.
- In
XDG_DATA_HOME/task-nexus/. - It shows the effective value and its source.
Real-world applications
gituses system, global, and local config layers.kubectluses config files and env vars with clear precedence.
Where you will apply it
- See §3.2 Functional Requirements and §5.1 Development Environment Setup.
- Also used in: Project 5: env-vault and Project 10: distro-flow.
References
- XDG Base Directory Specification
- The Linux Command Line, Ch. 11
Key insights
Configuration layering is a trust contract. If users cannot predict overrides, they stop using config.
Summary
XDG layout and precedence rules make your CLI predictable, debuggable, and friendly to automation.
Homework/Exercises to practice the concept
- Map one flag to env var and config key using a consistent naming rule.
- Create a small config file and confirm precedence.
- Implement a
config showcommand that prints sources.
Solutions to the homework/exercises
--format->TASK_NEXUS_FORMAT->format.- Show that
--format tableoverrides env and file. - Print values with annotations like “from env” or “from file”.
2.3 Local Storage and Data Integrity
Fundamentals
A task manager is only useful if tasks persist across runs. That means you need a storage layer. For a CLI, the simplest choice is a JSON file on disk, but SQLite is also viable. The key is to ensure durability and avoid data loss. Writes must be atomic so that a crash does not corrupt the store. Reads must tolerate partial or malformed data with clear error messages. A storage layer is more than just a file format; it is the mechanism that enforces the data model and protects user data.
Deep Dive into the concept
JSON storage is straightforward and easy to inspect, but it has trade-offs. You must load the entire file to update a single task. For small datasets, this is fine. For large datasets, it becomes slow and memory-heavy. SQLite provides efficient updates, indexing, and concurrency, but adds complexity and requires schema migrations. For a learning project, JSON is a good default, with an optional SQLite extension. Whichever you choose, atomicity is non-negotiable. The standard pattern for file-based storage is: write to a temporary file, flush and fsync, then rename over the original. Renames are atomic on most systems, which means you never leave a partial file.
Data integrity also involves schema management. If you add new fields later (priority, tags), you must handle missing fields in old records. This is a form of migration. For JSON, you can provide defaults when fields are absent. For SQLite, you can add columns with default values. You should also define a stable task ID generation scheme. A simple incremental ID is fine if you lock the file or store the last ID in metadata. If you use UUIDs, you avoid collisions but lose sorting by creation order unless you add a timestamp.
Another integrity issue is concurrency. Users might run multiple commands simultaneously (e.g., add and list). If both read and write the same file, you can get races. For a simple CLI, you can use file locks (flock) to ensure one writer at a time. For SQLite, it handles locking internally. If you do not implement locks, you must document that concurrent writes are undefined. Since this project aims for production-grade quality, implement a lock from the beginning.
Finally, storage design affects output and filtering. If you store tasks as a list of objects, you can filter in memory by project, tag, or status. If you store them in SQLite, you can query directly. The data model should be explicit: tasks have id, title, project, due date, done flag, created_at, and updated_at. This model allows for stable JSON output and future extensions.
How this fit on projects
This concept drives the storage file format, atomic write strategy, and data model. It also informs the test plan for persistence and concurrency.
Definitions & key terms
- Atomic write: Write that either fully succeeds or not at all.
- Schema migration: Updating stored data to match new fields.
- File lock: Mechanism to prevent concurrent writes.
- Durability: Guarantee that data persists after a crash.
Mental model diagram (ASCII)
Command -> Load store -> Modify in memory -> Write temp -> Rename -> Done
How it works (step-by-step)
- Acquire a lock on the data file.
- Read the current dataset into memory.
- Apply the mutation (add, done, delete).
- Write new JSON to a temp file.
- fsync and rename the temp file over the old file.
Minimal concrete example
{
"next_id": 15,
"tasks": [
{"id":14,"title":"Write CLI primer","project":"cli","due":"2026-01-15","done":false}
]
}
Common misconceptions
- “JSON files do not need locks.” -> Concurrent writes can corrupt the file.
- “Rename is enough without fsync.” -> Without fsync, data can be lost after a crash.
- “IDs can be derived from array length.” -> Deletions will cause duplicates.
Check-your-understanding questions
- Why should you write to a temp file before replacing the original?
- What is the simplest safe ID generation method?
- When does SQLite become a better choice than JSON?
Check-your-understanding answers
- It prevents partial writes from corrupting the data.
- An incrementing counter stored in the file metadata.
- When datasets grow large or concurrent access becomes common.
Real-world applications
- Local task managers and note apps use atomic file updates.
- SQLite is widely used in CLI tools for local state.
Where you will apply it
- See §3.5 Data Formats and §5.10 Implementation Phases.
- Also used in: Project 5: env-vault and Project 9: plug-master.
References
- The Linux Programming Interface, Ch. 4 (file I/O)
- SQLite documentation on atomic commits
Key insights
Durable storage is a contract with the user; once broken, trust is hard to rebuild.
Summary
Choose a storage model, make writes atomic, and define a clear data schema. This is the foundation of a reliable task manager.
Homework/Exercises to practice the concept
- Implement an atomic write for a JSON file.
- Simulate a crash during write and verify data integrity.
- Add a file lock and test concurrent
addcommands.
Solutions to the homework/exercises
- Write to
tasks.json.tmp, fsync, rename. - Kill the process mid-write and ensure old file remains intact.
- Use
flockor OS-specific locking to block concurrent writes.
3. Project Specification
3.1 What You Will Build
You will build a CLI named task-nexus with subcommands to add, list, complete, and delete tasks. It stores tasks locally in a JSON or SQLite file under XDG directories. It supports both human-friendly table output and JSON output for scripts. It provides a config show command to display effective configuration values and sources.
3.2 Functional Requirements
- Subcommands:
add,list,done,delete,config show. - Persistent storage: JSON by default; optional SQLite backend.
- Config precedence: defaults < config file < env vars < flags.
- Output formats:
tableandjsonvia--format. - Filters: list by project, tag, and due date range.
- Safety:
deleterequires--forceor confirmation prompt. - Deterministic output: stable ordering and timestamps in tests.
3.3 Non-Functional Requirements
- Performance: List 10k tasks within 1 second.
- Reliability: Atomic writes and file locking.
- Usability: Clear help output and examples per subcommand.
3.4 Example Usage / Output
$ task-nexus add "Write CLI primer" --project cli --due 2026-01-15
Added task #14
$ task-nexus list --project cli --format table
ID Done Due Project Title
14 [ ] 2026-01-15 cli Write CLI primer
3.5 Data Formats / Schemas / Protocols
{
"id": 14,
"title": "Write CLI primer",
"project": "cli",
"tags": ["writing"],
"due": "2026-01-15",
"done": false,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
3.6 Edge Cases
- Listing with no tasks returns an empty list with exit code 0.
- Deleting a missing ID returns exit code 2 with a clear error.
- Config file missing is not an error; defaults apply.
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
# Build
go build -o task-nexus ./cmd/task-nexus
# Run
./task-nexus add "Test task" --project demo --due 2026-01-02
./task-nexus list --project demo --format json
3.7.2 Golden Path Demo (Deterministic)
$ TASK_NEXUS_NOW=2026-01-01T00:00:00Z ./task-nexus add "Write CLI primer" --project cli --due 2026-01-15
Added task #1
$ ./task-nexus list --project cli --format json
{"tasks":[{"id":1,"title":"Write CLI primer","project":"cli","done":false}]}
$ echo $?
0
3.7.3 Failure Demo (Deterministic)
$ ./task-nexus done 999
task-nexus: task id 999 not found
$ echo $?
2
$ ./task-nexus delete 1
task-nexus: refusing to delete without --force
$ echo $?
3
3.7.4 Exit Codes
0: Success.2: Not found / invalid id.3: Safety refusal (missing –force).
4. Solution Architecture
4.1 High-Level Design
+------------------+
| CLI Parser |
+------------------+
|
v
+------------------+ +------------------+
| Config Loader | --> | Storage Layer |
+------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| Command Handler | --> | Output Formatter |
+------------------+ +------------------+
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Parser | Subcommands and flags | Verb-based command tree. |
| Config loader | Merge defaults, file, env, flags | Precedence order. |
| Storage | Persist tasks | JSON by default, SQLite optional. |
| Formatter | Output table/JSON | Stable ordering and schema. |
4.3 Data Structures (No Full Code)
type Task struct {
ID int
Title string
Project string
Tags []string
Due string
Done bool
CreatedAt time.Time
UpdatedAt time.Time
}
4.4 Algorithm Overview
Key Algorithm: Atomic JSON Update
- Lock data file.
- Read data into memory.
- Modify dataset.
- Write to temp file and rename.
Complexity Analysis:
- Time: O(N) for reads/writes where N is number of tasks.
- Space: O(N) memory for JSON backend.
5. Implementation Guide
5.1 Development Environment Setup
mkdir task-nexus && cd task-nexus
mkdir -p cmd/task-nexus internal
5.2 Project Structure
cmd/task-nexus/
main.go
internal/
config/
store/
commands/
output/
fixtures/
5.3 The Core Question You’re Answering
“How do I design a command tree and config system that scales as features grow?”
5.4 Concepts You Must Understand First
- Command hierarchy and subcommand discovery.
- Config precedence and XDG paths.
- Atomic file writes and data integrity.
5.5 Questions to Guide Your Design
- Which actions are verbs and which are flags?
- How will users override defaults without editing files?
- Should deletion require confirmation?
5.6 Thinking Exercise
Sketch the command tree and write the help output for task-nexus and task-nexus add.
5.7 The Interview Questions They’ll Ask
- “Why use subcommands instead of flags for actions?”
- “How do you ensure config precedence is predictable?”
- “How do you avoid data loss in a file-based store?”
5.8 Hints in Layers
Hint 1: Start with JSON
Write tasks to XDG_DATA_HOME/task-nexus/tasks.json.
Hint 2: Add a config file
Read XDG_CONFIG_HOME/task-nexus/config.toml.
Hint 3: Add output formats
Create a --format flag and two renderers.
Hint 4: Add safety
Require --force for destructive commands.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| CLI basics | The Linux Command Line | Ch. 11 |
| File I/O | The Linux Programming Interface | Ch. 4 |
| Architecture | Clean Architecture | Ch. 1 |
5.10 Implementation Phases
Phase 1: Foundation (2-3 days)
Goals: command tree, basic storage. Tasks: parse args, implement add/list. Checkpoint: tasks persist across runs.
Phase 2: Config and Output (2-3 days)
Goals: config layering and JSON output.
Tasks: add env/config parsing, --format.
Checkpoint: config show prints effective values.
Phase 3: Safety and Polish (2 days)
Goals: deletion safety, filters, tests.
Tasks: --force, filter by project/tags.
Checkpoint: all edge cases handled.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Storage | JSON vs SQLite | JSON default | Simpler for learning. |
| Output | table vs json | both | Human + script compatibility. |
| Delete safety | confirm vs force | --force |
Scriptable and explicit. |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Config merging | env overrides file |
| Integration Tests | CLI flows | add -> list -> done |
| Edge Case Tests | Missing id | done 999 |
6.2 Critical Test Cases
- Add task and verify it appears in list.
- Done task and ensure
done=truein JSON. - Delete without
--forcereturns exit code 3.
6.3 Test Data
fixtures/tasks.json with 3 tasks
7. Common Pitfalls and Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Config overrides ignored | Wrong defaults | Apply precedence order. |
| Data corruption | JSON unreadable | Use atomic writes. |
| Unclear errors | Confused users | Show usage per subcommand. |
7.2 Debugging Strategies
- Print effective config with sources.
- Use small fixture files to reproduce bugs.
- Add verbose logging behind
--debug.
7.3 Performance Traps
- Full JSON rewrite on every update may be slow for huge task lists.
- Missing indexes in SQLite can slow list queries.
8. Extensions and Challenges
8.1 Beginner Extensions
- Add
task-nexus search <text>. - Add
--priorityflag.
8.2 Intermediate Extensions
- Add recurring tasks.
- Add CSV import/export.
8.3 Advanced Extensions
- Implement SQLite backend with migrations.
- Add sync with a remote server.
9. Real-World Connections
9.1 Industry Applications
- Local tooling for sprint planning or personal GTD workflows.
- Internal developer task managers with scripted output.
9.2 Related Open Source Projects
taskwarrior- full-featured CLI task manager.todo.txtCLI tools.
9.3 Interview Relevance
- Config layering and persistence patterns are common system design topics.
- Command tree design tests UX thinking.
10. Resources
10.1 Essential Reading
- The Linux Command Line, Ch. 11
- The Linux Programming Interface, Ch. 4
10.2 Video Resources
- “Designing Command Line Interfaces” talks
- “Atomic File Writes” tutorials
10.3 Tools and Documentation
- Cobra or Clap docs for subcommands
- XDG Base Directory spec
10.4 Related Projects in This Series
- Project 1: minigrep-plus for stream discipline.
- Project 5: env-vault for secure config handling.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain config precedence and XDG layout.
- I can explain why subcommands improve UX.
- I can explain how atomic writes prevent corruption.
11.2 Implementation
- All functional requirements are met.
- All tests pass.
- Data persists across restarts.
11.3 Growth
- I can describe this project in an interview.
- I documented design trade-offs.
- I can add a new subcommand without redesigning the tree.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Subcommands add/list/done/delete work.
- Tasks persist in local storage.
- Config precedence implemented.
Full Completion:
- Supports JSON and table output.
- Includes safety checks for deletion.
Excellence (Going Above and Beyond):
- SQLite backend with migrations.
- Robust filtering and query support.