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.
Published Updated
Lesson preparation & details
Level: beginner
By the end, you should be able to
- A repo has no upstream; another has a dirty file. What should a read-only status script report, and should it fetch?
- Explain the version and execution boundaries before applying the examples
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 27 sections
Review and execution boundary
PowerShell 7.4; Git documentation snapshot, not a Windows bootstrap reproduction.
Reviewed on 7 October 2026 against the official source snapshots linked below. The review is bounded editorial correction, not certification of every dependency, security property or cloud deployment. Historical setup commands and optional exercises were not executed. No cloud resources, third-party packages or external side effects were created. Old screenshots and unavailable private assets remain in the private recovery archive, not prerequisites for this lesson.
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. Tooling fluency reduces avoidable friction: finding a changed file, inspecting a request and reproducing an install should be deliberate operations. Seniority also involves design judgment, collaboration and domain knowledge; command memorization is not a measure of those skills.
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 explain and reproduce a trusted repo’s setup, inspect changes without losing work, debug a request across API and SPA, and prepare a clean PR. Consult the versioned documentation; successful recall is not a substitute for checking a destructive command.
PowerShell as a daily driver — Stop using cmd. Pipelines pass objects, not strings.
Git's three trees — Working tree / index / HEAD snapshot; remote-tracking refs are a separate local view of another repository.
VS Code as IDE — Command palette, integrated terminal, debugger, source-control panel.
DevTools fluency — Six panels, six superpowers. Network waterfall + Performance flame chart.
Package managers — winget / pnpm / dotnet — and what a caret in package.json actually means.
Reproducible setups — winget export, pnpm-lock.yaml, Directory.Packages.props in CI.
Concepts and depth
PowerShell — variables, pipelines, $env:, cmdlets, scripts, execution policy
PowerShell is not bash with a different syntax. It is an object pipeline: cmdlets can pass .NET objects downstream instead of formatted display text. Native executables such as Git still have text output and exit codes. Filesystem Get-ChildItem returns FileInfo and DirectoryInfo objects; 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. Writing changes the current process environment and what newly launched child processes inherit, not already running terminals. For an intentional persistent user change on Windows 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. Inspect Get-ExecutionPolicy -List and the effective Get-ExecutionPolicy before changing anything. Windows policy depends on scopes, edition and organizational Group Policy; “Windows always defaults to Restricted” is not a portable rule. RemoteSigned permits local scripts and normally requires a trusted signature for internet-marked scripts. It does not certify script safety, and execution policy is not a security boundary. On a personally administered machine, an approved Set-ExecutionPolicy RemoteSigned -Scope CurrentUser changes that scope only; it does not override Group Policy. Review the script and respect the machine’s policy rather than bypassing it.
Good enough to ship
- 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- Inspect effective execution policy and follow the device owner’s approved setting
Expert tier
- Write advanced functions with
[CmdletBinding()]andparam([Parameter(Mandatory)]...) Get-Memberreflexively to inspect any pipeline stage$PROFILEcurated with aliases, prompt,Import-Modulefor posh-git, PSReadLinetry/catchwith$ErrorActionPreference = 'Stop'and-ErrorAction Stopon cmdlets
# Counterexample: parsing display columns loses filenames containing spaces.
ls -la | awk '{print $5, $9}' | sort -nr | head -5# PowerShell: real objects, real properties, no parsing
Get-ChildItem -File |
Sort-Object Length -Descending |
Select-Object -First 5 Name, Length, LastWriteTimeThe 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 }).
title: Use PowerShell 7, not Windows PowerShell 5.1
Windows PowerShell 5.1 ships with Windows and is in maintenance mode. PowerShell 7+ (formerly PowerShell Core) is cross-platform and has a separate release lifecycle; check module compatibility rather than assuming every script is faster. Install via winget install Microsoft.PowerShell and use pwsh from then on. Both can coexist on the same machine.
Git — working tree, index, commits, branches, remotes, merge vs rebase, conflicts, tags, .gitignore, stash
Git’s three-tree model compares the working tree (editable files), index (proposed next snapshot), and HEAD’s tree (the current commit snapshot). git add copies selected content into the index; git commit creates a commit from the index and advances the current branch. git status reports staged, unstaged and untracked changes, plus available branch tracking information. It does not contact a remote to discover its current state.
A branch is a moving reference to a commit, not a directory. A commit records a tree, metadata and zero or more parents; its object ID uses the repository’s hash format. HEAD usually refers to the checked-out branch, or directly to a commit when detached. origin is a configured remote repository; origin/main is a local remote-tracking reference. git fetch updates objects and configured refs without integrating changes into the working branch. git pull fetches and then integrates according to options/configuration (fast-forward, merge or rebase). git push transfers objects and requests remote ref updates; it does not copy unstaged files.
A non-fast-forward merge normally creates a commit with both histories as parents; a fast-forward can simply advance the branch. A rebase replays commits on a new base, creating new IDs for rewritten commits. Rebase an unshared feature branch if that fits the team’s policy; use merge or coordinated integration for shared history. Never treat rewriting a shared branch as the default synchronization step. Inspect a clean working tree, the intended base and branch ownership before either operation.
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 gives patterns for intentionally untracked paths; it does not stop tracking a file already in the index or remove a leaked secret from history. 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 tracked changes by default; untracked files require -u and ignored files require -a. Inspect what will be included before switching branches: 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
• Inspect git fetch results and use the team’s agreed merge/rebase policy
• 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
# Common daily Git operations — run intentionally, not as one script
git status # staged, unstaged, untracked; cached upstream information
git add -p # stage hunks interactively
git commit -m "fix: foo" # promote index → HEAD
git push # promote HEAD → origin
# Optional unshared feature-branch rebase; not a shared-branch default
git fetch origin
git rebase origin/main # replay my commits on top of the latest main
# A history-rewriting push, if needed, requires reviewed branch ownership and lease.
# Recovery
git reflog # recorded local ref movements; retention is configurable
git show "HEAD@{2}" # inspect an earlier reflog position without destroying worktitle: A lease is a guard, not permission to rewrite
A force-push can remove a collaborator’s commits from the remote branch tip. --force-with-lease adds an expected-tip check, but the shorthand normally uses a local remote-tracking ref. Background fetch can update that ref without you reviewing the new commits. For an explicitly coordinated rewrite, record the exact reviewed remote tip and use the explicit --force-with-lease=refs/heads/<branch>:<expected-object-id> form with a destination refspec. If it refuses, inspect the new history; do not refresh and retry blindly. This is not a recommended alias for every push.
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 recommend 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": false,
"git.confirmSync": true,
"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 re-evaluate periodically; avoid expressions with side effects and do not depend on a fixed sampling interval. - 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). Select an available network-throttling preset or create one with explicit bandwidth and latency; this simulates a network, not an entire mobile device. 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 nested call relationships, not class inheritance. 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 • Inspect LCP/CLS in lab traces; use interaction traces and field data for INP rather than claiming a default Lighthouse navigation measures real-user INP
To put the Network panel to work, select one slow request and open its Timing breakdown. The cards below break down that one request and its total, not six separate requests: distinguish time spent waiting for a connection, waiting for the first response byte, and downloading the body before choosing your next tool. A long TTFB includes network round-trip time as well as server processing, so correlate it with API telemetry rather than treating the bar alone as proof of a slow handler.
Stalled / Queued Browser-side wait Priority, cache allocation or connection limits; the six-connection limit applies to HTTP/1.x, not every protocol.
DNS / Connect / SSL Setup costs Name resolution and connection setup when needed; reuse can avoid these phases. Preconnect only to justified origins.
Request sent TX bytes A long send phase can reflect a large body or slow upload; compare byte size and bandwidth before diagnosing.
Waiting (TTFB) First-byte wait Network travel plus server work — correlate with API traces.
Content Download RX bytes Inspect response size, network rate and main-thread contention before choosing compression, pagination or splitting.
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
These ecosystems have different package managers; use the ones your operating system and repository require. 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 -o winget.json --include-versions writes matched package identities and versions for winget import -i winget.json. Unmatched applications produce warnings, and unavailable versions can prevent restoration. It is a package list, not an image of settings, SDK workloads or credentials.
For JavaScript, the three options are npm (ships with Node), pnpm (a content-addressed store with configurable hoisting), and Yarn (with version-dependent linker choices, including Plug'n'Play). This lesson uses pnpm as one package-manager option: it stores every package version once globally and hard-links into each project's node_modules, which can reduce repeated package storage; benchmark install time and disk use for the actual filesystem and cache state. Its symlinked layout makes root dependencies more explicit, but hoisting settings and compatibility exceptions matter: pnpm documents default private hoisting under node_modules/.pnpm/node_modules. Do not claim that undeclared imports are impossible; verify dependency declarations and test in a clean install. 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); for the .NET 8 SDK syntax used here, dotnet add package Polly --version <reviewed-version> records an explicitly reviewed compatible version; do not make unpinned latest resolution part of a reproducibility claim. Central package management via a Directory.Packages.props file at the repo root lets you pin versions once across every project — a shared declaration of direct package versions, not by itself a lock of the full transitive graph (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 PowerShell 7.4 reference scans immediate child directories of a trusted folder, accepting .git directories and worktree pointer files. It reports local state only: no fetch, no ref rewrite and no optional index refresh. Dirty counts porcelain status entries (a rename is one entry), not changed lines; missing upstream is null, not zero. Inspect repository configuration before running tools in an untrusted checkout.
#requires -Version 7.4
[CmdletBinding()]
param([string]$Root = (Get-Location).Path, [switch]$Stale)
$ErrorActionPreference = 'Stop'
$cutoff = [DateTimeOffset]::UtcNow.AddDays(-30).ToUnixTimeSeconds()
function Read-Git([string]$Repo, [string[]]$GitArgs) {
# Expected nonzero statuses are data; callers check each native exit code.
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
$lines = @(& git --no-optional-locks -C $Repo @GitArgs 2>$null)
$code = $LASTEXITCODE
[pscustomobject]@{ ExitCode = $code; Lines = $lines }
}
$rows = foreach ($dir in Get-ChildItem -LiteralPath $Root -Directory) {
$repo = $dir.FullName
if (-not (Test-Path -LiteralPath (Join-Path $repo '.git'))) { continue }
try {
$status = Read-Git $repo @('status', '--porcelain=v1', '--untracked-files=all')
if ($status.ExitCode -ne 0) { throw 'git status failed' }
$branch = Read-Git $repo @('symbolic-ref', '--quiet', '--short', 'HEAD')
if ($branch.ExitCode -gt 1) { throw 'branch inspection failed' }
$name = if ($branch.ExitCode -eq 0) { $branch.Lines[0] } else { '(detached)' }
$head = Read-Git $repo @('rev-parse', '--verify', 'HEAD')
$ahead = $null; $behind = $null; $last = $null; $upstream = $null
$state = 'unborn-or-unresolvable-HEAD'
if ($head.ExitCode -eq 0) {
$date = Read-Git $repo @('log', '-1', '--format=%ct')
if ($date.ExitCode -ne 0) { throw 'commit date inspection failed' }
$last = [long]$date.Lines[0]
$state = 'no-upstream'
if ($branch.ExitCode -eq 0) {
$ref = Read-Git $repo @('for-each-ref', '--format=%(upstream)', "refs/heads/$name")
if ($ref.ExitCode -ne 0) { throw 'upstream inspection failed' }
$upstream = ($ref.Lines -join '').Trim()
if ($upstream) {
$counts = Read-Git $repo @('rev-list', '--left-right', '--count', "HEAD...$upstream")
if ($counts.ExitCode -eq 0) {
$parts = ($counts.Lines -join '').Trim() -split '\s+'
if ($parts.Count -ne 2) { throw 'unexpected count output' }
$ahead = [int]$parts[0]; $behind = [int]$parts[1]
$state = 'local-upstream-snapshot'
} else { $state = 'upstream-unavailable' }
}
}
}
if ($Stale -and ($null -eq $last -or $last -ge $cutoff)) { continue }
[pscustomobject]@{
Repo=$dir.Name; Branch=$name; Ahead=$ahead; Behind=$behind
Dirty=$status.Lines.Count; State=$state; Error=$null
}
} catch {
[pscustomobject]@{
Repo=$dir.Name; Branch=$null; Ahead=$null; Behind=$null
Dirty=$null; State='error'; Error=$_.Exception.Message
}
}
}
$rows | Format-Table -AutoSizeWhat to notice:
#requires -Version 7.4declares the reference version. Native Git commands were checked in disposable local repositories; this PowerShell wrapper was source-reviewed but not executed in this review environment.[CmdletBinding()]andparam(...)turn this into a real cmdlet — you get common parameters such as-Verbose;-WhatIfrequires SupportsShouldProcess and guarded operations.- Cmdlet errors can be made terminating with
$ErrorActionPreference = 'Stop'; native Git failures are checked explicitly. Errors produce an error row rather than a plausible numeric zero. - Building
[pscustomobject]@{...}and piping intoFormat-Tableis the PowerShell idiom for tabular output. HEAD...upstreamwith--left-right --countreports commits unique to HEAD first (ahead), then to the upstream (behind). No network check is implied.-Staleselects known commit times strictly older than 30 days; unborn/unknown times are not silently treated as old.
2. A .vscode/launch.json that launches API + SPA together
The compound starts both debug configurations. It still needs the named tasks, the C# debugger, a matching built net8.0 application, installed SPA dependencies and source maps. This is an integration reference, not a supplied application or a proven one-click setup. Start Vite in a separate terminal with pnpm exec vite --host 127.0.0.1 --port 5173 --strictPort for the minimal route; the browser debugger below does not start that server.
{
"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", "ASPNETCORE_URLS": "http://127.0.0.1:5000" },
"preLaunchTask": "build-api",
"serverReadyAction": {
"pattern": "Now listening on: (https?://\\S+)",
"uriFormat": "%s",
"action": "openExternally"
}
},
{
"name": "SPA (Vite)",
"type": "chrome",
"request": "launch",
"url": "http://127.0.0.1:5173",
"webRoot": "${workspaceFolder}/src/Spa"
}
]
}What to notice:
compoundsis the magic word — it lets one F5 fire multiple configurations.build-apimust be a finite task intasks.jsonthat buildssrc/Api/Api.csproj; configure the API’s listener on loopback. For automatic SPA startup, add aspa-devbackground task and a readiness problem matcher whose end pattern matches the actual Vite ready message, then reference it aspreLaunchTask. A long-running command without readiness signaling can leave F5 waiting forever.serverReadyActionpreserves the HTTP/HTTPS scheme and full URL actually logged by the API. It opens the root, not an assumed/swaggerendpoint; OpenAPI UI and certificates require separate app configuration.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
These optional Windows commands export matched package identities and request installation on another machine. Review sources, versions, licenses and missing-package warnings first; export/import does not restore the whole machine.
# 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
# Example profile fragment: merge into an existing profile after inspection
@'
Import-Module posh-git
Set-PSReadLineOption -PredictionSource HistoryAndPlugin
function gst { git status -sb }
'@
# Review and merge; do not overwrite an existing profile.What to notice:
winget exportwrites matched packages and warns about unmatched applications. Inspect the manifest for private package identities before deciding whether to commit it.--accept-*-agreementsskips the interactive prompts so this can run unattended.- The here-string only prints a proposed profile fragment. Install/verify the referenced modules separately, inspect
$PROFILE, then merge the desired lines; this example performs no profile write. - Keep history-rewriting pushes explicit; an alias hides the branch/lease review that makes the operation intentional.
- Settings Sync can restore selected editor preferences; validate SDKs, extensions, credentials and a clean build separately. No onboarding-time guarantee follows from an import.
4. A .gitignore that covers both .NET and Node in one repo
A mixed full-stack repo needs both halves. Pattern precedence affects untracked candidates; no pattern can untrack an already committed dist/ file. Review existing tracked paths separately.
# ----- 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/
*.swpWhat to notice:
- Group by domain with comments so the file stays readable as it grows.
- This example ignores exactly
.envand.env.local, not every.env.*filename. Add project-specific secret filenames intentionally; never rely on ignore patterns as secret scanning or history cleanup. appsettings.Development.local.jsonis a custom filename requiring an explicit AddJsonFile registration, not a default-loaded ASP.NET Core configuration file — 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 checks Git repos in immediate child directories of~/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.
- Inspect the provided
-Staleimplementation and test a commit on each side of the 30-day cutoff (git log -1 --format=%ct); explain why an unknown HEAD is not an old commit. - 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. - Keep this exercise local; inspect before/after commit IDs. Publishing rewritten history is a separate coordinated operation, not required to learn rebase.
- Use
git reflogto inspect the pre-rebase commit, then create a new recovery branch at that commit. Preserve uncommitted work instead of practicingreset --hardon valuable files. - Done when: five feature commits become one without changing the final tree, and a recovery branch retains the old tip. Reflog is local and expirable, not a backup.
-
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).
- Select an available throttling preset, record its bandwidth/latency, reload and repeat. Which segments grew?
- Done when: you can state a measured hypothesis and a follow-up check; one TTFB bar alone cannot separate network latency from server processing.
- Open a real website you use daily, F12 → Network, hard-reload (
-
Compound launch in VS Code. Goal: F5 starts API + SPA together.
- In a trusted small full-stack repo, supply
build-api; start the loopback-only Vite server separately, or configurespa-devwith background readiness signaling. - Write
.vscode/launch.jsonwith a compound from the worked example. - Press F5; confirm a .NET breakpoint and a JS breakpoint both hit.
- Confirm
serverReadyActionopens the actual logged scheme/URL; configure an API documentation route only if the app supplies one. - Done when: both processes are running, both debuggers attach and both breakpoints fire. To claim automatic one-F5 startup, also demonstrate the optional SPA task’s readiness and shutdown behavior.
- In a trusted small full-stack repo, supply
-
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. - Use a disposable copy to compare package-manager resolutions; preserve the original lockfile and record sizes.
- Record an exact reviewed pnpm version in
packageManagerand provision that version in the shell and CI. If using Corepack, verify it is installed/enabled and honors the field; the field alone installs nothing. Node’s former bundled Corepack is not a universal availability guarantee. - Update CI to use
pnpm install --frozen-lockfile. - Done when: clean installs reproduce the lockfile and cold/warm timings are recorded without a promised speedup.
- 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 your authorized disposable Windows VM, inspect the manifest and run
winget import -i ~/winget.json; record unavailable packages/versions. - Commit the manifest to your dotfiles repo.
- Done when: every intended package is matched to an installed version or a documented unresolved item, and the required SDK/tool commands work. Settings and credentials remain separate.
- Run
Self-check questions
- Explain Git’s three trees and the distinction between a remote repository and a local remote-tracking ref.
- 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 does PowerShell’s execution policy control, how do scopes affect it, and what should you inspect before changing it?
- What does
git refloggive you thatgit logdoes not, and why is a 90-day recovery promise unsafe? - What is
--force-with-leaseand why is it safer than--force? - What changes when you switch a project from npm to pnpm — layout, hoisting, resolution and measured cold/warm behavior?
- 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 why is its JSON not a portable whole-machine or cross-package-manager backup?
Worked self-check answers
- Three trees: working files, index and HEAD snapshot.
addstages selected content,commitrecords the index. A remote is another repository;origin/mainis the last locally recorded remote branch tip, not a live query of that repository. - Fetch versus pull: fetch obtains objects/ref updates without integrating them into the current branch; pull also integrates. Fetch first when you need to inspect divergence and choose the team’s approved integration strategy.
- Merge versus rebase: merge preserves branch histories (or fast-forwards); rebase creates rewritten commits on a new base. Rebase unshared work when appropriate; do not rewrite shared commits without coordination.
- Ranges versus lockfile:
^1.2.3allows versions below 2.0.0 from 1.2.3;~1.2.3stays below 1.3.0; the exact string fixes that dependency declaration. None is a lockfile. A lockfile records the resolved graph; frozen CI installation checks agreement, not vulnerability absence. - Timing: TTFB includes round-trip latency and server response preparation. Queueing/stalling can reflect priority, cache allocation or HTTP/1.x connection limits. Compare traces, byte counts and controlled network conditions before choosing a fix.
- Execution policy: it controls script-loading conditions, not whether code is trustworthy. Inspect all scopes and effective policy; Group Policy takes precedence. Change only a permitted scope on a machine you administer.
- Reflog: local ref movement records can reveal an old tip no longer in the visible branch history. Documented expiry defaults include 90 days for entries and 30 days for unreachable entries, both configurable; garbage collection and explicit expiry mean there is no guaranteed recovery window.
- Lease: a force-with-lease checks an expected remote tip. The shorthand’s tracking ref can change via background fetch; explicit expected IDs and branch-owner coordination matter. Refusal calls for inspection, not a blind retry.
- pnpm: a content-addressed store plus links changes layout and reuse. Hoisting remains configurable, so undeclared imports are not universally impossible. Compare the actual resolved graph and clean cold/warm runs on the same filesystem; no fixed speedup follows.
- Compound launch: it starts named debugger configurations together. It does not invent build tasks, server readiness, source maps or SDKs. In this example Vite must already be ready unless a background prelaunch task explicitly signals readiness.
- Settings: user settings follow the developer; workspace/folder settings apply to that repository scope. Commit safe project-specific conventions, not local credentials/absolute paths. CI checks, not editor suggestions, enforce the team’s acceptance rules.
- Export: winget records source-matched package IDs and optional versions, warning on unmatched apps. It does not capture every installer, settings or secrets, and its schema/IDs are not interchangeable with another package manager’s manifest. Test restoration instead of assuming it.
Exercise acceptance answers
- Status: a dirty no-upstream repo should have
Dirty > 0, null ahead/behind andno-upstream; a clean tracked repo with equal local tips has zeros. Neither result proves the remote has not changed. A stale known timestamp is selected; unknown HEAD is not silently classified as stale. - History: compare the old/new tree IDs after squashing; they should match even though commit IDs differ. Create a recovery branch at the inspected old tip. No remote write is needed for this exercise.
- Network: if added latency raises TTFB while response bytes stay constant, round-trip time is a plausible contributor, not proof the server is fast. Correlate server spans; inspect download CPU contention as a separate possibility.
- Debugging: both breakpoints must bind to the expected source and one request must cross the API/SPA boundary. A running terminal alone proves neither debugger attachment nor readiness; use the exact API URL and fixed Vite port.
- Migration: preserve the starting lockfile in a disposable copy, inspect resolution differences, select one package-manager/lockfile policy and run a frozen clean install. Accept correctness before comparing performance; faster with different dependencies is not an equivalent install.
- Bootstrap: reconcile requested versus installed package IDs/versions and all warnings, then check actual tool commands. A successful process exit is not evidence that private/unmatched apps, editor modules or settings were restored.
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. Inspect the team’s Git integration policy and effective PowerShell execution policy before making local configuration changes. 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, using the hub’s Azure DevOps delivery reference (GitHub Actions would be a separate implementation choice). 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. Choose the capstone’s Git integration policy with its contributors; normal pushes and reviewed unshared-branch rebases do not make force-push a default for every PR. 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.
Validate onboarding with a second contributor: record missing packages, manual steps and whether both debuggers work after a clean setup. That evidence is more useful than a promised 30-minute setup time.
Next chapter → Ch 2 — Web fundamentals
Corrected contracts and failure analysis
An advanced function gets common parameters from CmdletBinding, but -WhatIf and -Confirm require SupportsShouldProcess and actual calls to $PSCmdlet.ShouldProcess. Merely accepting a switch does not prevent writes. Native Git exit codes require explicit checks; $ErrorActionPreference alone is not a portable substitute. A missing upstream is unknown, not zero commits behind.
Git has working tree, index and commit history; a remote is a separate repository, while origin/main is a local remote-tracking reference. Reflog retention is configurable, not a guaranteed recovery window. --force-with-lease is safer than force, not universally safe: background fetch can advance the tracking reference it relies on. Agree on branch ownership; do not use history rewriting as a default team synchronization policy.
A lockfile records a dependency resolution, not a security assessment or an operating-system image. Pin SDK/tool versions, record platform dependencies and compare cold/warm install measurements instead of promising a speed multiplier.
Boundary exercise with solution
A repo has no upstream; another has a dirty file. What should a read-only status script report, and should it fetch?
Solution and reasoning
Report upstream unavailable separately from a numeric ahead/behind result, preserve the dirty state and do not fetch by default. Fetch changes refs and requires network; make it an explicit option. A bootstrap must merge an existing profile, not overwrite it.
Source-backed review notes
- about_Functions_CmdletBindingAttribute - PowerShell | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “The SupportsShouldProcess argument adds Confirm and WhatIf parameters to the function. The Confirm parameter prompts the user before it runs the command on each object in the pipeline. The WhatIf parameter lists the changes that the command would make, instead of running the command.”
- Git - git-push Documentation — accessed 2026-10-07. Exact supporting passage: “The protection it offers over --force is ensuring that subsequent changes your work wasn’t based on aren’t clobbered, but this is trivially defeated if some background process is updating refs in the background. We don’t have anything except the remote tracking info to go by as a heuristic for refs you’re expected to have seen & are willing to clobber.”
Current documentation checks
Checked 9 October 2026: PowerShell policy scopes, Git status, push leases, reflog expiry, winget export and import, VS Code tasks, pnpm’s layout/hoisting, Corepack’s official repository, and DevTools timing phases. Local Git fixtures were executed; PowerShell, Windows package installation, .NET builds, editor F5 and browser debugging remain unexecuted integration boundaries, not production guarantees.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.