diff --git a/.claude/skills/add-devcontainer-feature/SKILL.md b/.claude/skills/add-devcontainer-feature/SKILL.md new file mode 100644 index 0000000..4064c7f --- /dev/null +++ b/.claude/skills/add-devcontainer-feature/SKILL.md @@ -0,0 +1,171 @@ +--- +name: add-devcontainer-feature +description: Scaffold a new DevContainer Feature — manifest, install.sh, README, test — following this repo's conventions +--- + +Add a new DevContainer Feature to `helpers4/devcontainer`, end to end. Ask the user for the +feature's name and purpose first if it wasn't already specified (kebab-case id, e.g. `foo-dev`). + +## 1. Before creating anything + +- Check `src/` for an existing feature that already covers this — `ls src/` for the current + list, don't hardcode or guess it, it drifts. Read `AGENTS.md`'s features table for a + one-line summary of each. +- Check whether an official/community feature already does this (e.g. + `ghcr.io/devcontainers/features/*`, `ghcr.io/devcontainers-extra/features/*`). If one exists + and covers the whole need, prefer depending on it (`installsAfter`) over reimplementing — + see `angular-dev`, which delegates CLI install to + `ghcr.io/devcontainers-extra/features/angular-cli` and only adds extensions/settings on top. + Reimplementing something that already exists needs a real justification (e.g. `github-dev` + reimplements `gh` CLI install because no known feature bundles the CLI *and* the IDE + extension together — that bundling is the point of the feature). +- If this is a **dedicated AI-tool feature** (a new IDE assistant, following `claude-dev`/ + `mistral-dev`/`copilot-dev`), it must cover all three: CLI install (if one exists), IDE + extension, and settings/configuration — not just one or two. + +## 2. Files to create + +```text +src//devcontainer-feature.json +src//install.sh +src//README.md +test//test.sh +``` + +Use `src/git-absorb/` as a reference for a simple binary-install feature, or `src/claude-dev/` +for one that persists host state via a bind-mount + symlink. + +### `devcontainer-feature.json` + +- Key order: `id` before `version` (some older features have this backwards — don't copy them). +- Start new features at `"version": "1.0.0"`. +- **Must be plain JSON — no `//` comments.** `devcontainers/action` parses it with + `JSON.parse`, which chokes on JSONC; this broke the release workflow once already (commit + `4e6e5ee`). Put rationale in the README or `install.sh` instead. +- `installsAfter`: at minimum `["ghcr.io/devcontainers/features/common-utils"]`; add + `ghcr.io/helpers4/devcontainer/helpers4-common` or other helpers4 features here if this one + needs them ordered first. Never reference a feature id that isn't in `src/` (or the official + registry) — it's silently ignored if unresolvable, which hides real typos. +- If this feature needs to persist state from the host (credentials, config), read the + "Design constraints for features" section in `AGENTS.md` **before** adding a `mounts` entry: + - A `mounts` source that doesn't exist on the host — file *or* directory — fails the whole + container at start, unconditionally. No feature script can catch this. + - A Feature-level `initializeCommand` **does not work** — verified against the + devcontainers CLI source; it's silently ignored. The only place `initializeCommand` has + effect is the *consumer's* top-level `devcontainer.json`. So: add the `mounts` + + `postStartCommand` to the feature as usual, but the required `mkdir -p`/`touch` line goes + in the feature's own README "Example Usage" as something the consumer must add — see + `claude-dev`'s or `mistral-dev`'s README for the exact pattern to copy. + +### `install.sh` + +Pattern (copy the bootstrap block verbatim from `src/helpers4-common/install.sh` or any +existing feature — a CI check in `pr-validation.yml`'s `shellcheck` job fails the build if a +copy drifts from the canonical version): + +```bash +#!/usr/bin/env bash + +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# + +set -euo pipefail + +# Bootstrap helpers4 shared library. helpers4-common installs it; if running +# standalone (e.g. devcontainer features test), create it inline so the feature +# is self-contained without a GHCR pull. +if [ ! -f /usr/local/share/helpers4/common.sh ]; then + mkdir -p /usr/local/share/helpers4 + cat > /usr/local/share/helpers4/common.sh << 'H4_COMMON' +# shellcheck shell=bash +h4_detect_user() { ... } # copy the exact body from an existing feature +h4_resolve_home() { ... } +h4_apt_update() { ... } +h4_ensure_packages() { ... } +H4_COMMON +fi +# shellcheck source=/dev/null +. /usr/local/share/helpers4/common.sh + +if [ "$(id -u)" -ne 0 ]; then + echo 'Script must be run as root.' + exit 1 +fi + +h4_detect_user +h4_resolve_home + +trap cleanup EXIT +cleanup() { rm -rf /tmp/-* ; } + +# ... arch detection (x86_64/aarch64), h4_ensure_packages for apt deps, +# download/install to /usr/local/bin/ ... +``` + +Read the actual bootstrap block out of an existing `install.sh` rather than retyping it from +this skill — copy it byte-for-byte so the CI drift check passes. + +### `README.md` + +Follow the shape of an existing feature's README: title, one-paragraph description, "Example +Usage" (with the `initializeCommand` snippet if this feature mounts host state), an options +table, and a "How it works" section. If the feature evolves after this, add a "Version +History" section (see `dotfiles-sync`'s) documenting what changed and why per bump — future +maintainers (and future agents) rely on it to avoid re-deriving decisions from scratch. + +### `test//test.sh` + +```bash +#!/usr/bin/env bash + +# This file is part of helpers4. +# Copyright (C) 2025 baxyz +# SPDX-License-Identifier: LGPL-3.0-or-later + +set -e + +echo "Testing feature..." +# assert the thing the feature installs is present/working; exit 1 on failure +echo "✅ PASS: ..." +``` + +Keep it about **build-time artifacts** (binary present, file generated, config written) — it +can't exercise `postStartCommand`/mount-dependent behavior, since `devcontainer features test` +doesn't wire up bind mounts from a real host. If the feature declares `mounts`, add a +"Create mount sources for ``" step to `pr-validation.yml`'s `test-features` job (see the +existing steps for `claude-dev`/`mistral-dev`/`dotfiles-sync`) so the test job's own container +build doesn't fail on a missing source. + +## 3. Wire it into the repo + +In this order — missing any of these breaks CI or leaves the feature undiscoverable: + +1. `scopes.json` → add `` (commit scope validation reads this file automatically; PR CI + fails without it). +2. `.github/workflows/pr-validation.yml` → add `` to the `test-features` job's matrix + (one entry per base image worth testing against), plus a mount-source step if needed. +3. `.github/workflows/test.yml` → add the same matrix entries (this one runs on push to `main`, + not per-PR). +4. `AGENTS.md` → add a row to the features table (`| \`\` | 1.0.0 | |`). + +## 4. Verify + +```bash +devcontainer features test --features . +``` + +Then, since this is a **new** feature (not yet published), the version-bump-check in +`pr-validation.yml` won't flag it either way — but confirm the manifest still validates as +plain JSON (`jq empty src//devcontainer-feature.json`) and that `bash -n +src//install.sh` / `test//test.sh` come back clean before moving on. Run +`shellcheck -S warning` on both scripts if it's available locally. + +## 5. Commit + +Only if explicitly asked to commit *this turn* — per `.dev/AGENTS.md`, commit authorization is +per-turn, not a standing grant. When authorized: `feat(): ✨ add feature` as the +scope (matches `scopes.json` once step 3.1 above is done), following the commit format in +`AGENTS.md` (`(): `). diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7818436..fe9ef85 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -31,6 +31,66 @@ jobs: echo "status=failure" >> $GITHUB_OUTPUT fi + version-bump-check: + runs-on: ubuntu-latest + outputs: + status: ${{ steps.status.outputs.status }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Check touched features bumped their version + id: check + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + + MERGE_BASE=$(git merge-base "${BASE_SHA}" HEAD) + echo "::notice::Diff base = ${MERGE_BASE} → HEAD ($(git rev-parse HEAD))" + + mapfile -t names < <(git diff --name-only "${MERGE_BASE}..HEAD" -- src/ \ + | awk -F/ '{print $2}' | sort -u) + + FAILED=0 + for name in "${names[@]}"; do + manifest="src/${name}/devcontainer-feature.json" + [ -f "$manifest" ] || continue # feature removed on this branch + + new_version=$(jq -r '.version' "$manifest") + old_version=$(git show "${MERGE_BASE}:${manifest}" 2>/dev/null | jq -r '.version' 2>/dev/null || echo "") + + if [ -z "$old_version" ]; then + echo "✅ ${name}: new feature, no bump required" + elif [ "$old_version" = "$new_version" ]; then + echo "❌ ${name}: touched but version unchanged (${new_version})" + FAILED=1 + else + echo "✅ ${name}: ${old_version} → ${new_version}" + fi + done + + if [ "$FAILED" -eq 1 ]; then + echo "" + echo "One or more touched features didn't bump 'version' in devcontainer-feature.json." + echo "release.yml only publishes a feature whose version changed — bump it (patch by" + echo "default), once for the whole branch. See AGENTS.md 'Modifying an existing" + echo "feature — version bump'." + fi + exit "$FAILED" + + - name: Set status + id: status + if: always() + run: | + if [ "${{ job.status }}" == "success" ]; then + echo "status=success" >> $GITHUB_OUTPUT + else + echo "status=failure" >> $GITHUB_OUTPUT + fi + test-features: runs-on: ubuntu-latest strategy: @@ -175,7 +235,7 @@ jobs: pr-comment: runs-on: ubuntu-latest - needs: [conventional-commits, test-features, shellcheck] + needs: [conventional-commits, version-bump-check, test-features, shellcheck] if: always() steps: - name: Update PR comment @@ -185,6 +245,7 @@ jobs: script: | const jobs = { '🧾 Conventional Commits': "${{ needs.conventional-commits.outputs.status || 'unknown' }}", + '🔖 Version Bump': "${{ needs.version-bump-check.outputs.status || 'unknown' }}", '🧪 Feature Tests': "${{ needs.test-features.result || 'unknown' }}", '🐚 ShellCheck': "${{ needs.shellcheck.outputs.status || 'unknown' }}", }; diff --git a/AGENTS.md b/AGENTS.md index 406f43f..0fc735a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,17 +32,17 @@ devcontainer features test . | `github-dev` | 1.0.3 | gh CLI, Copilot Chat, PR/Issues/Actions extensions | | `copilot-dev` | 1.0.1 | Copilot Chat + AI instructions (commits, PRs, code review) | | `claude-dev` | 1.0.4 | Claude Code extension + `~/.claude` bind-mount (credentials + memory persist) | -| `mistral-dev` | 1.0.1 | Mistral Vibe extension + `~/.vibe` bind-mount | +| `mistral-dev` | 1.0.2 | Mistral Vibe extension + `~/.vibe` bind-mount | | `typescript-dev` | 1.0.5 | TS/JS dev, import management (dependsOn essential-dev) | -| `angular-dev` | 1.0.2 | Angular dev, port 4200 | +| `angular-dev` | 1.0.6 | Angular dev, port 4200 | | `vite-plus` | 1.0.3 | vp CLI, Oxlint/Oxfmt, Vitest | -| `package-auto-install` | — | Auto-detect and install packages | +| `package-auto-install` | 1.0.7 | Auto-detect and install packages | | `pnpm-store` | 1.0.4 | Shared pnpm store via Docker named volume (dependsOn helpers4-common) | | `auto-header` | — | LGPL-3.0 license headers | -| `git-absorb` | 1.0.2 | git-absorb from GitHub releases | -| `dotfiles-sync` | 1.0.2 | Sync Git/SSH/GPG/npm/gh config from host | +| `git-absorb` | 1.0.7 | git-absorb from GitHub releases | +| `dotfiles-sync` | 1.0.7 | Sync Git/SSH/GPG/npm/gh config from host | | `peon-ping` | 1.0.3 | AI agent sound notifications | -| `shell-history-per-project` | 1.0.2 | Persistent shell history (zsh/bash/fish) | +| `shell-history-per-project` | 1.0.7 | Persistent shell history (zsh/bash/fish) | **Adding a new feature — checklist:** @@ -52,6 +52,24 @@ devcontainer features test . 4. `.github/workflows/pr-validation.yml` + `test.yml` → add to test matrix 5. This `AGENTS.md` features table +**Modifying an existing feature — version bump:** + +Any change under `src//` (`install.sh`, `devcontainer-feature.json`, +`README.md`, …) must bump that feature's `version` field (patch by default, +minor/major when warranted) — `release.yml` only tags and publishes a feature +whose `version` changed between the base branch and HEAD, so an unbumped +feature change silently never ships. Bump it **once per branch**: if the +version on the branch already differs from `main`'s, a further commit on that +same branch/PR must *not* bump it again — check the diff against `main` +first, don't bump reflexively on every commit. + +Enforced by the `version-bump-check` job in `pr-validation.yml`: it fails the +PR if a touched feature's `version` is unchanged from `main`. It's a blocking +check only — it never commits a bump on your behalf (deliberately: no bot +commits, no push-permission/fork edge cases, consistent with how +`conventional-commits` already works in this repo). Bump the version yourself +and push again. + **License header (all scripts):** ```bash @@ -59,3 +77,52 @@ devcontainer features test . # Copyright (C) 2025 baxyz # SPDX-License-Identifier: LGPL-3.0-or-later ``` + +## Design constraints for features + +These are hard requirements, not style preferences — violating them breaks the +container for users, sometimes silently. + +- **A `mounts` entry fails the whole container if the host source doesn't + exist — file *or* directory, no exceptions.** Verified against + `@devcontainers/cli` directly (decompiled `devContainersSpecCLI.js`): + `docker run --mount type=bind,...` is used, which errors out + (`bind source path does not exist`) instead of silently creating anything. + No feature script can catch this — mounts resolve before `install.sh` / + `postCreateCommand` / `postStartCommand` ever run. +- **A Feature's own `initializeCommand` cannot fix this — it's silently + ignored.** Also verified in the CLI source: the lifecycle-command merge + step only collects `onCreateCommand`, `updateContentCommand`, + `postCreateCommand`, `postStartCommand`, `postAttachCommand` from each + Feature (`lv` in the minified bundle) plus `mounts`/`customizations`/etc. + (`xV`) — `initializeCommand` is *not* in either list. It only has effect in + the **consumer's own top-level `devcontainer.json`**. This was tried on + `claude-dev` (v1.0.3, since reverted as dead code) before the real fix + landed: document a required `initializeCommand` in the feature's README + Example Usage (see `claude-dev`'s and `mistral-dev`'s READMEs) — a Feature + cannot inject into the consumer's `devcontainer.json`, so this is the only + place it can actually run. +- **"Out-of-the-box" for a mount-dependent feature means "one documented + `initializeCommand` line away," not zero-config.** Given the constraint + above, don't chase a fully silent fix for a feature with a hard mount + dependency — document the required line prominently instead. If a feature + can tolerate a missing/empty source at the file level (many small files, + like `dotfiles-sync`), prefer staging into `/mnt/h4dotfiles`-style path and + merging at `postStartCommand` over a hard mount dependency — but note that + even staged mounts still fail hard if *their* source is missing, so staging + only helps once the source is guaranteed to exist (see `dotfiles-sync`'s + own `initializeCommand` requirement for exactly the files it can't avoid + mounting). +- **`devcontainer-feature.json` must be plain JSON — no `//` comments.** + `devcontainers/action` parses it with `JSON.parse`, which chokes on JSONC. + This broke the release workflow once already (see commit `4e6e5ee`). Put + rationale in the README or a neighboring `.sh` file instead. +- **Must work inside a VS Code multi-root `.code-workspace`** — this repo's own + devcontainer bundles 6 sibling repos this way (see the root `.dev/CLAUDE.md` + workspace layout). Treat this as a standard case, not an edge case. +- **Dedicated AI-tool features must cover three things**: CLI install, IDE + extension, and settings/configuration — see `claude-dev`, `mistral-dev`, + `copilot-dev`. `github-dev` intentionally reimplements `gh` CLI install + rather than depending on an upstream feature, because no known existing + devcontainer feature bundles the CLI *and* the IDE extension together — that + bundling is the actual reason this feature exists. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3b72ce1 --- /dev/null +++ b/TODO.md @@ -0,0 +1,95 @@ +# TODO — devcontainer feature audit + +Plan for two follow-up axes from a feature-suite audit (relevance, host-sync +efficiency, cross-feature coherence). Relevance findings were resolved inline +during the audit; this file tracks the remaining two axes as actionable items. + +See `AGENTS.md` → "Design constraints for features" for the invariants this +plan builds on (no direct `mounts` on a host path that might not exist, +out-of-the-box requirement, multi-root workspace support, AI-feature pattern). + +## 1. Host sync efficiency + +- [x] **`mistral-dev` was non-functional.** Fixed: added `mounts` + + `postStartCommand` to `devcontainer-feature.json` (v1.0.2). Turns out a + Feature-level `initializeCommand` (the originally-planned fix) is **silently + ignored** by the devcontainers CLI — verified by decompiling + `@devcontainers/cli`'s `devContainersSpecCLI.js`: the lifecycle-merge step + only collects `initializeCommand` from the top-level `devcontainer.json`, + never from a Feature. The real fix is a documented, required + `initializeCommand` line in the consumer's own `devcontainer.json` — added to + mistral-dev's README "Example Usage", mirroring the pattern `claude-dev` + already used correctly. See AGENTS.md "Design constraints for features". +- [x] **`claude-dev`'s direct mount of `~/.claude` — checked, no fix needed.** + Same crash risk is real (confirmed empirically with `docker --mount + type=bind`: hard failure, both for missing files and missing directories), + but `claude-dev` already documents the required consumer-side + `initializeCommand` correctly in its README (this was in fact tried as a + Feature-level field in v1.0.3 and reverted in a later commit as dead code — + the removal was correct, not a regression as originally suspected here). No + change made. +- [x] **`dotfiles-sync`: mandatory file mounts can crash startup.** Fixed by + documenting a comprehensive `initializeCommand` in the README covering every + mount source (mandatory *and* opt-in), not just the two originally flagged + (`~/.npmrc`, `~/.yarnrc.yml`) — same underlying constraint as above (v1.0.7). +- [x] **`dotfiles-sync` username resolution — investigated, not actually a + divergence.** `claude-dev`/`mistral-dev`'s `username` option turned out to be + dead code too: their `install.sh` never reads `_BUILD_ARG_USERNAME`, so + `h4_detect_user`'s auto-detection runs unconditionally and the option has no + effect either way. `dotfiles-sync`'s simpler `_BUILD_ARG_USERNAME` → getent + → `"node"` fallback is actually the *more* correct behavior of the three (it's + the only one where setting the option does anything), and is already + documented ("Match this to your `remoteUser`"). No change made here — but see + the new follow-up below, this surfaced a real bug elsewhere. +- [ ] **New: `claude-dev` and `mistral-dev`'s `username` option is dead code.** + Neither `install.sh` reads `_BUILD_ARG_USERNAME` — `h4_detect_user` runs its + own auto-detection regardless of what a consumer sets via the `username` + option. Either wire the option's value into `h4_detect_user`'s precedence + (consistent with `dotfiles-sync`), or remove the option from both schemas if + auto-detection is meant to always win. Needs its own pass — touches 2 + features + the shared bootstrap block copied in 8 files, not done in this + pass to avoid scope creep. + +## 2. Cross-feature coherence + +- [x] **Shared bootstrap duplication — already covered, no new code needed.** + `pr-validation.yml`'s `shellcheck` job already has a "Verify bootstrap copies + match helpers4-common canonical" step that diffs every `install.sh` copy of + the `h4_detect_user`/`h4_resolve_home`/`h4_ensure_packages` block against + `helpers4-common`'s canonical version and fails the job on drift. This + predates this audit; re-verified it actually catches drift (tested locally). +- [x] **License header format drift.** Fixed: `angular-dev`, `git-absorb`, + `shell-history-per-project` now use the canonical 3-line header (bumped + 1.0.6, 1.0.7, 1.0.7 respectively). +- [x] **Stale reference to a removed feature.** Fixed: `package-auto-install`'s + `installsAfter` now points at `dotfiles-sync` instead of the removed + `local-mounts` (v1.0.7). +- [x] **JSON key ordering.** Fixed: `git-absorb` and `shell-history-per-project` + now put `id` before `version`, matching every other feature (same commits as + the header fix above). + +## 3. Process / tooling + +- [x] **Bump feature version on change, once per branch — blocking check.** + Implemented as a `version-bump-check` job in `pr-validation.yml` (same + pattern as `conventional-commits`/`shellcheck`: sets `outputs.status`, feeds + the `pr-comment` summary table). For each feature touched by the PR, diffs + `version` between the PR's merge-base and HEAD; **fails the job** if it's + unchanged. No bot commits — a first attempt at auto-bumping-and-pushing was + dropped as unnecessarily complex (push permissions, fork PRs can't be + pushed to, bot-commit noise) in favor of a plain blocking check, consistent + with how `conventional-commits` already works here. The "once per branch" + behavior falls out of the same diff-against-base logic: once bumped, the + check passes for every subsequent push to that branch without re-bumping. + +## 4. Tooling for contributors + +- [x] **`/add-devcontainer-feature` Claude skill.** Added + `.claude/skills/add-devcontainer-feature/SKILL.md`, mirroring + `helpers4/typescript`'s `/add-helper` skill: scaffolds a new feature end to + end following the AGENTS.md checklist (manifest, install.sh, README, test, + `scopes.json`, workflow matrices, features table) and bakes in the design + constraints above (mounts, `initializeCommand` placement, JSON-only + manifests, version bump). Wired into the workspace via + `.dev/.claude/settings.json`'s `additionalDirectories`, same mechanism + already used for the typescript repo's skills directory. diff --git a/src/angular-dev/devcontainer-feature.json b/src/angular-dev/devcontainer-feature.json index 84fe105..f3798e8 100644 --- a/src/angular-dev/devcontainer-feature.json +++ b/src/angular-dev/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "angular-dev", - "version": "1.0.5", + "version": "1.0.6", "name": "Angular Development Environment", "description": "Angular-specific development environment with port forwarding, VS Code extensions, and CLI autocompletion.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/angular-dev", diff --git a/src/angular-dev/install.sh b/src/angular-dev/install.sh index 75bb942..6169981 100644 --- a/src/angular-dev/install.sh +++ b/src/angular-dev/install.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Angular Development Environment DevContainer Feature +# This file is part of helpers4. # Copyright (C) 2025 baxyz -# Licensed under LGPL-3.0 - see LICENSE file for details +# SPDX-License-Identifier: LGPL-3.0-or-later # # Configures Angular development environment with CLI autocompletion diff --git a/src/dotfiles-sync/README.md b/src/dotfiles-sync/README.md index 42ea43b..4c22b01 100644 --- a/src/dotfiles-sync/README.md +++ b/src/dotfiles-sync/README.md @@ -12,7 +12,26 @@ Syncs local Git, SSH, GPG, npm, and yarn config files into the devcontainer. Opt } ``` -That's it. The feature auto-detects the environment and adapts its behavior. +The feature auto-detects the environment and adapts its behavior. **Add +`initializeCommand`** so every bind-mount source is guaranteed to exist before +Docker resolves the mounts — without it, a container can fail to start on a +machine that's simply never created one of these files (see "Operational +note" below): + +```json +{ + "initializeCommand": "mkdir -p ~/.ssh ~/.gnupg ~/.config/git ~/.aws ~/.kube ~/.docker && touch ~/.gitconfig ~/.npmrc ~/.yarnrc.yml ~/.aws/config ~/.kube/config ~/.docker/config.json", + "features": { + "ghcr.io/helpers4/devcontainer/dotfiles-sync:1": {} + } +} +``` + +This can't be baked into the feature itself — a Feature's own +`initializeCommand` field is silently ignored by the devcontainers CLI, only +the consumer's top-level `devcontainer.json` is honored for it (see AGENTS.md +"Design constraints for features"). `mkdir -p`/`touch` on paths that already +exist are no-ops, so this is always safe to add. ### With custom username @@ -56,9 +75,13 @@ That's it. The feature auto-detects the environment and adapts its behavior. **Operational note — bind-mounts are unconditional:** DevContainer Feature `mounts` cannot be gated on option values. The files below are always bind-mounted into `/mnt/h4dotfiles` at container start, regardless of the option value. The option only controls whether `sync-files.sh` copies the staged file into `$HOME`. Consequences: -- **Startup failure risk** — Docker file bind-mounts fail hard if the source path does not exist on the host. If you don't have `~/.aws/config`, `~/.kube/config`, or `~/.docker/config.json`, the container will fail to start even if the corresponding option is `false`. Create the file (it can be empty) to unblock startup. +- **Startup failure risk** — Docker file bind-mounts fail hard if the source path does not exist on the host. If you don't have `~/.aws/config`, `~/.kube/config`, or `~/.docker/config.json`, the container will fail to start even if the corresponding option is `false`. The `initializeCommand` in "Usage" above pre-creates all of them (empty file, `touch`) so this can't happen — that's the supported fix, not creating the file by hand once and hoping it survives. - **Data visible in staging** — even when the option is `false`, the host file is accessible inside the container at `/mnt/h4dotfiles/`. Nothing reads that path unless the option is enabled, but if your threat model requires full isolation, do not use the feature for that credential. +The same startup-failure risk applies to the "always synced" files above +(`~/.npmrc`, `~/.yarnrc.yml` in particular are frequently absent on a fresh +machine) — the `initializeCommand` in "Usage" covers those too. + | Local Path | Option | Notes | |------------|--------|-------| | `~/.aws/config` | `syncAwsConfig` | AWS profiles. `~/.aws/credentials` (long-lived access keys) is **not bind-mounted** and never synced. | @@ -244,6 +267,7 @@ ssh-add -l ## Version History +- **v1.0.7**: Documented the required `initializeCommand` in "Usage" (pre-creates every bind-mount source, mandatory and opt-in) so a container can't fail to start on a machine missing one of these files — a Feature's own `initializeCommand` is silently ignored by the devcontainers CLI, so this has to live in the consumer's `devcontainer.json`, not the feature. No behavior change to `sync-files.sh`. - **v1.0.4**: Removed bind-mounts for files that are frequently absent on host machines and have little value inside a devcontainer: `~/.gitignore_global` (redundant with `~/.config/git/` directory mount), `~/.config/pnpm/rc` (pnpm store-dir is counter-productive in a container), `~/.config/gh/config.yml` and `~/.config/gh/hosts.yml` (gh CLI auth managed separately), `~/.cargo/config.toml` (cargo not relevant in most containers), `~/.config/pip/pip.conf` (too environment-specific). Docker file bind-mounts fail hard if the source path doesn't exist on the host, which was causing containers to fail to start. The `syncGhAuth` option is removed. - **v1.0.3**: Fixed incompatibility with `docker-in-docker` feature — staging directory moved from `/tmp/dotfiles-sync/` to `/mnt/h4dotfiles/` to avoid being hidden by the tmpfs that `docker-in-docker` mounts on `/tmp` at container start. - **v1.0.2**: Added `syncGhAuth` opt-in to copy `~/.config/gh/hosts.yml` (GitHub OAuth token used by `gh` CLI) into `$HOME`. Default `false`, skipped on cloud environments. The file is bind-mounted into `/tmp/dotfiles-sync/` regardless (Feature `mounts` cannot be conditional) but only copied to `$HOME` when the option is enabled. For fine-grained PATs prefer the `github-dev` feature with `GH_TOKEN`. diff --git a/src/dotfiles-sync/devcontainer-feature.json b/src/dotfiles-sync/devcontainer-feature.json index e24de34..bb730ec 100644 --- a/src/dotfiles-sync/devcontainer-feature.json +++ b/src/dotfiles-sync/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "dotfiles-sync", - "version": "1.0.6", + "version": "1.0.7", "name": "Dotfiles Sync", "description": "Syncs local Git, SSH, GPG, npm, yarn config files into the devcontainer. Optionally syncs cloud credentials (AWS, kube, Docker) — opt-in only. Works on macOS, Linux, Windows (WSL), Codespaces, Gitpod, DevPod. Merges instead of overwriting.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/dotfiles-sync", diff --git a/src/git-absorb/devcontainer-feature.json b/src/git-absorb/devcontainer-feature.json index 95537ce..7150b7b 100644 --- a/src/git-absorb/devcontainer-feature.json +++ b/src/git-absorb/devcontainer-feature.json @@ -1,6 +1,6 @@ { - "version": "1.0.6", "id": "git-absorb", + "version": "1.0.7", "name": "git-absorb", "description": "Installs git-absorb, a tool to automatically absorb staged changes into their logical commits. Like 'git commit --fixup' but automatic.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/git-absorb", diff --git a/src/git-absorb/install.sh b/src/git-absorb/install.sh index c1781d9..8f076a1 100755 --- a/src/git-absorb/install.sh +++ b/src/git-absorb/install.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# git-absorb DevContainer Feature +# This file is part of helpers4. # Copyright (C) 2025 baxyz -# Licensed under LGPL-3.0 - see LICENSE file for details +# SPDX-License-Identifier: LGPL-3.0-or-later # # Installs git-absorb: automatically absorb staged changes into their logical commits diff --git a/src/mistral-dev/README.md b/src/mistral-dev/README.md index d4f710d..585c0bb 100644 --- a/src/mistral-dev/README.md +++ b/src/mistral-dev/README.md @@ -16,6 +16,18 @@ including `--no-cache`. } ``` +Add `initializeCommand` to your `devcontainer.json` so the host directory is +guaranteed to exist before Docker tries to bind-mount it: + +```jsonc +{ + "initializeCommand": "mkdir -p ~/.vibe", + "features": { + "ghcr.io/helpers4/devcontainer/mistral-dev:1": {} + } +} +``` + With the optional CLI: ```jsonc @@ -50,16 +62,27 @@ The feature declares the `mistralai.mistral-vibe-code` extension via the installs it automatically. ### Credential persistence -At build time, `install.sh` generates `/usr/local/share/mistral-dev/setup-credentials.sh` -with the target user's home directory baked in. At every container start -(`postStartCommand`), that script replaces `~/.vibe` with a symlink pointing to -the host bind-mounted path `/mnt/h4vibe` (sourced from `~/.vibe` on the host). +1. **Build time** (`install.sh`): generates `/usr/local/share/mistral-dev/setup-credentials.sh` + with the target user's home path baked in. +2. **Mount** (`devcontainer-feature.json → mounts`): bind-mounts `$HOME/.vibe` from + the host to `/mnt/h4vibe` inside the container. +3. **Every start** (`postStartCommand`): `setup-credentials.sh` replaces `~/.vibe` + with a symlink to `/mnt/h4vibe` — credentials and config survive rebuilds. This means: - Credentials survive container rebuilds (including `--no-cache`). - First-time auth inside the container writes back to the host automatically. - `VIBE_HOME` is not required — the symlink is transparent to Mistral Vibe. +Docker refuses to start the container if a bind-mount source has never existed +on the host (fresh machine, first Vibe login). A Feature's `devcontainer-feature.json` +can't fix this itself — `initializeCommand` set there is silently ignored by the +devcontainers CLI, only the consumer's top-level `devcontainer.json` is honored for +it — which is why `initializeCommand` above is required in your own config rather +than bundled into the feature. If `/mnt/h4vibe` is not mounted (e.g. missing +`initializeCommand`, standalone test), the script errors out — check that +`initializeCommand` is present in your `devcontainer.json`. + ### CLI (optional) When `installCli: true`, the `vibe` command is installed at build time via `uv` (preferred) or `pip`. See [Mistral Vibe CLI docs](https://docs.mistral.ai/vibe/code/cli/install-setup). diff --git a/src/mistral-dev/devcontainer-feature.json b/src/mistral-dev/devcontainer-feature.json index fbcf1e9..324da17 100644 --- a/src/mistral-dev/devcontainer-feature.json +++ b/src/mistral-dev/devcontainer-feature.json @@ -1,8 +1,8 @@ { "id": "mistral-dev", - "version": "1.0.1", + "version": "1.0.2", "name": "Mistral Vibe Development Environment", - "description": "Installs the Mistral Vibe IDE extension (mistralai.mistral-vibe-code) for VS Code and Cursor. Mounts ~/.vibe from the host via a directory symlink — credentials and config persist across all rebuilds.", + "description": "Installs the Mistral Vibe IDE extension (mistralai.mistral-vibe-code) for VS Code and Cursor. Bind-mounts ~/.vibe from the host and symlinks it at container start — credentials and config persist across all rebuilds.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/mistral-dev", "licenseURL": "https://github.com/helpers4/devcontainer/blob/main/LICENSE", "keywords": [ @@ -25,6 +25,14 @@ } }, + "mounts": [ + { + "source": "${localEnv:HOME}/.vibe", + "target": "/mnt/h4vibe", + "type": "bind" + } + ], + "postStartCommand": "/usr/local/share/mistral-dev/setup-credentials.sh", "customizations": { "vscode": { "extensions": [ diff --git a/src/package-auto-install/devcontainer-feature.json b/src/package-auto-install/devcontainer-feature.json index 55646da..6048555 100644 --- a/src/package-auto-install/devcontainer-feature.json +++ b/src/package-auto-install/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "package-auto-install", - "version": "1.0.6", + "version": "1.0.7", "name": "Automatic Package Installation", "description": "Automatically detects and runs npm/yarn/pnpm install in non-interactive mode after container creation.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/package-auto-install", @@ -63,6 +63,6 @@ "ghcr.io/devcontainers/features/node", "ghcr.io/helpers4/devcontainer/angular-dev", "ghcr.io/helpers4/devcontainer/vite-plus", - "ghcr.io/helpers4/devcontainer/local-mounts" + "ghcr.io/helpers4/devcontainer/dotfiles-sync" ] } diff --git a/src/shell-history-per-project/devcontainer-feature.json b/src/shell-history-per-project/devcontainer-feature.json index f29af3b..453f140 100644 --- a/src/shell-history-per-project/devcontainer-feature.json +++ b/src/shell-history-per-project/devcontainer-feature.json @@ -1,6 +1,6 @@ { - "version": "1.0.6", "id": "shell-history-per-project", + "version": "1.0.7", "name": "Shell History Per Project", "description": "Persist shell history per project by automatically detecting and configuring all available shells (zsh, bash, fish). Supports auto-detection or manual shell selection.", "documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/shell-history-per-project", diff --git a/src/shell-history-per-project/install.sh b/src/shell-history-per-project/install.sh index 9b37628..1b3ebe5 100755 --- a/src/shell-history-per-project/install.sh +++ b/src/shell-history-per-project/install.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -# Shell History Per Project DevContainer Feature +# This file is part of helpers4. # Copyright (C) 2025 baxyz -# Licensed under LGPL-3.0 - see LICENSE file for details +# SPDX-License-Identifier: LGPL-3.0-or-later +# +# Shell History Per Project DevContainer Feature set -euo pipefail