Search Tech Journey

Find topics, journeys and posts

back to blog
system designbeginner 28m2026-06-10

MS Stack Ch 1 — Developer tooling

PowerShell, Git's three trees, VS Code, browser DevTools, and the package managers that run your day. The boring tools that determine whether you'll feel fluent or fighting for the next two years.

Chapter 1 of From Novice to Fluent on the Modern Microsoft Web Stack — a 22-chapter self-study plan.

Why this chapter

Before you write a single line of C# or React, the tools have to feel like extensions of your hands. The difference between a senior engineer and a junior is rarely "knows more frameworks" — it is more often "spends 3 seconds on what takes a junior 3 minutes". That gap is almost entirely tooling fluency: shell, version control, editor, browser inspector, and package manager. Two weeks invested here saves you two years of friction.

The Microsoft web stack pulls together five tool families that you will touch every single working day: PowerShell as the shell, Git as the source of truth, VS Code as the editor and debugger, the browser's DevTools as the runtime inspector, and three different package managers (winget for the OS, pnpm for JavaScript, dotnet CLI plus NuGet for .NET). Each one has a shallow API you can stumble through and a deep model that, once internalised, makes every problem look obvious. The job of this chapter is to push you across that line.

Shipping-tier means you can navigate a repo from PowerShell, stage and push from VS Code without thinking, set a JavaScript breakpoint in DevTools, and install packages without copy-pasting from Stack Overflow. Expert-tier means the tools serve you: you script repetitive workflows in PowerShell with error handling, rewrite history with rebase -i without breaking a sweat, debug minified production JavaScript via source maps, and own a ~/.gitconfig plus ~/.vscode/settings.json with strong opinions.

You finish this chapter when you can sit in front of any Windows dev box, clone a repo, install its dependencies, debug it across the API and the SPA from a single F5, and ship a clean PR — without Googling a single command.

Concepts and depth

PowerShell — variables, pipelines, $env:, cmdlets, scripts, execution policy

PowerShell is not bash with a different syntax. It is an object pipeline: every cmdlet emits .NET objects, and every downstream stage works on those objects directly. There is no parsing of column-aligned text. Get-ChildItem returns System.IO.FileInfo instances; pipe them into Where-Object, Select-Object, or Sort-Object and you are operating on real properties. This single design choice eliminates an entire class of bash bugs (filenames with spaces, columns shifting between OS versions, locale-dependent output formats).

Variables are $name = "foo"; they are typed under the hood to whatever .NET type the right-hand side produced. Environment variables live in the $env: drive: $env:PATH, $env:ASPNETCORE_ENVIRONMENT. Reading is free, writing only affects the current process — for persistent changes use [Environment]::SetEnvironmentVariable("NAME", "value", "User"). Cmdlets follow Verb-Noun naming (Get-Process, Set-Content, New-Item, Remove-Item, Test-Path, Invoke-RestMethod). Aliases (ls, cd, cat, gci, gc) exist for interactive use; in scripts always spell out the full cmdlet — your reviewer will thank you.

Scripts live in .ps1 files. They run only if the execution policy allows it. The default on Windows is Restricted, which means even your own script will not run. The standard developer-machine setting is Set-ExecutionPolicy RemoteSigned -Scope CurrentUser — your local scripts run, but scripts downloaded from the internet must be signed. Treat the execution policy as a guard-rail, not as security: it is trivially bypassed by anyone with intent (powershell -ExecutionPolicy Bypass -File ...); its job is to stop you from double-clicking a malicious .ps1 by mistake.

Good enough to ship
  • • Variables, pipelines, Where-Object, Select-Object, ForEach-Object
  • • Read JSON with ConvertFrom-Json, write back with ConvertTo-Json -Depth 10
  • $LASTEXITCODE after every external command in a script
  • • Set execution policy once: RemoteSigned -Scope CurrentUser
Expert tier
  • • Write advanced functions with [CmdletBinding()] and param([Parameter(Mandatory)]...)
  • Get-Member reflexively to inspect any pipeline stage
  • $PROFILE curated with aliases, prompt, Import-Module for posh-git, PSReadLine
  • try/catch with $ErrorActionPreference = 'Stop' and -ErrorAction Stop on cmdlets
# bash: parse text out of `ls` and hope columns line up
ls -la | awk '{print $5, $9}' | sort -nr | head -5

# PowerShell: real objects, real properties, no parsing
Get-ChildItem |
  Sort-Object Length -Descending |
  Select-Object -First 5 Name, Length, LastWriteTime

The PowerShell prompt customisation lives in $PROFILE (run notepad $PROFILE to edit). Typical contents: a prompt function that shows the current Git branch, Import-Module posh-git, Set-PSReadLineOption -PredictionSource History for fish-style autocomplete, and a couple of aliases (Set-Alias k kubectl, function g { git $args }).

Git — working tree, index, commits, branches, remotes, merge vs rebase, conflicts, tags, .gitignore, stash

The single most important diagram in version control is the three-tree model. Every Git command moves files between four locations: the working tree (files on disk you edit), the index also called the staging area (a snapshot of what the next commit will contain), HEAD (the tip of your current branch — the last commit), and the remote (origin). git add promotes working tree → index; git commit promotes index → HEAD; git push promotes HEAD → remote; git checkout and git reset pull state back the other way. Once you internalise this, git status stops being mysterious — it is just reading the diff between these four locations and summarising it.

A branch in Git is not a folder of files; it is a moving pointer to a commit. A commit is an immutable snapshot of the whole tree, identified by a SHA-1 hash, with one or more parents. The branch pointer moves forward each time you commit. HEAD is a pointer to a branch (or, in detached state, directly to a commit). Remotes are named references to other Git repositories (origin, upstream); git fetch downloads their commits into your local repo without touching your working tree, while git pull is fetch followed by merge (or rebase if you configure it). In a team, prefer git fetch && git rebase origin/main to keep history linear.

Merge vs rebase is the question juniors get wrong most often. A merge creates a new commit with two parents, preserving the actual order of work. A rebase rewrites your local commits as if they were authored on top of the latest base, producing a linear history. Use merge for long-lived integration branches (e.g., merging release/1.0 into main) where you want the historical record preserved. Use rebase for short-lived feature branches before pushing or before opening a PR — clean history, easier git bisect. The cardinal rule: never rebase commits that are already on a shared branch, because rebasing rewrites SHAs and forces collaborators into a recovery situation.

Conflicts happen when two branches change the same lines. Git inserts <<<<<<<, =======, >>>>>>> markers; resolve by editing the file, git add the resolved file, then git rebase --continue (or git merge --continue). The 3-way merge tools in VS Code (open the Source Control panel → click the conflicted file) show ours / theirs / common ancestor side by side; this is much faster than editing markers by hand. Tags are named pointers to commits, used for releases: git tag -a v1.2.0 -m "release" then git push origin v1.2.0. Annotated tags (-a) carry metadata; lightweight tags do not — use annotated for releases.

.gitignore is a per-directory list of glob patterns Git will not track. Cherry-pick the right templates from github.com/github/gitignoreVisualStudio.gitignore for .NET (bin/, obj/, .vs/), Node.gitignore for JS (node_modules/, dist/, .next/), plus .env and *.log. git stash shelves your working-tree changes so you can switch branches without committing half-done work: git stash push -m "wip: refactor" then git stash pop to bring them back. Stashes live in a stack; git stash list shows them; never rely on stash for important work — it is not pushed anywhere.

Good enough to ship
  • • Branch, commit, push, open PR, resolve simple conflicts
  • git add -p to stage hunks interactively
  • git fetch + git rebase origin/main before pushing
  • • Useful .gitignore per stack
Expert tier
  • git rebase -i HEAD~5 to squash/reword/reorder before opening a PR
  • git reflog recovery from a hard reset
  • git bisect to find the commit that introduced a bug
  • git worktree add to keep two branches checked out at once
# The four commands that cover 80% of daily Git
git status                      # what's different across the four trees?
git add -p                      # stage hunks interactively
git commit -m "fix: foo"        # promote index → HEAD
git push                        # promote HEAD → origin

# The next 15% — fetch + rebase + reflog
git fetch origin
git rebase origin/main          # replay my commits on top of the latest main
git push --force-with-lease     # safe force-push after a rebase

# Recovery
git reflog                      # every HEAD movement, last ~90 days
git reset --hard HEAD@{2}       # go back 2 HEAD moves

VS Code — command palette, workspace settings, extensions, integrated terminal, debugger, source-control panel

VS Code is not a fancy text editor; it is a full IDE with a built-in debugger, source-control panel, integrated terminal, and an extension marketplace that lets you add C#, TypeScript, Tailwind, Docker, and remote SSH support in a single click. The trick to fluency is to stop using the mouse for things the keyboard does faster: Ctrl+Shift+P opens the command palette, and every action — built-in or from any extension — is reachable from there. Learn the palette first; the shortcuts come later.

Settings live in three layers: user settings (%APPDATA%/Code/User/settings.json), workspace settings (.vscode/settings.json checked into the repo), and folder settings (multi-root workspaces). Workspace settings are how you enforce a consistent formatter, linter, and TypeScript SDK across the whole team — commit them. The same applies to .vscode/extensions.json (recommended extensions for this repo), .vscode/launch.json (debug configurations), and .vscode/tasks.json (build commands). A repo that bootstraps a new contributor's editor in one click is a sign of a healthy team.

The integrated terminal (Ctrl+`) supports multiple sessions and split panes — run dotnet watch in one, pnpm dev in another, git status in a third, all without leaving the editor. The default profile on Windows should be PowerShell 7 (pwsh). The debugger (F5) supports breakpoints, conditional breakpoints (right-click a breakpoint), log-points (a breakpoint that just logs to the debug console without stopping — invaluable in production-shaped code), watch expressions, the call stack panel, and the variables panel. The source-control panel (Ctrl+Shift+G) shows changes, stages hunks visually, opens the 3-way merge editor on conflicts, and surfaces inline blame if you enable git.blame.editorDecoration.enabled.

Good enough to ship
  • • Command palette over menus
  • • Format-on-save + ESLint/Prettier auto-fix
  • • F5 launches a single project under the debugger
  • • Stage and commit from the source-control panel
Expert tier
  • • Compound launch configs (API + SPA on one F5)
  • • Per-workspace .vscode/* committed to the repo
  • • Custom snippets and keybindings
  • • Remote-SSH, Dev Containers, GitHub Codespaces
// .vscode/settings.json — workspace-level, committed
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": { "source.fixAll": "explicit" },
  "files.autoSave": "onFocusChange",
  "git.autofetch": true,
  "git.confirmSync": false,
  "explorer.compactFolders": false,
  "workbench.editor.enablePreview": false,
  "terminal.integrated.defaultProfile.windows": "PowerShell",
  "typescript.tsdk": "node_modules/typescript/lib",
  "[csharp]": { "editor.defaultFormatter": "ms-dotnettools.csharp" }
}

Browser DevTools — Elements, Console, Sources, Network, Application, Performance

Open DevTools with F12 (or Ctrl+Shift+I). Six panels, each one a different lens on the running page:

  1. Elements — the live DOM tree, computed CSS, the box model, the accessibility tree, and event listeners on any node. Edit CSS in place to test changes without rebuilding. The "Force element state" toggle lets you simulate :hover/:focus/:active.
  2. Console — JavaScript REPL with autocomplete on the live page's globals. Filter by log level, source, or regex. Use $0 for "the currently selected DOM node" and $_ for "the last expression's result". Live expressions (the eye icon) re-evaluate every 250 ms — perfect for watching state without console.log spam.
  3. Sources — set breakpoints in source-mapped TypeScript or JSX, even though the browser is running compiled JavaScript. Conditional breakpoints, log-points, blackboxing of framework code, and the call stack with async stack traces. Local Overrides let you persist edits to fetched files across reloads.
  4. Network — every request with method, status, type, size, and a timing waterfall. Filter by type (XHR/Fetch/JS/CSS/Img/Doc). Throttle to "Fast 3G" or "Slow 3G" to test mobile performance. Right-click a request → "Copy as fetch" gives you a ready-to-run snippet for the console.
  5. Application — cookies (with HttpOnly, Secure, SameSite flags), localStorage, sessionStorage, IndexedDB, service workers, cache storage, and the manifest if it is a PWA. Clear site data from one button.
  6. Performance — record a flame chart of the main thread, layout/paint events, long tasks, and frame timing. The bottom-up view shows which functions dominated total CPU; the call tree shows the inheritance chain. Pair with Lighthouse for an end-to-end report.
Good enough to ship
  • • Read a Network waterfall, identify the slow request
  • • Set a breakpoint in source-mapped JS
  • • Inspect cookies and localStorage
  • • Throttle and reload to simulate mobile
Expert tier
  • • Record a Performance flame chart, spot long tasks
  • • Use the Coverage panel to find unused JS/CSS
  • • Override a remote file with Local Overrides
  • • Audit Core Web Vitals (LCP, INP, CLS) with Lighthouse
Stalled / Queued
Browser-side wait
Too many parallel reqs to same origin; HTTP/1.1 6-conn limit.
DNS / Connect / SSL
Setup costs
First request per origin; preconnect to mitigate.
Request sent
TX bytes
Long bar = huge POST body.
Waiting (TTFB)
Server thinking
Frontend cannot fix this — go look at the API.
Content Download
RX bytes
Compress, paginate, or split.
Total
End-to-end
Sum of the above. Sort by this to find worst offenders.
What each segment of a Network waterfall bar tells you.

Package managers — winget, npm vs pnpm vs yarn, dotnet CLI, NuGet, SemVer

Three ecosystems, three package managers — and you need all three. winget is Microsoft's OS-level package manager (the Windows equivalent of brew or apt); use it to install developer tools (winget install Microsoft.PowerShell, winget install Git.Git, winget install Microsoft.VisualStudioCode). winget export winget.json writes your installed apps to a manifest you can winget import on a new machine — reproducible dev-box setup in two commands.

For JavaScript, the three options are npm (ships with Node), pnpm (a content-addressed store with strict isolation), and yarn (now mostly Yarn 3+ "Berry" with Plug'n'Play). The community in 2026 has largely converged on pnpm for new projects: it stores every package version once globally and hard-links into each project's node_modules, making installs 2–3× faster and consuming 5–10× less disk space than npm. It is also stricter — a package can only import what it explicitly depends on, eliminating "phantom dependencies" that work locally and break in CI. Use pnpm add <pkg>, pnpm install --frozen-lockfile in CI, and check in pnpm-lock.yaml.

For .NET, the dotnet CLI is the entry point: dotnet new <template> scaffolds a project, dotnet restore resolves NuGet packages, dotnet build compiles, dotnet run runs, dotnet test runs the test suite, dotnet publish -c Release produces a deployment artifact. Solution files (.sln) group multiple projects; dotnet sln add Api/Api.csproj keeps the solution in sync. NuGet is the package registry; package sources live in nuget.config (per-repo or per-user); dotnet add package Polly installs the latest stable. Central package management via a Directory.Packages.props file at the repo root lets you pin versions once across every project — your single source of truth for the dependency tree (covered in detail in Chapter 4).

Semantic versioning (SemVer) is MAJOR.MINOR.PATCH. MAJOR = breaking changes, MINOR = backwards-compatible additions, PATCH = backwards-compatible bug fixes. In package.json, ^1.2.3 means >=1.2.3 <2.0.0 (any compatible MINOR or PATCH), ~1.2.3 means >=1.2.3 <1.3.0 (PATCH only), and 1.2.3 is exact. Library authors violate SemVer more often than they should — that is why the lockfile is the real source of truth. The ranges in package.json describe what you would accept on a fresh install; the lockfile records what you actually resolved. In CI use pnpm install --frozen-lockfile (or npm ci) to fail loudly if package.json and the lockfile disagree.

Good enough to ship
  • winget install for OS tools, export/import for new machines
  • pnpm add / pnpm install --frozen-lockfile
  • dotnet new, restore, build, run, test, publish
  • • Know what ^, ~, and pinned versions mean in package.json
Expert tier
  • • Central package management with Directory.Packages.props
  • • Private NuGet feeds in nuget.config with credential provider
  • • pnpm workspaces (monorepo) and pnpm-workspace.yaml
  • • Renovate or Dependabot policies for safe upgrade cadence
// What the caret actually means
{
  "dependencies": {
    "react":      "^19.0.0",   // >= 19.0.0, < 20.0.0
    "react-dom":  "~19.0.0",   // >= 19.0.0, < 19.1.0
    "lodash":     "4.17.21"    // exactly this version
  }
}

Worked examples

1. A dev-status.ps1 that walks every repo in a folder

This is the kind of script you write once and use forever: it scans every subdirectory looking for a .git folder, prints the branch, ahead/behind counts, and the uncommitted file count.

#requires -Version 7
[CmdletBinding()]
param(
  [string]$Root = (Get-Location)
)

$ErrorActionPreference = 'Stop'

Get-ChildItem -Path $Root -Directory | ForEach-Object {
  $repo = $_.FullName
  $git  = Join-Path $repo '.git'
  if (-not (Test-Path $git)) { return }

  Push-Location $repo
  try {
    $branch = git rev-parse --abbrev-ref HEAD 2>$null
    git fetch --quiet 2>$null

    $ahead  = (git rev-list --count "@{u}..HEAD" 2>$null) -as [int]
    $behind = (git rev-list --count "HEAD..@{u}" 2>$null) -as [int]
    $dirty  = (git status --porcelain | Measure-Object).Count

    [pscustomobject]@{
      Repo   = $_.Name
      Branch = $branch
      Ahead  = $ahead
      Behind = $behind
      Dirty  = $dirty
    }
  }
  finally {
    Pop-Location
  }
} | Format-Table -AutoSize

What to notice:

  • #requires -Version 7 fails fast if someone runs it under Windows PowerShell 5.1.
  • [CmdletBinding()] and param(...) turn this into a real cmdlet — you get -Verbose, -WhatIf, tab-completion on parameters, all free.
  • $ErrorActionPreference = 'Stop' makes non-terminating errors throw, so try/catch actually works.
  • Building [pscustomobject]@{...} and piping into Format-Table is the PowerShell idiom for tabular output.
  • git rev-list --count "@{u}..HEAD" is the canonical way to count commits ahead/behind the upstream branch.

2. A .vscode/launch.json that launches API + SPA together

One F5 starts both the .NET API under the debugger and the Vite dev server with Chrome attached, so breakpoints fire on both sides of the wire.

{
  "version": "0.2.0",
  "compounds": [
    {
      "name": "Stack: API + SPA",
      "configurations": ["API (.NET)", "SPA (Vite)"]
    }
  ],
  "configurations": [
    {
      "name": "API (.NET)",
      "type": "coreclr",
      "request": "launch",
      "program": "${workspaceFolder}/src/Api/bin/Debug/net8.0/Api.dll",
      "cwd": "${workspaceFolder}/src/Api",
      "env": { "ASPNETCORE_ENVIRONMENT": "Development" },
      "preLaunchTask": "build-api",
      "serverReadyAction": {
        "pattern": "Now listening on: https?://\\S+:([0-9]+)",
        "uriFormat": "https://localhost:%s/swagger",
        "action": "openExternally"
      }
    },
    {
      "name": "SPA (Vite)",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}/src/Spa",
      "preLaunchTask": "spa-dev"
    }
  ]
}

What to notice:

  • compounds is the magic word — it lets one F5 fire multiple configurations.
  • preLaunchTask runs a task from tasks.json (e.g., dotnet build or pnpm dev).
  • serverReadyAction opens the Swagger UI once the API prints "Now listening on:" — no more tab-juggling.
  • type: "chrome" requires the JavaScript Debugger extension (bundled in recent VS Code).
  • webRoot tells the debugger where source maps resolve relative paths from.

3. A reproducible dev-box bootstrap with winget

Two scripts: one exports your current setup, one restores it on a fresh Windows install.

# Export your current machine
winget export -o $HOME\winget.json `
  --include-versions --accept-source-agreements

# On the new machine
winget import -i $HOME\winget.json `
  --accept-package-agreements --accept-source-agreements

# Bonus: bootstrap the shell profile
@'
Import-Module posh-git
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
function gpfwl { git push --force-with-lease $args }
function gst   { git status -sb }
'@ | Set-Content -Path $PROFILE -Force

What to notice:

  • winget export writes a JSON manifest of every installed package; commit it to your dotfiles repo.
  • --accept-*-agreements skips the interactive prompts so this can run unattended.
  • The $PROFILE block creates a profile if one does not exist; Set-Content -Force overwrites without prompting.
  • gpfwl is a five-character alias for the safe force-push — you will use it after every rebase.
  • Pair this with VS Code's "Settings Sync" (logged into your GitHub account) and a new machine is productive in 10 minutes.

4. A .gitignore that covers both .NET and Node in one repo

A mixed full-stack repo needs both halves. The wrong order can accidentally untrack dist/ builds or leave bin/ cluttering PRs.

# ----- OS -----
.DS_Store
Thumbs.db

# ----- .NET -----
bin/
obj/
*.user
*.suo
.vs/
TestResults/
coverage.cobertura.xml

# ----- Node / pnpm -----
node_modules/
dist/
.next/
.turbo/
.cache/
*.log

# ----- Secrets -----
.env
.env.local
appsettings.Development.local.json

# ----- IDE -----
.idea/
*.swp

What to notice:

  • Group by domain with comments so the file stays readable as it grows.
  • .env* patterns sit in a "secrets" section so reviewers immediately notice if they were touched.
  • appsettings.Development.local.json is the ASP.NET Core convention for per-developer overrides — keep it untracked.
  • Use git check-ignore -v <path> to debug why a file is or is not ignored; it prints the matching rule.
  • Cherry-pick from github.com/github/gitignore for any extra languages.

Hands-on exercises

  1. Build dev-status.ps1. Goal: a script that walks every Git repo under ~/code and prints branch + ahead/behind + dirty count as a table.

    1. Create ~/code/scripts/dev-status.ps1 from the worked example above.
    2. Run it across at least 5 repos and confirm the table is correctly formatted.
    3. Add a -Stale switch that filters to repos whose last commit is older than 30 days (git log -1 --format=%ct).
    4. Add it to $PROFILE as a function alias: function ds { dev-status.ps1 $args }.
    5. Done when: typing ds in any shell prints the table; ds -Stale shows only stale repos.
  2. Rewrite history with rebase -i. Goal: clean up a feature branch before opening a PR.

    1. On a scratch branch, make 5 small commits with bad messages ("wip", "fix", "asdf", etc.).
    2. Run git rebase -i HEAD~5, change pick to squash on commits 2-5, write a clean message.
    3. Force-push with git push --force-with-lease origin <branch>.
    4. Use git reflog to find the SHA before the rebase, then git reset --hard <sha> to undo.
    5. Done when: you can squash 5 commits into 1 without consulting docs, and you can recover from a wrong rebase via reflog.
  3. DevTools Network deep-dive. Goal: explain the bottleneck on a slow page.

    1. Open a real website you use daily, F12 → Network, hard-reload (Ctrl+Shift+R).
    2. Sort by Time (descending), pick the slowest XHR/Fetch request.
    3. Hover its bar — write 1 sentence per timing segment (Queued, Stalled, DNS, Connect, SSL, Send, Wait/TTFB, Receive).
    4. Throttle to "Fast 3G", reload, repeat. Which segments grew?
    5. Done when: you can tell whether the bottleneck is the network, the server, or the response size.
  4. Compound launch in VS Code. Goal: F5 starts API + SPA together.

    1. In a small full-stack repo, write .vscode/tasks.json with build-api and spa-dev tasks.
    2. Write .vscode/launch.json with a compound from the worked example.
    3. Press F5; confirm a .NET breakpoint and a JS breakpoint both hit.
    4. Add serverReadyAction so Swagger opens automatically when the API is ready.
    5. Done when: one F5 brings up both processes, both debuggers attach, both breakpoints fire.
  5. pnpm migration. Goal: switch a small repo from npm to pnpm.

    1. Pick a repo with package-lock.json, run du -sh node_modules and record the size.
    2. rm -rf node_modules package-lock.json, pnpm install, record the new size.
    3. Add "packageManager": "pnpm@9.0.0" to package.json so Corepack pins the version.
    4. Update CI to use pnpm install --frozen-lockfile.
    5. Done when: install time drops at least 2× and disk usage drops at least 3×.
  6. winget export/import. Goal: reproducible Windows dev-box setup.

    1. Run winget list and pick 10 packages you actually use.
    2. Run winget export -o ~/winget.json --include-versions.
    3. On a clean VM or a coworker's machine, run winget import -i ~/winget.json.
    4. Commit the manifest to your dotfiles repo.
    5. Done when: a fresh Windows install reaches your full toolset in one command.

Self-check questions

  1. Explain the four locations Git moves files between, and which command moves each direction.
  2. What's the difference between git fetch and git pull, and which do you reach for in a team?
  3. When would you merge and when would you rebase? What's the cardinal rule about rebasing?
  4. What does ^1.2.3 vs ~1.2.3 vs 1.2.3 mean in package.json? Which one is the lockfile?
  5. In DevTools → Network, what does a long "Waiting (TTFB)" bar tell you about the bottleneck? What about a long "Stalled"?
  6. What is PowerShell's execution policy, why does it default to restrictive, and how should a developer set it?
  7. What does git reflog give you that git log doesn't, and what is its 90-day window?
  8. What is --force-with-lease and why is it safer than --force?
  9. What changes when you switch a project from npm to pnpm — install speed, disk usage, isolation guarantees?
  10. What is a VS Code compound launch configuration, and when do you reach for one?
  11. What is the difference between a workspace setting and a user setting in VS Code, and which one belongs in source control?
  12. What does winget export produce, and how does it differ from a Chocolatey or scoop manifest?

High-signal resources

Official docs

Books or courses

  • The Pragmatic Programmer (20th-anniversary edition) — Hunt & Thomas. Chapters 3 + 4 on "Basic Tools" and "Power Editing" are timeless.
  • Learn PowerShell in a Month of Lunches — Don Jones & Jeffery Hicks. Skim the parts you already know.

Practitioner posts

Weekly milestones

  1. Day 1 — Install PowerShell 7, Git, VS Code, winget if missing. Set pull.rebase=true, push.default=current, RemoteSigned execution policy. Read Pro Git ch 1–2.
  2. Day 2 — Three hours on Git: clone a repo, branch, commit, push, open a PR, resolve a 2-file conflict in VS Code's merge editor. Read Pro Git ch 3. Answer self-check questions 1-3.
  3. Day 3 — PowerShell: write dev-status.ps1 (exercise 1). Customise $PROFILE with posh-git and PSReadLine prediction. Answer self-check questions 6.
  4. Day 4-5 — VS Code: build a compound launch (exercise 4) and a full .vscode/ for a real repo. DevTools: do exercise 3 on three different sites. Read the Chrome DevTools docs end-to-end.
  5. Day 6-7 — Package managers: migrate one project to pnpm (exercise 5). Build a winget export (exercise 6). Run a git rebase -i cleanup (exercise 2). Re-read this chapter; answer every self-check question without looking back.

How it shows up in the capstone

The capstone is an analytics dashboard with a .NET API and a React SPA, deployed via GitHub Actions. Every chapter feeds it. From this chapter you will land four artifacts: a committed .vscode/launch.json that runs API + SPA on one F5, a shared .editorconfig that keeps .NET and JS files aligned, a .gitignore that covers both stacks, and a PROFILE.ps1 snippet in the repo's docs/ folder that adds a start-dev alias running dotnet watch + pnpm dev in two terminal panes.

You will use PowerShell to bootstrap CI scripts (database seeding, log rotation, smoke tests) and winget for the developer onboarding doc. The Git flow you settle on here — fetch + rebase + push --force-with-lease — is the one you will use for every PR on the capstone. The DevTools muscle memory will let you debug both the React SPA and the API responses by reading the Network waterfall the moment something feels slow.

When you onboard a second contributor and they are productive in 30 minutes — clone, winget import, open in VS Code, hit F5, watch the dashboard load — you will know this chapter paid off.

Next chapter → Ch 2 — Web fundamentals