diff --git a/.github/fixtures/host-commands/bash-only-initialize-command.json b/.github/fixtures/host-commands/bash-only-initialize-command.json new file mode 100644 index 0000000..d50f9cc --- /dev/null +++ b/.github/fixtures/host-commands/bash-only-initialize-command.json @@ -0,0 +1,4 @@ +{ + "image": "ghcr.io/helpers-no/devcontainer-toolbox:latest", + "initializeCommand": "mkdir -p .devcontainer.secrets/env-vars && hostname -s > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || hostname > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || true" +} diff --git a/.github/scripts/test-initialize-command.ps1 b/.github/scripts/test-initialize-command.ps1 new file mode 100644 index 0000000..a256bbd --- /dev/null +++ b/.github/scripts/test-initialize-command.ps1 @@ -0,0 +1,73 @@ +# test-initialize-command.ps1 - Run a devcontainer.json's initializeCommand the way VS Code does +# +# Uses the real devcontainers CLI (`devcontainer up`), so the command goes through the same +# path as for a user: on Windows the CLI runs it with `cmd.exe /c` (devcontainers/cli +# src/spec-node/utils.ts, runInitializeCommand). The CLI runs initializeCommand before it pulls +# the image, so on a runner that cannot run Linux containers `up` still fails later - that is +# expected. Only the initializeCommand outcome is judged. +# +# Usage: test-initialize-command.ps1 -Config -Expect pass|fail + +param( + [Parameter(Mandatory = $true)][string]$Config, + [Parameter(Mandatory = $true)][ValidateSet('pass', 'fail')][string]$Expect +) + +$ErrorActionPreference = 'Stop' + +$ranMarker = 'Running the initializeCommand from devcontainer.json' +$failedMarker = 'The initializeCommand in the devcontainer.json failed' + +# GitHub's Windows runners put Git for Windows' Unix tools (C:\Program Files\Git\usr\bin, with +# true.exe, sh.exe, ...) on PATH. An ordinary office PC does not: Git's default install does not +# add them, and most users have no Git at all. With them on PATH the old bash-only command ends in +# a real `true` and "succeeds", so the check proves nothing. Remove them so cmd.exe sees what a +# normal PC sees, and verify it. +$env:Path = (($env:Path -split ';') | Where-Object { + $_ -and ($_ -notmatch '\\Git\\(usr\\)?bin\\?$') -and ($_ -notmatch '\\Git\\mingw64\\bin\\?$') -and ($_ -notmatch '(msys|cygwin)') +}) -join ';' +$unixTrue = Get-Command true -ErrorAction SilentlyContinue +if ($unixTrue) { + Write-Host "ERROR: a Unix 'true' is still on PATH ($($unixTrue.Source)), so this run would not match a normal PC." + exit 1 +} + +$workspace = Join-Path ([IO.Path]::GetTempPath()) ("dct-ic-" + [guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path (Join-Path $workspace '.devcontainer') -Force | Out-Null +Copy-Item -LiteralPath $Config -Destination (Join-Path $workspace '.devcontainer\devcontainer.json') + +Write-Host "Config: $Config" +Write-Host "Expect: $Expect" +Write-Host "Workspace: $workspace" +Write-Host "" + +$ErrorActionPreference = 'Continue' +$output = (& devcontainer up --workspace-folder $workspace 2>&1 | Out-String) +$ErrorActionPreference = 'Stop' + +$ran = $output -match [regex]::Escape($ranMarker) +$failed = $output -match [regex]::Escape($failedMarker) + +Write-Host "initializeCommand ran: $ran" +Write-Host "initializeCommand failed: $failed" + +$ok = $false +if (-not $ran) { + Write-Host "ERROR: the CLI never reached initializeCommand, so this run proves nothing." +} elseif ($Expect -eq 'pass' -and -not $failed) { + $ok = $true +} elseif ($Expect -eq 'fail' -and $failed) { + $ok = $true +} + +if (-not $ok) { + Write-Host "" + Write-Host "----- devcontainer up output -----" + Write-Host $output + Write-Host "----------------------------------" + Write-Host "RESULT: FAIL (expected initializeCommand to $Expect)" + exit 1 +} + +Write-Host "RESULT: OK (initializeCommand did $Expect, as expected)" +exit 0 diff --git a/.github/workflows/host-commands.yml b/.github/workflows/host-commands.yml new file mode 100644 index 0000000..662a7d7 --- /dev/null +++ b/.github/workflows/host-commands.yml @@ -0,0 +1,67 @@ +# Host-side commands in the user template must work on every host OS. +# +# initializeCommand runs on the HOST, before the container exists: with /bin/sh on Mac/Linux and +# with cmd.exe on Windows. A bash-only command broke new Windows installs from 2026-04-07 (733dd73) +# on every PC without Unix tools on PATH, which is the normal case. The test script removes the +# runner's Git-for-Windows Unix tools from PATH, because with them the old command "passes". See website/docs/ai-developer/plans/active/PLAN-fix-windows-quickstart.md. + +name: Host Commands + +on: + push: + branches: [main] + paths: + - 'devcontainer-user-template.json' + - '.github/workflows/host-commands.yml' + - '.github/scripts/test-initialize-command.ps1' + - '.github/fixtures/host-commands/**' + pull_request: + branches: [main] + paths: + - 'devcontainer-user-template.json' + - '.github/workflows/host-commands.yml' + - '.github/scripts/test-initialize-command.ps1' + - '.github/fixtures/host-commands/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + initialize-command-windows: + name: initializeCommand on Windows (cmd.exe) + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install the devcontainers CLI (the engine VS Code uses) + run: npm install -g @devcontainers/cli@0.89.0 + + - name: Template command must succeed + shell: pwsh + run: ./.github/scripts/test-initialize-command.ps1 -Config devcontainer-user-template.json -Expect pass + + - name: Control - the old bash-only command must fail + shell: pwsh + run: ./.github/scripts/test-initialize-command.ps1 -Config .github/fixtures/host-commands/bash-only-initialize-command.json -Expect fail + + initialize-command-linux: + name: initializeCommand on Linux (/bin/sh) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + + - name: Template command must write the host hostname + run: | + cmd=$(jq -r .initializeCommand devcontainer-user-template.json) + ws=$(mktemp -d) + cd "$ws" + /bin/sh -c "$cmd" + test -s .devcontainer.secrets/env-vars/.host-hostname + echo "host hostname captured: $(cat .devcontainer.secrets/env-vars/.host-hostname)" diff --git a/devcontainer-user-template.json b/devcontainer-user-template.json index eac5ca5..45213c7 100644 --- a/devcontainer-user-template.json +++ b/devcontainer-user-template.json @@ -57,6 +57,6 @@ "shutdownAction": "stopContainer", "updateRemoteUserUID": true, "init": true, - "initializeCommand": "mkdir -p .devcontainer.secrets/env-vars && hostname -s > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || hostname > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || true", + "initializeCommand": "ver || sh -c \"mkdir -p .devcontainer.secrets/env-vars && { hostname -s 2>/dev/null || hostname; } > .devcontainer.secrets/env-vars/.host-hostname; true\"", "postStartCommand": "bash /opt/devcontainer-toolbox/additions/config-host-info.sh --verify 2>/dev/null || true" } diff --git a/install.ps1 b/install.ps1 index d7b0868..549160e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -89,6 +89,47 @@ if (Test-Path $extFile) { Write-Host "Created $extFile with Dev Containers extension recommendation" } +# --- 4b. Install the Dev Containers extension in VS Code --------------------------- +# Per user, no admin. VS Code itself comes from Intune (Company Portal) or the user's own +# install, so `code` may not be on PATH yet: also look in the user and system install folders. + +$codeCmd = $null +$codeOnPath = Get-Command code -ErrorAction SilentlyContinue +if ($codeOnPath) { + $codeCmd = $codeOnPath.Source +} else { + foreach ($candidate in @( + "$env:LOCALAPPDATA\Programs\Microsoft VS Code\bin\code.cmd", + "$env:ProgramFiles\Microsoft VS Code\bin\code.cmd" + )) { + if (Test-Path $candidate) { $codeCmd = $candidate; break } + } +} + +Write-Host "" +if (-not $codeCmd) { + Write-Host "VS Code was not found on this PC, so the Dev Containers extension could not be installed." -ForegroundColor Yellow + Write-Host "Install VS Code (on a work PC: from Company Portal), then run this again." +} else { + # Native commands write progress to stderr; keep that from being treated as a script error. + $prevPref = $ErrorActionPreference + $ErrorActionPreference = "Continue" + $installed = & $codeCmd --list-extensions 2>$null + if ($installed -contains $extId) { + Write-Host "Dev Containers extension is already installed in VS Code" + } else { + Write-Host "Installing the Dev Containers extension in VS Code..." + & $codeCmd --install-extension $extId | Out-Host + if ($LASTEXITCODE -eq 0) { + Write-Host "Dev Containers extension installed" + } else { + Write-Host "Could not install the Dev Containers extension (code exit $LASTEXITCODE)." -ForegroundColor Yellow + Write-Host "Open VS Code and accept its offer to install the recommended extensions." + } + } + $ErrorActionPreference = $prevPref +} + # --- 5. Pull the Docker image ----------------------------------------------------- Write-Host "" diff --git a/install.sh b/install.sh index c5b7f09..31a5739 100755 --- a/install.sh +++ b/install.sh @@ -111,6 +111,40 @@ EXTENSIONS_EOF echo "Created $EXT_FILE with Dev Containers extension recommendation" fi +# ─── 4b. Install the Dev Containers extension in VS Code ───────────────────── +# Per user, no sudo. `code` is often not on PATH on macOS (the shell command is +# opt-in), so also look inside the app bundle. + +CODE_CMD="" +if command -v code >/dev/null 2>&1; then + CODE_CMD="$(command -v code)" +else + for candidate in \ + "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" \ + "$HOME/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"; do + if [ -x "$candidate" ]; then + CODE_CMD="$candidate" + break + fi + done +fi + +echo "" +if [ -z "$CODE_CMD" ]; then + echo "VS Code was not found, so the Dev Containers extension could not be installed." + echo "Install VS Code (on a work Mac: from Self Service), then run this again." +elif "$CODE_CMD" --list-extensions 2>/dev/null | grep -qx "$EXT_ID"; then + echo "Dev Containers extension is already installed in VS Code" +else + echo "Installing the Dev Containers extension in VS Code..." + if "$CODE_CMD" --install-extension "$EXT_ID"; then + echo "Dev Containers extension installed" + else + echo "Could not install the Dev Containers extension." + echo "Open VS Code and accept its offer to install the recommended extensions." + fi +fi + # ─── 5. Pull the Docker image ──────────────────────────────────────────────── echo "" diff --git a/version.txt b/version.txt index a8fdfda..53adb84 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.8.1 +1.8.2 diff --git a/website/docs/ai-developer/plans/backlog/PLAN-fix-windows-quickstart.md b/website/docs/ai-developer/plans/active/PLAN-fix-windows-quickstart.md similarity index 66% rename from website/docs/ai-developer/plans/backlog/PLAN-fix-windows-quickstart.md rename to website/docs/ai-developer/plans/active/PLAN-fix-windows-quickstart.md index 856fde8..f0b0400 100644 --- a/website/docs/ai-developer/plans/backlog/PLAN-fix-windows-quickstart.md +++ b/website/docs/ai-developer/plans/active/PLAN-fix-windows-quickstart.md @@ -4,17 +4,17 @@ > - [WORKFLOW.md](../../WORKFLOW.md) - The implementation process > - [PLANS.md](../../PLANS.md) - Plan structure and best practices -## Status: Backlog +## Status: Active **Goal**: A Windows user who follows the Quick Start on [dct.sovereignsky.no/docs](https://dct.sovereignsky.no/docs/) gets a running devcontainer. -**Who this is for**: ordinary Windows office users with no knowledge of git, containers or Docker (Terje, 2026-09-25). Every message this plan touches must be plain language with the next action spelled out. This plan fixes the defects in today's script; installing the prerequisites for the user (WSL, Rancher Desktop, VS Code) belongs to [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) (decision 2026-09-25, see [PLAN-host-installer-handover](PLAN-host-installer-handover.md)). +**Who this is for**: ordinary Windows office users with no knowledge of git, containers or Docker (Terje, 2026-09-25). Every message this plan touches must be plain language with the next action spelled out. This plan fixes the defects in today's script; installing the prerequisites for the user (WSL, Rancher Desktop, VS Code) belongs to [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) (decision 2026-09-25, see [PLAN-host-installer-handover](../backlog/PLAN-host-installer-handover.md)). -**Priority**: High — every new Windows install is affected. Reported by Terje, 2026-09-24. +**Priority**: High — every new Windows install on a PC without Unix tools on PATH (the normal office PC) is affected. Reported by Terje, 2026-09-24. **Last Updated**: 2026-09-25 -**Related**: [PLAN-host-installer-handover](PLAN-host-installer-handover.md) (DCT's side of the host-installer split), [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) (the host installer), [PLAN-windows-testing](PLAN-windows-testing.md) (broader Windows validation) +**Related**: [PLAN-host-installer-handover](../backlog/PLAN-host-installer-handover.md) (DCT's side of the host-installer split), [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) (the host installer), [PLAN-windows-testing](../backlog/PLAN-windows-testing.md) (broader Windows validation) --- @@ -56,11 +56,11 @@ The Quick Start runs it as `irm … | iex`, so the script runs inside the user's --- -## Phase 1: Cross-shell `initializeCommand` +## Phase 1: Cross-shell `initializeCommand` — IN PROGRESS ### Tasks -- [ ] 1.1 Replace the template's `initializeCommand` with one string that is valid in both shells: +- [x] 1.1 Replace the template's `initializeCommand` with one string that is valid in both shells: ``` ver || sh -c "mkdir -p .devcontainer.secrets/env-vars && { hostname -s 2>/dev/null || hostname; } > .devcontainer.secrets/env-vars/.host-hostname; true" @@ -71,7 +71,10 @@ The Quick Start runs it as `irm … | iex`, so the script runs inside the user's - Windows does not need the file: `config-host-info.sh` checks `DEV_HOST_COMPUTERNAME` (from `${localEnv:COMPUTERNAME}`) before it reads the file (lines 76–87). - Measured on macOS 2026-09-25: exit 0 and the same `.host-hostname` content as today's command. The side effect is one `ver: command not found` line in the startup log. - [ ] 1.2 CI: add a `windows-latest` job that reads `initializeCommand` from `devcontainer-user-template.json` and runs it with `cmd.exe /c` in a temp folder. It must exit 0. Also run the Linux side with `/bin/sh -c` and check that `.host-hostname` is non-empty. This guards against the regression coming back; nothing tested the Windows side before. -- [ ] 1.3 Add a comment next to the command (in the contributor docs, since JSON has no comments) saying it runs under `cmd.exe` on Windows and must stay valid in both shells. + - **As built (2026-09-25):** a separate workflow, `.github/workflows/host-commands.yml`, not `ci-tests.yml`. `ci-tests.yml` only triggers on `.devcontainer/**` and builds the whole image first, and the template lives at the repo root. The Windows job runs the command through the **real devcontainers CLI** (`@devcontainers/cli@0.89.0`, `devcontainer up`), so it takes the same `cmd.exe /c` path, quoting included, as VS Code does. A **control step** runs the old bash-only command (`.github/fixtures/host-commands/bash-only-initialize-command.json`) and must see it fail, which proves the check can detect the bug. Script: `.github/scripts/test-initialize-command.ps1`. + - **First run on GitHub (PR #102, 2026-09-25):** the new command passed through the real CLI on `windows-latest`. **The control did not fail**: `cmd.exe` printed "The syntax of the command is incorrect." and "The system cannot find the path specified.", but the old command still exited 0, because the runner has Git for Windows' `usr\bin` (with `true.exe`) on PATH. A normal office PC does not. The script now strips Git/MSYS/Cygwin Unix tool folders from PATH and refuses to run if a Unix `true` is still found. That also sharpens the bug: it hits PCs **without** Unix tools on PATH, which is the normal case. +- [x] 1.3 Add a comment next to the command (in the contributor docs, since JSON has no comments) saying it runs under `cmd.exe` on Windows and must stay valid in both shells. + - Done in `contributors/architecture/devcontainer-json.md` and `startup-lifecycle.md`. Both still showed the old command and claimed `.host-hostname` is written on Windows; corrected. ### Validation @@ -85,8 +88,12 @@ The CI job is green on `windows-latest` and `ubuntu-latest`. The command still w - [ ] 2.1 Wrap the script body in a scriptblock (`& { … }`), so `$ErrorActionPreference` stays local to it, and replace every `exit 1` with an error message plus `return`. The user's window stays open and shows what went wrong. - [ ] 2.2 Check `$LASTEXITCODE` after `docker pull`. On failure, stop without printing "installed!" and say what to do in plain words, for example: "Rancher Desktop is not running. Start Rancher Desktop from the Start menu, wait until it says it is ready, then run this again." -- [ ] 2.3 Rewrite every message the script prints for a non-developer: no "PATH", "Docker CLI" or "image" without explanation; each error says what happened and the one thing to do next. The missing-Docker case uses the handover sentence from [PLAN-host-installer-handover](PLAN-host-installer-handover.md) Phase 2. -- [ ] 2.4 CI: in the `windows-latest` job, run `install.ps1` in a temp folder with `docker` missing from `PATH`. It must print the Docker error and leave the PowerShell process running (the job's next step still executes). +- [ ] 2.3 Rewrite every message the script prints for a non-developer: no "PATH", "Docker CLI" or "image" without explanation; each error says what happened and the one thing to do next. The missing-Docker case uses the handover sentence from [PLAN-host-installer-handover](../backlog/PLAN-host-installer-handover.md) Phase 2. +- [ ] 2.4 Stop deleting the user's previous backup: `install.ps1` removes an existing `.devcontainer.backup/` without asking (lines 30–31), so a second run loses the original setup. Refuse instead, with a plain message, the way `install.sh` already does. Found by client-provisioning (urb-agents #1505). +- [x] 2.5 Install the VS Code Dev Containers extension as the user (`code --install-extension ms-vscode-remote.remote-containers`), finding `code` even when it is not on PATH yet (user and system install paths). If VS Code is missing, stop with a plain message. VS Code itself comes from Intune (Terje, 2026-09-25). The shared spec is [PLAN-host-installer-handover](../backlog/PLAN-host-installer-handover.md) task 2.8. + - **Done 2026-09-25, pulled forward for Terje's PC test**, in `install.ps1` and `install.sh` (step 4b). Finds `code` on PATH, else in the user and system install folders (Windows) or the app bundle (macOS); skips when `code --list-extensions` already lists it. **Limit until 2.1:** if VS Code is missing it prints a plain warning and continues, rather than exiting `1`, because `exit` inside `irm | iex` would close the user's window. + - Checked: `install.sh` shellcheck clean; its step 4b run against a stub `code` (installs when missing, skips when present) and, with no `code` on PATH, it found this Mac's real VS Code in the app bundle. `install.ps1` parses with 0 errors and has 0 PSScriptAnalyzer findings apart from `PSAvoidUsingWriteHost` (the script has always used `Write-Host`). **Not run on Windows yet.** +- [ ] 2.6 CI: in the `windows-latest` job, run `install.ps1` in a temp folder with `docker` missing from `PATH`. It must print the Docker error and leave the PowerShell process running (the job's next step still executes). ### Validation @@ -99,7 +106,7 @@ The CI job is green. The script still works end to end on a Windows machine (Pha ### Tasks - [ ] 3.1 Split the Quick Start in `website/docs/index.md` and `README.md` into two blocks: a `bash` block for Mac/Linux and a `powershell` block for Windows, so each copy button copies one command. -- [ ] 3.1b Correct the Windows prerequisites in `website/docs/getting-started.md`: Rancher Desktop needs **Windows 11** x64 (not Windows 10), and `wsl --install` also installs Ubuntu, which asks for a Linux username the user does not need. Point to the "What your computer needs" page from [PLAN-host-installer-handover](PLAN-host-installer-handover.md) when it exists. +- [ ] 3.1b Correct the Windows prerequisites in `website/docs/getting-started.md`: Rancher Desktop needs **Windows 11** x64 (not Windows 10), and `wsl --install` also installs Ubuntu, which asks for a Linux username the user does not need. Point to the "What your computer needs" page from [PLAN-host-installer-handover](../backlog/PLAN-host-installer-handover.md) when it exists. - [ ] 3.2 **Needs a Windows machine (Terje, or someone he names):** in an empty folder, run the Quick Start from the site, open it in VS Code, and choose "Reopen in Container". The container must start, and `dev-help` must run. - [ ] 3.3 Have one non-developer office user do 3.2 from the site alone, with Rancher Desktop and VS Code already installed, and note every point where they got stuck. Pass those notes to the owner of [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) through the bus. - [ ] 3.4 Release: bump `version.txt` (PATCH). @@ -128,4 +135,4 @@ Terje confirms 3.2 on Windows. `npm run build` passes for the docs change. ## Not in this plan -- PowerShell 5.1 on old Windows 10 builds may default to TLS 1.0 *before* the script's own TLS 1.2 line runs, so `irm` itself could fail. Current Windows 10/11 is not affected; handle it in [PLAN-windows-testing](PLAN-windows-testing.md) if it shows up. +- PowerShell 5.1 on old Windows 10 builds may default to TLS 1.0 *before* the script's own TLS 1.2 line runs, so `irm` itself could fail. Current Windows 10/11 is not affected; handle it in [PLAN-windows-testing](../backlog/PLAN-windows-testing.md) if it shows up. diff --git a/website/docs/ai-developer/plans/active/README.md b/website/docs/ai-developer/plans/active/README.md index 96a8a97..9aa6c8d 100644 --- a/website/docs/ai-developer/plans/active/README.md +++ b/website/docs/ai-developer/plans/active/README.md @@ -21,4 +21,5 @@ INVESTIGATE files never live here — see [PLANS.md](../../PLANS.md). | Plan | Where it stands | |---|---| | [PLAN-dct-exec-host-helper](PLAN-dct-exec-host-helper.md) | All five phases done and shipped in 1.8.0. Some checks are marked unverified, and 5.3 (a follow-up issue in `sovdev-logger`) waits on a decision. | +| [PLAN-fix-windows-quickstart](PLAN-fix-windows-quickstart.md) | Started 2026-09-25. Phase 1 (cross-shell `initializeCommand` + Windows CI) in progress. | | [PLAN-p1-dct-shim](PLAN-p1-dct-shim.md) | Phases 1–2 shipped in 1.7.36+. Phase 3 (end-to-end test with the rewritten template READMEs) is not started. | diff --git a/website/docs/ai-developer/plans/backlog/1PRIORITY.md b/website/docs/ai-developer/plans/backlog/1PRIORITY.md index f32c358..1db7687 100644 --- a/website/docs/ai-developer/plans/backlog/1PRIORITY.md +++ b/website/docs/ai-developer/plans/backlog/1PRIORITY.md @@ -23,7 +23,7 @@ Fleet work is on the bus in `terchris/urb-agents` — | # | What | Why this one | |---|---|---| -| **1** | [PLAN-fix-windows-quickstart](PLAN-fix-windows-quickstart.md) phases 1–2: cross-shell `initializeCommand`, safe `install.ps1`, Windows CI job | Every new Windows install fails to start the container today (reported 2026-09-24). | +| **1** | [PLAN-fix-windows-quickstart](../active/PLAN-fix-windows-quickstart.md) phases 1–2: cross-shell `initializeCommand`, safe `install.ps1`, Windows CI job | Every new Windows install fails to start the container today (reported 2026-09-24). | | **2** | [PLAN-p1-dct-shim](../active/PLAN-p1-dct-shim.md) phase 3: end-to-end test of the `uis` shim with `python-basic-webserver-database` | The shim is shipped but has never been confirmed end to end. First check the prerequisite: the template README no longer mentions `docker exec`, but it also has no `uis connect` / `uis status` sections for tasks 3.2–3.3 to test. | | **3** | Close out [PLAN-dct-exec-host-helper](../active/PLAN-dct-exec-host-helper.md): run the unverified checks, then move it to `completed/` | Shipped in 1.8.0; only its bookkeeping is open. | @@ -32,7 +32,7 @@ Fleet work is on the bus in `terchris/urb-agents` — | What | Who | Since | Unblocks | Raised | |---|---|---|---|---| | Whether to open follow-up issue 5.3 in `helpers-no/sovdev-logger` | Terje | 2026-09-24 | Closing PLAN-dct-exec-host-helper | This session | -| A Windows machine for the real-install test (task 3.2) | Terje | 2026-09-25 | Releasing PLAN-fix-windows-quickstart | [the plan](PLAN-fix-windows-quickstart.md) | +| A Windows machine for the real-install test (task 3.2) | Terje | 2026-09-25 | Releasing PLAN-fix-windows-quickstart | [the plan](../active/PLAN-fix-windows-quickstart.md) | | An owner agent for helpers-no/client-provisioning | ops-dev | 2026-09-25 | The one-command install (PLAN-host-installer-handover phases 3–4) | urb-agents bus | ## If Terje wants work started, these rank highest diff --git a/website/docs/ai-developer/plans/backlog/PLAN-host-installer-handover.md b/website/docs/ai-developer/plans/backlog/PLAN-host-installer-handover.md index 8944f2b..f91d9f3 100644 --- a/website/docs/ai-developer/plans/backlog/PLAN-host-installer-handover.md +++ b/website/docs/ai-developer/plans/backlog/PLAN-host-installer-handover.md @@ -12,7 +12,7 @@ **Last Updated**: 2026-09-25 -**Related**: [PLAN-fix-windows-quickstart](PLAN-fix-windows-quickstart.md) (defects in today's `install.ps1`), [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) (the host installer) +**Related**: [PLAN-fix-windows-quickstart](../active/PLAN-fix-windows-quickstart.md) (defects in today's `install.ps1`), [helpers-no/client-provisioning](https://github.com/helpers-no/client-provisioning) (the host installer) --- @@ -21,7 +21,9 @@ | Repo | Owns | Needs admin? | |---|---|---| | **helpers-no/client-provisioning** — "the machine is ready" | WSL, Rancher Desktop, VS Code + Dev Containers extension, elevation, restart and resume, Intune/Jamf packages, and the one-script installer for non-developers | Yes | -| **devcontainer-toolbox** (this repo) — "the project is ready" | `devcontainer.json`, the image, everything inside the container. `install.ps1` / `install.sh` stay small and never need admin | No | +| **devcontainer-toolbox** (this repo) — "the project is ready" | `devcontainer.json`, the image, everything inside the container, **and the VS Code Dev Containers extension** (per user). `install.ps1` / `install.sh` stay small and never need admin | No | + +**Amended 2026-09-25 (Terje):** VS Code itself comes from **Intune**, which already deploys it on managed PCs (on unmanaged PCs, from client-provisioning's installer if it chooses). The **Dev Containers extension is installed by DCT's script**: it is per user and needs no admin. Recorded with client-provisioning in urb-agents #1533. The user still sees **one command** on DCT's website. client-provisioning's installer prepares the machine and, as its **last step, runs DCT's own `install.ps1` / `install.sh`**. DCT stays the only place that writes `devcontainer.json`. @@ -35,7 +37,9 @@ The user still sees **one command** on DCT's website. client-provisioning's inst The full investigation of the one-script installer (the user, vendor findings, managed machines, prior art, options) moved to client-provisioning with its new owner agent. It was handed over through the fleet bus. -**Owner of client-provisioning:** none yet. ops-dev was asked on 2026-09-25 to onboard an agent for it. +**Owner of client-provisioning:** the fleet agent `client-provisioning` (onboarded 2026-09-25, urb-agents #1492). + +**Contract agreed with it on 2026-09-25 (urb-agents #1505).** Phases 1–3 below are written to that agreement. Nothing in it has run on a managed Windows 11 PC or a Jamf Mac yet. --- @@ -43,39 +47,60 @@ The full investigation of the one-script installer (the user, vendor findings, m ### Tasks -- [ ] 1.1 Add one page, **"What your computer needs"**, as the single list both repos work from: Windows 11 x64 (Rancher Desktop no longer supports Windows 10), or macOS; Rancher Desktop ≥ 1.24 with the `moby` engine; VS Code with the Dev Containers extension; virtualization on; free disk space for the image. Plain language first, exact versions in a table below. -- [ ] 1.2 Link it from Getting Started and the Quick Start. +- [ ] 1.1 Add one page, **"What your computer needs"**, as the single list both repos work from. Plain language first, exact values in a table: + - **Windows 11, build ≥ 22000, x64.** Rancher Desktop no longer supports Windows 10. + - **WSL 2, installed without a Linux distribution.** It's the step with the restart. + - **macOS 13 (Ventura) or later, Apple Silicon (M1 or later).** Intel Macs are out of scope (Terje, urb-agents #1511, passed on in #1514). The macOS minimum comes from the Rancher Desktop 1.24 docs ("macOS 13 (Ventura) or higher"); client-provisioning's Mac check uses the same number. + - **Memory and CPU:** Rancher Desktop 1.24 recommends 8 GB of memory and 4 CPUs on both Windows and macOS. Many office laptops sit exactly at 8 GB, so state it as recommended, not required. + - **Rancher Desktop ≥ 1.24.0**, engine `moby`, Kubernetes off. + - **VS Code** (from Intune on managed PCs). The `ms-vscode-remote.remote-containers` extension is **not** a prerequisite: DCT's script installs it. + - **Virtualization** on. + - **Free disk: DCT owns this number.** Measure the image size on disk and state it × 2, in GB. +- [ ] 1.2 Publish the same values as a machine-readable **`host-requirements.json`**, so client-provisioning's checks read DCT's numbers instead of copying them. +- [ ] 1.3 Link the page from Getting Started and the Quick Start. ### Validation -client-provisioning's owner agent confirms the page is what their installer checks against. `npm run build` passes. +client-provisioning confirms its installer checks against the page / `host-requirements.json`. `npm run build` passes. --- -## Phase 2: `install.ps1` / `install.sh` as a callable last step +## Phase 2: `install.ps1` / `install.sh` as a callable, pinned last step ### Tasks -- [ ] 2.1 Make both scripts safe to call from another script: a parameter for the target folder, no interactive prompts, documented exit codes (for example 0 = done, 1 = a prerequisite is missing, 2 = download failed). -- [ ] 2.2 When a prerequisite is missing (no Docker/Rancher, Rancher not running, no VS Code), print one plain sentence and the next action instead of a technical error, for example: *"Your PC isn't ready for DevContainer Toolbox yet. Run this first: [the installer command]"*. Until client-provisioning's installer exists, point to the "What your computer needs" page. -- [ ] 2.3 Both scripts stay admin-free. Add a test that fails if either one elevates or calls an installer. +- [ ] 2.1 **Interface:** `-TargetDir ` / `--target-dir `, no interactive prompts, **never elevates**, safe to run twice on the same folder. client-provisioning runs it un-elevated, as the user, after its admin steps and the restart. +- [ ] 2.2 **Exit codes:** `0` done, `1` a prerequisite is missing (runtime not installed or not running, no VS Code), `2` download or network failure, `3` target folder problem. Never use `3010` / `1641` (they mean "restart required" to client-provisioning and Intune). Each error line carries an **`ERRnnn`** code, which client-provisioning passes through verbatim for support calls. +- [ ] 2.3 **Absorb what client-provisioning's `devcontainer-init` does**, so it can retire it: + - Back up an existing `.devcontainer/` to `.devcontainer.backup/`, and **refuse when a backup already exists**. `install.sh` already refuses; **`install.ps1` silently deletes the old backup** (lines 30–31), so a second run loses the user's original setup. That fix is shared with [PLAN-fix-windows-quickstart](../active/PLAN-fix-windows-quickstart.md) Phase 2. + - Create or merge the host-side `.vscode/extensions.json` recommending the Dev Containers extension, both scripts. It's needed on the host before "Reopen in Container". + - A connectivity pre-check before downloading (exit `2`, with its own message). +- [ ] 2.4 **Pin to the release:** the script defaults to the image tag of its own release (`ghcr.io/helpers-no/devcontainer-toolbox:`), not `latest`, and downloads the template from the same tag. +- [ ] 2.5 When a prerequisite is missing, print one plain sentence and the next action. Until client-provisioning's installer exists, point to the "What your computer needs" page. +- [ ] 2.7 `install.sh` checks the Mac architecture (`uname -m` = `arm64`) and stops on an Intel Mac with the same plain sentence client-provisioning's installer uses, so a user who runs DCT's script directly gets the same answer (suggested in urb-agents #1514). +- [ ] 2.8 **Install the Dev Containers extension** (`code --install-extension ms-vscode-remote.remote-containers`), as the user, in both scripts. Skip it if `code --list-extensions` already lists it. + - **Find `code` even when it is not on PATH yet** (for example right after Intune installed VS Code). Windows: `%LOCALAPPDATA%\Programs\Microsoft VS Code\bin\code.cmd` (user install), then `%ProgramFiles%\Microsoft VS Code\bin\code.cmd` (system install). macOS: `/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`. + - If VS Code is not found, exit `1` with an `ERRnnn` line and the plain handover sentence. +- [ ] 2.6 Both scripts stay admin-free. Add a test that fails if either one elevates or calls an installer. ### Validation -Tests pass. Called with a missing Rancher Desktop, each script exits with the documented code and prints the handover sentence. +Tests pass. Called with Rancher Desktop missing, each script exits `1`, prints an `ERRnnn` line and the handover sentence. Run twice on the same folder, it refuses to overwrite the backup and loses nothing. --- -## Phase 3: Agree the handover with client-provisioning +## Phase 3: Releases client-provisioning can pin ### Tasks -- [ ] 3.1 Through the bus: agree with client-provisioning's owner agent how their installer calls DCT's script: the URL (a release tag, not `main`), the parameters, and the exit codes from Phase 2. -- [ ] 3.2 Ask them to replace their `devcontainer-init` (which writes `devcontainer.json` its own way) with a call to DCT's script, so the file has one owner. That change is theirs to make. +- [x] 3.1 Agree the handover with client-provisioning (urb-agents #1505, 2026-09-25). +- [ ] 3.2 **Tag each release as `v`** (DCT has no version tags today; only a moving `latest`) and **protect `v*` tags** against moving. +- [ ] 3.3 **Publish a GitHub release per version** with `install.ps1`, `install.sh`, `SHA256SUMS` and `host-requirements.json` as assets. client-provisioning pins the version and the hash, and refuses on a mismatch. Add this to the release workflow and to `contributors/releasing.md`. +- [ ] 3.4 Send client-provisioning the first release URL as a bus task. It then replaces `devcontainer-init` (which today downloads DCT's template from `main`, unpinned) with a call to the pinned script. That change is theirs, with Terje's go. ### Validation -A client-provisioning test run ends by calling DCT's script and produces the same `devcontainer.json` as DCT's own installer. +A client-provisioning test run fetches a tagged DCT release, verifies `SHA256SUMS`, runs the script un-elevated, and gets the same `devcontainer.json` as DCT's own installer. --- @@ -94,7 +119,8 @@ A non-developer on a clean Windows 11 PC follows the Quick Start alone and gets ## Acceptance Criteria -- [ ] "What your computer needs" exists and client-provisioning checks against it +- [ ] "What your computer needs" and `host-requirements.json` exist, and client-provisioning checks against them +- [ ] Every DCT release is a protected `v` tag with `SHA256SUMS`, and its scripts pull that release's image - [ ] DCT's install scripts never need admin, and can be called by another script with documented exit codes - [ ] A missing prerequisite produces a plain-language handover, not a technical error - [ ] `devcontainer.json` is written by DCT's script only @@ -106,4 +132,5 @@ A non-developer on a clean Windows 11 PC follows the Quick Start alone and gets - `website/docs/getting-started.md`, `website/docs/index.md`, `README.md` - a new "What your computer needs" page under `website/docs/` - `.devcontainer/additions/tests/` (admin-free test) +- `.github/workflows/` (release assets, tag protection), `website/docs/contributors/releasing.md` - `version.txt` diff --git a/website/docs/ai-developer/plans/backlog/index.md b/website/docs/ai-developer/plans/backlog/index.md index b1fe443..ba9ad15 100644 --- a/website/docs/ai-developer/plans/backlog/index.md +++ b/website/docs/ai-developer/plans/backlog/index.md @@ -14,7 +14,6 @@ in progress. | Item | What it does | Priority | |---|---|---| -| [PLAN-fix-windows-quickstart](PLAN-fix-windows-quickstart.md) | Make the Windows Quick Start work: cross-shell `initializeCommand`, safe `install.ps1`, split docs block | High | | [PLAN-host-installer-handover](PLAN-host-installer-handover.md) | DCT's side of the host-installer split: host requirements page, admin-free callable `install.ps1`/`install.sh`, plain-language handover | High | | [PLAN-finish-move-to-helpers-no](PLAN-finish-move-to-helpers-no.md) | Leftovers of the terchris → helpers-no move: fork network reversed, old image deleted (phases 1–2 done) | — | | [INVESTIGATE-outdated-software-versions](INVESTIGATE-outdated-software-versions.md) | Decide how the base image pins or tracks Node and other bundled software versions | — | diff --git a/website/docs/contributors/architecture/devcontainer-json.md b/website/docs/contributors/architecture/devcontainer-json.md index d4fd646..c8e4ca4 100644 --- a/website/docs/contributors/architecture/devcontainer-json.md +++ b/website/docs/contributors/architecture/devcontainer-json.md @@ -289,19 +289,27 @@ Adds a tiny init process (`tini`) as PID 1 inside the container. This properly h ### initializeCommand ```json -"initializeCommand": "mkdir -p .devcontainer.secrets/env-vars && hostname -s > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || hostname > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || true" +"initializeCommand": "ver || sh -c \"mkdir -p .devcontainer.secrets/env-vars && { hostname -s 2>/dev/null || hostname; } > .devcontainer.secrets/env-vars/.host-hostname; true\"" ``` Runs on the **host machine** before the container starts. This is the only devcontainer lifecycle command that executes outside the container. +**It must be valid in two shells.** The devcontainers CLI (which VS Code uses) runs a string `initializeCommand` with `cmd.exe /c` on Windows and with `/bin/sh -c` everywhere else (`devcontainers/cli`, `src/spec-node/utils.ts`, `runInitializeCommand`). A non-zero exit stops the container from starting. A bash-only command therefore broke new Windows installs from 2026-04-07 (`733dd73`) until 1.8.2, on every PC without Unix tools on PATH, which is the normal case. (With Git for Windows' Unix tools on PATH, a stray `true.exe` made the old command "succeed" by accident.) + +How the command works in each shell: + +- **`cmd.exe` (Windows):** `ver` succeeds, so `||` skips the rest. The quoted part is a single argument, so `cmd.exe` never interprets the `&&`, `>` or `{ }` inside it. Windows does not need the file: the hostname comes from `COMPUTERNAME` via `remoteEnv`. +- **`/bin/sh` (Mac, Linux):** `ver` does not exist (one `ver: command not found` line in the log), so `||` runs the capture, which ends in `true` (exit 0). + +The `Host Commands` workflow (`.github/workflows/host-commands.yml`) checks both, on a `windows-latest` runner through the real devcontainers CLI and with `/bin/sh` on Linux. It also runs a control that confirms the old bash-only command still fails on Windows. The script first removes Git for Windows' Unix tools from PATH, as on a normal PC, because GitHub's runner has them and they hide the bug. **Change this command only with that workflow green.** + Used to capture the host's real hostname, which is not available via `remoteEnv` on macOS (zsh doesn't export `HOSTNAME`). The file `.devcontainer.secrets/env-vars/.host-hostname` is read by `config-host-info.sh` as a fallback when `DEV_HOST_HOSTNAME` is empty. -| Platform | What `hostname -s` returns | -|----------|---------------------------| -| macOS | Machine name (e.g., `MBP-J4G0G066W2`) | -| Linux | Machine hostname (e.g., `terje-desktop`) | -| Windows (WSL2) | WSL hostname | -| Windows (PowerShell) | Falls back to `hostname` without `-s` | +| Platform | Result | +|----------|--------| +| macOS | `.host-hostname` = machine name (e.g., `MBP-J4G0G066W2`) | +| Linux | `.host-hostname` = machine hostname (e.g., `terje-desktop`) | +| Windows | No file written; `config-host-info.sh` uses `DEV_HOST_COMPUTERNAME` instead | The `mkdir -p` ensures the secrets directory exists on fresh installs. The file is in `.devcontainer.secrets/` which is gitignored. diff --git a/website/docs/contributors/architecture/startup-lifecycle.md b/website/docs/contributors/architecture/startup-lifecycle.md index ac36f7c..26371de 100644 --- a/website/docs/contributors/architecture/startup-lifecycle.md +++ b/website/docs/contributors/architecture/startup-lifecycle.md @@ -18,8 +18,8 @@ sequenceDiagram participant Container as Container Note over Host: Stage 1: initializeCommand - Host->>Host: mkdir -p .devcontainer.secrets/env-vars - Host->>Host: hostname -s > .host-hostname + Host->>Host: Mac/Linux: hostname -s > .host-hostname + Host->>Host: Windows: ver (nothing written) Note over Docker,Container: Stage 2: ENTRYPOINT Docker->>Container: Start container (PID 1) @@ -55,26 +55,34 @@ sequenceDiagram **File:** Defined in `devcontainer.json` ```json -"initializeCommand": "mkdir -p .devcontainer.secrets/env-vars && hostname -s > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || hostname > .devcontainer.secrets/env-vars/.host-hostname 2>/dev/null || true" +"initializeCommand": "ver || sh -c \"mkdir -p .devcontainer.secrets/env-vars && { hostname -s 2>/dev/null || hostname; } > .devcontainer.secrets/env-vars/.host-hostname; true\"" ``` Runs on the **host machine** before the container starts. This is the only stage that executes outside the container. +**It must be valid in two shells.** The devcontainers CLI (which VS Code uses) runs a string `initializeCommand` with `cmd.exe /c` on Windows and with `/bin/sh -c` everywhere else (`devcontainers/cli`, `src/spec-node/utils.ts`, `runInitializeCommand`). A non-zero exit stops the container from starting. A bash-only command therefore broke new Windows installs from 2026-04-07 (`733dd73`) until 1.8.2, on every PC without Unix tools on PATH, which is the normal case. (With Git for Windows' Unix tools on PATH, a stray `true.exe` made the old command "succeed" by accident.) + +How the command works in each shell: + +- **`cmd.exe` (Windows):** `ver` succeeds, so `||` skips the rest. The quoted part is a single argument, so `cmd.exe` never interprets the `&&`, `>` or `{ }` inside it. Windows does not need the file: the hostname comes from `COMPUTERNAME` via `remoteEnv`. +- **`/bin/sh` (Mac, Linux):** `ver` does not exist (one `ver: command not found` line in the log), so `||` runs the capture, which ends in `true` (exit 0). + +The `Host Commands` workflow (`.github/workflows/host-commands.yml`) checks both, on a `windows-latest` runner through the real devcontainers CLI and with `/bin/sh` on Linux. It also runs a control that confirms the old bash-only command still fails on Windows. The script first removes Git for Windows' Unix tools from PATH, as on a normal PC, because GitHub's runner has them and they hide the bug. **Change this command only with that workflow green.** + **Purpose:** Capture the host's real hostname. This is needed because: - macOS (zsh) doesn't export `HOSTNAME` as an environment variable - `remoteEnv` can only pass variables that exist — if `HOSTNAME` is empty, `DEV_HOST_HOSTNAME` is empty -- `hostname -s` works on Mac, Linux, and Windows (WSL2) +- Windows does not need it: `COMPUTERNAME` reaches the container through `remoteEnv` **Output:** `.devcontainer.secrets/env-vars/.host-hostname` containing the short hostname (e.g., `MBP-J4G0G066W2`). **Cross-platform behavior:** -| Platform | `hostname -s` returns | -|----------|----------------------| -| macOS | Machine name (e.g., `MBP-J4G0G066W2`) | -| Linux | Machine hostname (e.g., `terje-desktop`) | -| Windows (WSL2) | WSL hostname | -| Windows (PowerShell) | Falls back to `hostname` without `-s` | +| Platform | Result | +|----------|--------| +| macOS | `.host-hostname` = machine name (e.g., `MBP-J4G0G066W2`) | +| Linux | `.host-hostname` = machine hostname (e.g., `terje-desktop`) | +| Windows | No file written; `config-host-info.sh` uses `DEV_HOST_COMPUTERNAME` instead | --- @@ -145,7 +153,7 @@ flowchart TD |--------|----------|------------------| | `DEV_HOST_HOSTNAME` | Linux | `${localEnv:HOSTNAME}` in remoteEnv — bash exports it | | `DEV_HOST_COMPUTERNAME` | Windows | `${localEnv:COMPUTERNAME}` in remoteEnv | -| `.host-hostname` file | Mac, Linux, Windows | `initializeCommand` runs `hostname -s` on host | +| `.host-hostname` file | Mac, Linux | `initializeCommand` runs `hostname -s` on host (skipped on Windows) | | Fallback `devcontainer` | All | When nothing else is available | ### Platform detection logic