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.
- • Variables, pipelines,
Where-Object,Select-Object,ForEach-Object - • Read JSON with
ConvertFrom-Json, write back withConvertTo-Json -Depth 10 - •
$LASTEXITCODEafter every external command in a script - • Set execution policy once:
RemoteSigned -Scope CurrentUser
- • Write advanced functions with
[CmdletBinding()]andparam([Parameter(Mandatory)]...) - •
Get-Memberreflexively to inspect any pipeline stage - •
$PROFILEcurated with aliases, prompt,Import-Modulefor posh-git, PSReadLine - •
try/catchwith$ErrorActionPreference = 'Stop'and-ErrorAction Stopon 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/gitignore — VisualStudio.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.
- • Branch, commit, push, open PR, resolve simple conflicts
- •
git add -pto stage hunks interactively - •
git fetch+git rebase origin/mainbefore pushing - • Useful
.gitignoreper stack
- •
git rebase -i HEAD~5to squash/reword/reorder before opening a PR - •
git reflogrecovery from a hard reset - •
git bisectto find the commit that introduced a bug - •
git worktree addto 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.
- • 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
- • 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:
- 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. - Console — JavaScript REPL with autocomplete on the live page's globals. Filter by log level, source, or regex. Use
$0for "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 withoutconsole.logspam. - 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.
- 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.
- Application — cookies (with
HttpOnly,Secure,SameSiteflags), localStorage, sessionStorage, IndexedDB, service workers, cache storage, and the manifest if it is a PWA. Clear site data from one button. - 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.
- • 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
- • 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
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.
- •
winget installfor 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
- • Central package management with
Directory.Packages.props - • Private NuGet feeds in
nuget.configwith 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 7fails fast if someone runs it under Windows PowerShell 5.1.[CmdletBinding()]andparam(...)turn this into a real cmdlet — you get-Verbose,-WhatIf, tab-completion on parameters, all free.$ErrorActionPreference = 'Stop'makes non-terminating errors throw, sotry/catchactually works.- Building
[pscustomobject]@{...}and piping intoFormat-Tableis 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:
compoundsis the magic word — it lets one F5 fire multiple configurations.preLaunchTaskruns a task fromtasks.json(e.g.,dotnet buildorpnpm dev).serverReadyActionopens 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).webRoottells 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 exportwrites a JSON manifest of every installed package; commit it to your dotfiles repo.--accept-*-agreementsskips the interactive prompts so this can run unattended.- The
$PROFILEblock creates a profile if one does not exist;Set-Content -Forceoverwrites without prompting. gpfwlis 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.jsonis 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
-
Build
dev-status.ps1. Goal: a script that walks every Git repo under~/codeand prints branch + ahead/behind + dirty count as a table.- Create
~/code/scripts/dev-status.ps1from the worked example above. - Run it across at least 5 repos and confirm the table is correctly formatted.
- Add a
-Staleswitch that filters to repos whose last commit is older than 30 days (git log -1 --format=%ct). - Add it to
$PROFILEas a function alias:function ds { dev-status.ps1 $args }. - Done when: typing
dsin any shell prints the table;ds -Staleshows only stale repos.
- Create
-
Rewrite history with
rebase -i. Goal: clean up a feature branch before opening a PR.- On a scratch branch, make 5 small commits with bad messages ("wip", "fix", "asdf", etc.).
- Run
git rebase -i HEAD~5, changepicktosquashon commits 2-5, write a clean message. - Force-push with
git push --force-with-lease origin <branch>. - Use
git reflogto find the SHA before the rebase, thengit reset --hard <sha>to undo. - Done when: you can squash 5 commits into 1 without consulting docs, and you can recover from a wrong rebase via reflog.
-
DevTools Network deep-dive. Goal: explain the bottleneck on a slow page.
- Open a real website you use daily, F12 → Network, hard-reload (
Ctrl+Shift+R). - Sort by Time (descending), pick the slowest XHR/Fetch request.
- Hover its bar — write 1 sentence per timing segment (Queued, Stalled, DNS, Connect, SSL, Send, Wait/TTFB, Receive).
- Throttle to "Fast 3G", reload, repeat. Which segments grew?
- Done when: you can tell whether the bottleneck is the network, the server, or the response size.
- Open a real website you use daily, F12 → Network, hard-reload (
-
Compound launch in VS Code. Goal: F5 starts API + SPA together.
- In a small full-stack repo, write
.vscode/tasks.jsonwithbuild-apiandspa-devtasks. - Write
.vscode/launch.jsonwith a compound from the worked example. - Press F5; confirm a .NET breakpoint and a JS breakpoint both hit.
- Add
serverReadyActionso Swagger opens automatically when the API is ready. - Done when: one F5 brings up both processes, both debuggers attach, both breakpoints fire.
- In a small full-stack repo, write
-
pnpm migration. Goal: switch a small repo from npm to pnpm.
- Pick a repo with
package-lock.json, rundu -sh node_modulesand record the size. rm -rf node_modules package-lock.json,pnpm install, record the new size.- Add
"packageManager": "pnpm@9.0.0"topackage.jsonso Corepack pins the version. - Update CI to use
pnpm install --frozen-lockfile. - Done when: install time drops at least 2× and disk usage drops at least 3×.
- Pick a repo with
-
winget export/import. Goal: reproducible Windows dev-box setup.
- Run
winget listand pick 10 packages you actually use. - Run
winget export -o ~/winget.json --include-versions. - On a clean VM or a coworker's machine, run
winget import -i ~/winget.json. - Commit the manifest to your dotfiles repo.
- Done when: a fresh Windows install reaches your full toolset in one command.
- Run
Self-check questions
- Explain the four locations Git moves files between, and which command moves each direction.
- What's the difference between
git fetchandgit pull, and which do you reach for in a team? - When would you
mergeand when would yourebase? What's the cardinal rule about rebasing? - What does
^1.2.3vs~1.2.3vs1.2.3mean inpackage.json? Which one is the lockfile? - In DevTools → Network, what does a long "Waiting (TTFB)" bar tell you about the bottleneck? What about a long "Stalled"?
- What is PowerShell's execution policy, why does it default to restrictive, and how should a developer set it?
- What does
git refloggive you thatgit logdoesn't, and what is its 90-day window? - What is
--force-with-leaseand why is it safer than--force? - What changes when you switch a project from npm to pnpm — install speed, disk usage, isolation guarantees?
- What is a VS Code compound launch configuration, and when do you reach for one?
- What is the difference between a workspace setting and a user setting in VS Code, and which one belongs in source control?
- What does
winget exportproduce, and how does it differ from a Chocolatey or scoop manifest?
High-signal resources
Official docs
- Pro Git book (free, online) — read chapters 1–3 + 7. The single best Git resource ever written.
- PowerShell official docs — start with "Learn PowerShell in a Month of Lunches".
- VS Code docs — the "Getting Started" and "Debugger" sections are essential.
- Chrome DevTools docs — every section is worth a read at least once.
- winget documentation — manifests, sources, and the export/import flow.
- pnpm motivation — why it exists; once you read it, you will stop reaching for npm.
- NuGet documentation — sources,
nuget.config, central package management.
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
- Think Like (a) Git — the best mental-model explainer for branches and refs.
- So you think you know Git — FOSDEM talk by Scott Chacon — 40 minutes, you will learn at least 3 things.
- Julia Evans' Git zines and blog posts — short, deep, illustrated.
- Addy Osmani's "Visualizing the DevTools Network Panel" — exactly what each bar means.
Weekly milestones
- Day 1 — Install PowerShell 7, Git, VS Code, winget if missing. Set
pull.rebase=true,push.default=current,RemoteSignedexecution policy. Read Pro Git ch 1–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.
- Day 3 — PowerShell: write
dev-status.ps1(exercise 1). Customise$PROFILEwithposh-gitand PSReadLine prediction. Answer self-check questions 6. - 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. - Day 6-7 — Package managers: migrate one project to pnpm (exercise 5). Build a
winget export(exercise 6). Run agit rebase -icleanup (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