Project 3: init-wizard (Interactive Scaffolding)

Build an interactive project scaffolding CLI that is safe for humans and predictable in automation.

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: Fun and useful
Business Potential 3: Indie tooling
Prerequisites CLI basics, file I/O, templating
Key Topics TTY detection, prompts, validation, templates

1. Learning Objectives

By completing this project, you will:

  1. Design a prompt flow that works interactively and in non-interactive mode.
  2. Detect TTY presence and fall back to flags safely.
  3. Validate user input and refuse unsafe project names.
  4. Render templates and write files without accidental overwrite.
  5. Provide deterministic scaffolding outputs for tests and CI.

2. All Theory Needed (Per-Concept Breakdown)

2.1 TTY Detection and Interactive UX

Fundamentals

Interactive CLIs behave differently depending on whether the user is in a terminal. When stdin or stdout is not a TTY, prompts can hang the process and break automation. TTY detection lets you decide when it is safe to ask questions and when to force non-interactive mode. This is not a minor detail: a single blocking prompt can stall CI pipelines or scripts. The UX design must assume both contexts. When a TTY is present, prompts can guide users with defaults and validation. When a TTY is absent, flags must provide the same inputs, and the tool must fail fast if required values are missing.

Deep Dive into the concept

TTY detection is typically done with isatty on stdin or stdout. The choice depends on the prompt model. Most wizards read from stdin and write to stdout, so you should treat the tool as interactive only if stdin is a TTY and stdout is a TTY. This avoids strange cases where the tool can read prompts but output is redirected. The behavior should be explicit: if --no-prompt is set or stdin is not a TTY, the tool must refuse to prompt and require flags instead. This prevents hidden hangs and makes automation safe.

Interactive UX design also involves the structure of prompts. Users expect defaults, auto-complete, and clear validation errors. A good prompt flow asks for the most important decisions first (project name, language, license), and then optional details. You should avoid asking questions that can be derived from flags or defaults. That reduces friction for experienced users. When prompts are used, validation should be immediate, not deferred to the end. For example, if a project name contains path separators, the wizard should reject it immediately and explain why.

Non-interactive mode is not just a special case. It is a first-class requirement. Every prompt must have an equivalent flag. That is the only way to guarantee that automation can reproduce the same outcome. This is also the reason for deterministic outputs. If the wizard writes files with timestamps, tests become flaky. You should allow a --fixed-time or INIT_WIZARD_NOW env var to freeze timestamps. The exact same inputs should yield the exact same file tree, byte-for-byte.

Finally, interactive UX must be safe. The wizard should detect if the output directory exists and require --force or --overwrite to proceed. This is especially important because wizards can create many files. A single mistake could overwrite a real project. Good UX is as much about preventing catastrophic mistakes as it is about convenience.

How this fit on projects

This concept defines when the wizard prompts, how it detects terminals, and how it safely handles automation. It is the core of the tool’s human-friendly behavior.

Definitions & key terms

  • TTY detection: Checking if stdin/stdout are terminals.
  • Non-interactive mode: Mode where prompts are disabled.
  • Prompt flow: Ordered sequence of questions and defaults.
  • Validation: Rejecting invalid inputs early.

Mental model diagram (ASCII)

stdin/stdout TTY?
   | yes -> prompt flow -> validated inputs
   | no  -> require flags -> validated inputs

How it works (step-by-step)

  1. Check isatty(stdin) and isatty(stdout).
  2. If no TTY or --no-prompt, disable prompts.
  3. Gather inputs from flags or config defaults.
  4. Validate inputs and ask again (interactive) or fail (non-interactive).
  5. Proceed to template rendering and file creation.

Minimal concrete example

$ init-wizard new --name demo --lang go --license mit --no-prompt
# Runs without any prompts

Common misconceptions

  • “Prompting is always safe.” -> It can hang CI.
  • “TTY detection only needs stdout.” -> stdin matters for user input.
  • “Defaults remove the need for flags.” -> Automation requires flags.

Check-your-understanding questions

  1. Why should prompts be disabled when stdin is not a TTY?
  2. How can a CLI provide deterministic output for prompts?
  3. Why should every prompt have a flag equivalent?

Check-your-understanding answers

  1. Because prompts cannot be answered and will block.
  2. Allow fixed timestamps and deterministic defaults.
  3. To allow non-interactive automation and reproducibility.

Real-world applications

  • npm init, cargo new, and create-react-app all use this pattern.
  • CI pipelines rely on non-interactive modes for scaffolding.

Where you will apply it

References

  • Advanced Programming in the UNIX Environment, Ch. 18 (TTY)
  • The Pragmatic Programmer, Ch. 3 (defensive design)

Key insights

Interactivity is a feature only when the environment can support it.

Summary

A safe wizard detects TTYs, provides non-interactive equivalents, and validates early to prevent mistakes.

Homework/Exercises to practice the concept

  1. Write a tiny program that prints whether stdin is a TTY.
  2. List all prompts in a wizard and map them to flags.
  3. Design a prompt flow that can be skipped entirely via flags.

Solutions to the homework/exercises

  1. Use isatty on stdin and stdout.
  2. Example: --name, --lang, --license map to prompts.
  3. Provide defaults and a --no-prompt flag.

2.2 Template Rendering and File System Safety

Fundamentals

A scaffolding tool is only as good as its templates. Templates encode the structure of the generated project and must be rendered into files safely. File system safety means never overwriting existing files without explicit consent, and never writing outside the intended output directory. You must also ensure that template variables are validated and sanitized before rendering to avoid generating invalid files. A single unsafe path in a template can lead to security issues or destructive behavior.

Deep Dive into the concept

Template rendering involves substituting variables (project name, language, license) into predefined file templates. This can be done with a templating engine or simple string replacement. The key is to define a template schema and a list of required variables. If a required variable is missing, the tool should fail with a clear error. This prevents partially generated projects with broken files.

File system safety begins with path handling. The output directory must be resolved to an absolute path and normalized to avoid path traversal. For example, a project name of ../../ should be rejected, not used to create a directory outside the working folder. You should enforce a strict regex for project names, such as ^[a-zA-Z0-9_-]+$. When writing files, always join paths relative to the output directory and confirm that the final path still starts with that directory.

Overwriting is another critical concern. Many scaffolding tools use an overwrite policy: if the directory exists, require --force or --overwrite. If the directory is empty, you might allow it. If it contains files, you must stop unless explicitly allowed. For individual files, you can also provide a --merge strategy, but for this project, a strict policy is better. It keeps the tool safe and predictable.

Templates should be deterministic. That means file ordering, content, and timestamps should not depend on runtime randomness. If templates include a generated UUID, tests become non-repeatable. The fix is to allow an explicit --seed or --now parameter that freezes randomness and time. This makes the tool testable and ensures that users can compare outputs across runs.

Finally, consider template organization. Keep templates in a dedicated directory and include metadata about which files are generated. This makes future updates and testing easier. It also allows you to build a “dry-run” mode that prints what would be generated without writing files.

How this fit on projects

This concept drives template design, file generation, and safety checks. It informs your output format and the --force behavior in the spec.

Definitions & key terms

  • Template schema: The list of variables and files in a scaffold.
  • Path traversal: Using ../ to escape a directory.
  • Deterministic output: Same inputs yield identical outputs.
  • Dry run: Show intended actions without writing files.

Mental model diagram (ASCII)

Inputs -> Validate -> Render templates -> Write files -> Summary

How it works (step-by-step)

  1. Load template definitions and required variables.
  2. Validate inputs and sanitize project name.
  3. Create output directory safely.
  4. Render each template file and write to disk.
  5. Print a summary of created files.

Minimal concrete example

Template: README.md
"# {{project_name}}"

Rendered:
"# demo"

Common misconceptions

  • “Paths are safe if you join strings.” -> Join does not prevent traversal.
  • “Overwriting is fine if the user said yes once.” -> Always require explicit consent per run.
  • “Determinism is only for tests.” -> Users also benefit from reproducibility.

Check-your-understanding questions

  1. Why must project names be validated before use?
  2. What is the simplest safe overwrite policy?
  3. How does deterministic output help users?

Check-your-understanding answers

  1. To prevent invalid paths and unsafe file writes.
  2. Refuse if directory exists unless --force is provided.
  3. It makes outputs reproducible and testable.

Real-world applications

  • cargo new and npm init validate names and refuse unsafe paths.
  • Static site generators rely on deterministic template rendering.

Where you will apply it

References

  • The Pragmatic Programmer, Ch. 3
  • OWASP File Path Traversal notes

Key insights

Safe scaffolding requires strict path validation and explicit overwrite rules.

Summary

Template rendering is powerful but dangerous without validation and deterministic rules. Build safety first.

Homework/Exercises to practice the concept

  1. Design a regex for valid project names.
  2. Implement a dry-run mode that prints intended file writes.
  3. Simulate a path traversal input and ensure it is rejected.

Solutions to the homework/exercises

  1. Use ^[a-zA-Z0-9_-]+$ and reject others.
  2. Print file paths instead of writing them.
  3. Reject any name containing .. or /.

2.3 Non-Interactive Modes, Validation, and Defaults

Fundamentals

Non-interactive mode is the backbone of automation. It forces you to define which inputs are required and how defaults are applied. When prompts are disabled, the tool must either use defaults or fail with a clear error. This is the difference between a reliable CLI and one that silently makes poor assumptions. Validation is equally important. Inputs like language, license, and package name must be checked against allowed values. Defaults must be deterministic and documented.

Deep Dive into the concept

The best way to design non-interactive mode is to treat prompts as a UI over a strict parameter set. Each prompt maps to a required flag or a config default. If the flag is not provided and no default exists, the CLI should exit with a non-zero code and a message that lists missing fields. This is similar to HTTP API validation: reject invalid input early and explain why.

Defaults should not be random. For example, defaulting language to “Go” might be fine, but it should be documented and consistent. If you use the current directory name as the default project name, you should also expose it in --help and allow overriding. Determinism is important for testing; you can allow --seed or INIT_WIZARD_SEED to control any random behavior.

Validation should be layered. First, validate flag types (e.g., --lang must be one of go, rust, python). Second, validate derived values (project name, module name). Third, validate file system state (directory exists, permissions). Each validation should have its own error message and exit code. For example, exit code 2 for invalid input, exit code 3 for file system conflicts. This makes automation more reliable because scripts can react to specific failures.

Non-interactive mode also affects output. A wizard should still print a summary of what it created, but it should avoid progress spinners or interactive prompts. Output should be stable and machine-readable if --json is requested. Consider supporting a --print mode that outputs the rendered files to stdout instead of writing to disk. This makes integration with other tools easier.

How this fit on projects

This concept determines how flags replace prompts, how defaults are chosen, and how validation errors are surfaced. It ensures the wizard works in CI and scripts.

Definitions & key terms

  • Required input: A value that must be provided by flag or default.
  • Validation error: A failure due to invalid user input.
  • Deterministic defaults: Defaults that do not change across runs.
  • Exit code taxonomy: Mapping of error types to exit codes.

Mental model diagram (ASCII)

Flags/config -> Validate -> Missing? -> Error
                   | no
                   v
              Render templates

How it works (step-by-step)

  1. Collect inputs from flags and config.
  2. Apply defaults for missing optional values.
  3. Validate required inputs and allowed values.
  4. If validation fails, exit with a clear error code.
  5. Proceed to render and write files.

Minimal concrete example

$ init-wizard new --no-prompt
init-wizard: missing required flags: --name, --lang, --license
# exit code 2

Common misconceptions

  • “Defaults are always safe.” -> Poor defaults can create unintended projects.
  • “Validation can be skipped in non-interactive mode.” -> It must be stricter, not looser.
  • “One exit code is enough.” -> Multiple error types improve automation.

Check-your-understanding questions

  1. Why is non-interactive mode more strict than interactive mode?
  2. How do deterministic defaults aid testing?
  3. What is a good exit code strategy for validation errors?

Check-your-understanding answers

  1. Because there is no human to resolve missing or invalid inputs.
  2. They allow reproducible outputs and golden tests.
  3. Use a dedicated non-zero code (e.g., 2) for invalid input.

Real-world applications

  • docker CLI validates inputs and fails fast with clear errors.
  • npm init -y uses deterministic defaults for non-interactive mode.

Where you will apply it

References

  • The Pragmatic Programmer, Ch. 3
  • Command Line Interface Guidelines

Key insights

Automation-safe CLIs require strict validation and deterministic defaults.

Summary

Non-interactive mode is not a shortcut; it is the strictest path through your CLI. Design it intentionally.

Homework/Exercises to practice the concept

  1. Define required flags for a wizard and list their defaults.
  2. Create a validation table mapping errors to exit codes.
  3. Add a --print or dry-run mode and test outputs.

Solutions to the homework/exercises

  1. Required: --name, --lang, --license; defaults for optional fields.
  2. Invalid input -> exit 2, fs conflict -> exit 3.
  3. Use dry-run to print file list without writing.

3. Project Specification

3.1 What You Will Build

A CLI named init-wizard that scaffolds new projects. It supports interactive prompts when running in a TTY and a fully non-interactive mode with flags. It validates inputs, renders templates, and writes files safely. It refuses to overwrite existing directories unless --force is provided. It supports deterministic outputs via fixed timestamps and a seed.

3.2 Functional Requirements

  1. Interactive prompts for project name, language, license.
  2. Non-interactive flags: --name, --lang, --license, --no-prompt.
  3. Validation of names, allowed languages, and licenses.
  4. Template rendering with variable substitution.
  5. Safe overwrite requiring --force.
  6. Dry-run mode that prints intended file writes.
  7. Deterministic outputs via INIT_WIZARD_NOW and INIT_WIZARD_SEED.

3.3 Non-Functional Requirements

  • Performance: scaffold in under 1 second for small templates.
  • Reliability: no partial project creation on error.
  • Usability: clear prompts and error messages.

3.4 Example Usage / Output

$ init-wizard new
? Project name: demo
? Language: Go
? License: MIT
Scaffolded ./demo

3.5 Data Formats / Schemas / Protocols

Template variables schema:

{
  "project_name": "demo",
  "language": "go",
  "license": "mit",
  "module_path": "github.com/user/demo"
}

3.6 Edge Cases

  • stdin not a TTY with prompts enabled -> fail with message.
  • invalid project name -> exit code 2.
  • output directory exists -> exit code 3 unless --force.

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

# Build
go build -o init-wizard ./cmd/init-wizard

# Interactive
./init-wizard new

# Non-interactive
./init-wizard new --name demo --lang go --license mit --no-prompt

3.7.2 Golden Path Demo (Deterministic)

$ INIT_WIZARD_NOW=2026-01-01T00:00:00Z INIT_WIZARD_SEED=1337 ./init-wizard new --name demo --lang go --license mit --no-prompt
Scaffolded ./demo

$ ls demo
README.md  go.mod  main.go

$ echo $?
0

3.7.3 Failure Demo (Deterministic)

$ ./init-wizard new --no-prompt
init-wizard: missing required flags: --name, --lang, --license
$ echo $?
2

$ ./init-wizard new --name "../../" --lang go --license mit --no-prompt
init-wizard: invalid project name
$ echo $?
2

3.7.4 Exit Codes

  • 0: Success.
  • 2: Validation error or missing required flags.
  • 3: Output directory exists without --force.

4. Solution Architecture

4.1 High-Level Design

+------------------+
| CLI Parser       |
+------------------+
          |
          v
+------------------+     +------------------+
| Input Collector  | --> | Validator        |
+------------------+     +------------------+
          |                        |
          v                        v
+------------------+     +------------------+
| Template Engine  | --> | File Writer      |
+------------------+     +------------------+

4.2 Key Components

Component Responsibility Key Decisions
Parser Flags and commands Separate new subcommand.
Input collector prompts or flags TTY-aware mode selection.
Validator validate values strict regex for names.
Template engine render files deterministic rendering.
File writer write output safe overwrite and dry-run.

4.3 Data Structures (No Full Code)

type Inputs struct {
    Name      string
    Lang      string
    License   string
    Module    string
    Force     bool
    NoPrompt  bool
}

4.4 Algorithm Overview

Key Algorithm: Scaffold Generation

  1. Collect inputs from prompts or flags.
  2. Validate inputs and determine output dir.
  3. Load templates and render with inputs.
  4. Write files or print dry-run summary.

Complexity Analysis:

  • Time: O(F) where F is number of files.
  • Space: O(S) where S is total template size.

5. Implementation Guide

5.1 Development Environment Setup

mkdir init-wizard && cd init-wizard
mkdir -p cmd/init-wizard templates

5.2 Project Structure

cmd/init-wizard/
  main.go
internal/
  prompt/
  validate/
  render/
  fs/
templates/
  go/
  rust/

5.3 The Core Question You’re Answering

“How do I make a CLI friendly to humans without blocking automation?”

5.4 Concepts You Must Understand First

  1. TTY detection and prompt safety.
  2. Template rendering and path validation.
  3. Strict non-interactive validation.

5.5 Questions to Guide Your Design

  1. When should prompts be disabled?
  2. What is the safest overwrite policy?
  3. How do you expose deterministic outputs for tests?

5.6 Thinking Exercise

Write a prompt flow and the equivalent flag-only invocation. Ensure they produce identical outputs.

5.7 The Interview Questions They’ll Ask

  1. “How do you detect when prompting is safe?”
  2. “Why is validation stricter in non-interactive mode?”
  3. “How do you prevent path traversal?”

5.8 Hints in Layers

Hint 1: Start with a single template Implement only one language template first.

Hint 2: Add TTY checks Disable prompts if stdin is not a TTY.

Hint 3: Add dry-run Print file paths without writing.

Hint 4: Add deterministic flags Use --seed and --now to freeze randomness/time.

5.9 Books That Will Help

Topic Book Chapter
Defensive programming The Pragmatic Programmer Ch. 3
Terminal input Advanced Programming in the UNIX Environment Ch. 18

5.10 Implementation Phases

Phase 1: Prompts and Inputs (2-3 days)

Goals: prompt flow, validation. Checkpoint: interactive flow works in TTY.

Phase 2: Templates and Output (2-3 days)

Goals: render templates, write files safely. Checkpoint: scaffolding generates correct files.

Phase 3: Automation and Polish (2 days)

Goals: non-interactive mode, dry-run, deterministic output. Checkpoint: CI-safe flag-only runs.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Prompt library Bubble Tea vs simple readline simple prompt first fewer dependencies.
Templates embedded vs external external easier to edit.
Overwrite confirm vs force --force safe and scriptable.

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Validation invalid names rejected
Integration Tests CLI flows non-interactive scaffolding
Edge Case Tests path traversal name “../../”

6.2 Critical Test Cases

  1. Non-interactive with missing flags returns exit code 2.
  2. Existing directory without --force returns exit code 3.
  3. Deterministic run with fixed seed produces same output.

6.3 Test Data

fixtures/templates/simple-go/

7. Common Pitfalls and Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Prompting in CI Pipeline hangs Disable prompts without TTY.
Unsafe paths Files written outside dir Validate project name and paths.
Overwrite surprises Existing files lost Require –force.

7.2 Debugging Strategies

  • Print detected TTY state in --debug mode.
  • Add a dry-run mode to inspect planned files.
  • Use fixtures with known outputs.

7.3 Performance Traps

  • Rendering large templates repeatedly without caching.

8. Extensions and Challenges

8.1 Beginner Extensions

  • Add --module flag for Go module path.
  • Add --license-file to inject custom licenses.

8.2 Intermediate Extensions

  • Add template packs loaded from a directory.
  • Add a list-templates command.

8.3 Advanced Extensions

  • Add template updates with versioning.
  • Add plugin system for community templates.

9. Real-World Connections

9.1 Industry Applications

  • Project generators like cargo new and yeoman follow these patterns.
  • cargo-generate for Rust project templates.
  • cookiecutter for Python templates.

9.3 Interview Relevance

  • Validation and safe file operations are frequent CLI design topics.

10. Resources

10.1 Essential Reading

  • The Pragmatic Programmer, Ch. 3
  • Advanced Programming in the UNIX Environment, Ch. 18

10.2 Video Resources

  • “Building Safe CLI Wizards” talks

10.3 Tools and Documentation

  • Prompt libraries (Bubble Tea, Inquirer)
  • Template libraries for your language

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain when prompts are safe and when they are not.
  • I can explain how to validate project names.
  • I can explain deterministic scaffolding outputs.

11.2 Implementation

  • All functional requirements are met.
  • Non-interactive mode works in CI.
  • Safe overwrite rules are enforced.

11.3 Growth

  • I can describe how I would add a new template.
  • I documented a clear error taxonomy.
  • I can demo both interactive and automated flows.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Interactive prompts work in a TTY.
  • Non-interactive flags create identical output.
  • Validation and safe overwrite enforced.

Full Completion:

  • Dry-run mode and deterministic flags.
  • Multiple templates supported.

Excellence (Going Above and Beyond):

  • Template pack manager with versioning.
  • Plugin-based template discovery.