Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,24 @@ release:

changelog:
sort: asc
use: github
use: git
groups:
- title: Features
- title: "Features"
regexp: '^.*?feat(\(.+\))??!?:.+$'
order: 0
- title: Bug Fixes
- title: "Bug Fixes"
regexp: '^.*?fix(\(.+\))??!?:.+$'
order: 1
- title: Other
- title: "Chores"
regexp: '^.*?chore(\(.+\))??!?:.+$'
order: 2
- title: "Tests"
regexp: '^.*?test(\(.+\))??!?:.+$'
order: 3
- title: "Other"
order: 999
filters:
exclude:
- '^chore'
- '^test'
- '^docs'
- 'Merge pull request'
- 'Merge branch'

Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.24.1
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ image_tag = "latest-go"
- [Playwright](https://playwright.dev/) — browser automation and testing

### Version Management
- **asdf** — Node.js, Python, Go, Ruby, Terraform, OpenTofu
- **mise** — Node.js, Go, Terraform, OpenTofu
- **Nix** — package manager with flakes support

### Infrastructure
Expand Down Expand Up @@ -150,8 +150,8 @@ The image is built from composable Nix home-manager modules (`nixhome/modules/`)
| `shell.nix` | zsh + Starship prompt, autosuggestions, syntax highlighting |
| `base.nix` | Universal utilities (git, ripgrep, tmux, jq, fonts) |
| `build.nix` | Native build toolchain (clang, cmake, lld) |
| `go.nix` | Go runtime (asdf) + tooling (golangci-lint, gopls) |
| `node.nix` | Node.js runtime (asdf) + yarn |
| `go.nix` | Go runtime (mise) + tooling (golangci-lint, gopls) |
| `node.nix` | Node.js runtime (mise) + yarn |
| `python.nix` | Python3 + uv package manager |
| `infra.nix` | Terraform, OpenTofu, packer |
| `web.nix` | Chromium + Hugo + Playwright |
Expand Down
9 changes: 9 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package main

import (
"fmt"
"os"

"github.com/DimmKirr/devcell/internal/config"
"github.com/DimmKirr/devcell/internal/scaffold"
"github.com/spf13/cobra"
)

Expand All @@ -26,6 +28,13 @@ func runBuild(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("load config: %w", err)
}

// Sync local nixhome into build context when DEVCELL_NIXHOME_PATH is set.
if nixhomePath := os.Getenv("DEVCELL_NIXHOME_PATH"); nixhomePath != "" {
if err := scaffold.SyncNixhome(nixhomePath, c.ConfigDir); err != nil {
return fmt.Errorf("sync nixhome: %w", err)
}
}

if err := buildImageWithSpinner(c.ConfigDir, noCache, "Building devcell image", false); err != nil {
return err
}
Expand Down
5 changes: 4 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var initCmd = &cobra.Command{
func init() {
initCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts and proceed with defaults")
initCmd.Flags().Bool("macos", false, "Set up a macOS VM box via UTM + Vagrant")
initCmd.Flags().Bool("force", false, "Overwrite existing Dockerfile, flake.nix, and devcell.toml")
}

func runInit(cmd *cobra.Command, _ []string) error {
Expand All @@ -30,6 +31,7 @@ func runInit(cmd *cobra.Command, _ []string) error {
}
applyOutputFlags()
yes, _ := cmd.Flags().GetBool("yes")
force, _ := cmd.Flags().GetBool("force")

// Override base image tag for scaffold Dockerfile if --base-image is set.
if bi, _ := cmd.Flags().GetString("base-image"); bi != "" {
Expand All @@ -46,7 +48,8 @@ func runInit(cmd *cobra.Command, _ []string) error {
modelsSnippet := detectOllamaModels()

fmt.Printf(" Scaffolding %s\n", c.ConfigDir)
if err := scaffold.Scaffold(c.ConfigDir, modelsSnippet); err != nil {
nixhomePath := os.Getenv("DEVCELL_NIXHOME_PATH")
if err := scaffold.Scaffold(c.ConfigDir, modelsSnippet, nixhomePath, force); err != nil {
return fmt.Errorf("scaffold: %w", err)
}
fmt.Printf(" Config dir ready: %s\n", c.ConfigDir)
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func runAgent(binary string, defaultFlags, userArgs []string, extraEnv map[strin
// First-run: scaffold if devcell.toml absent
if !scaffold.IsInitialized(c.ConfigDir) {
fmt.Printf(" First run — scaffolding %s\n", c.ConfigDir)
if err := scaffold.Scaffold(c.ConfigDir, ""); err != nil {
if err := scaffold.Scaffold(c.ConfigDir, "", "", false); err != nil {
return fmt.Errorf("scaffold: %w", err)
}
ok, promptErr := ux.GetConfirmation("Build image now? (~5 min first time)")
Expand Down
48 changes: 26 additions & 22 deletions docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ target "base" {
"${REGISTRY}:${VERSION}-base",
"${REGISTRY}:${VERSION}",
]
cache-from = ["type=gha,scope=base"]
cache-to = ["type=gha,mode=max,scope=base"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-base"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-base,mode=max"]
}

target "go" {
Expand All @@ -74,8 +74,8 @@ target "go" {
target = "go"
platforms = split(",", PLATFORMS)
tags = ["${REGISTRY}:${VERSION}-go"]
cache-from = ["type=gha,scope=go"]
cache-to = ["type=gha,mode=max,scope=go"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-go"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-go,mode=max"]
}

target "node" {
Expand All @@ -85,8 +85,8 @@ target "node" {
target = "node"
platforms = split(",", PLATFORMS)
tags = ["${REGISTRY}:${VERSION}-node"]
cache-from = ["type=gha,scope=node"]
cache-to = ["type=gha,mode=max,scope=node"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-node"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-node,mode=max"]
}

target "python" {
Expand All @@ -96,8 +96,8 @@ target "python" {
target = "python"
platforms = split(",", PLATFORMS)
tags = ["${REGISTRY}:${VERSION}-python"]
cache-from = ["type=gha,scope=python"]
cache-to = ["type=gha,mode=max,scope=python"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-python"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-python,mode=max"]
}

target "electronics" {
Expand All @@ -107,8 +107,8 @@ target "electronics" {
target = "electronics"
platforms = split(",", PLATFORMS)
tags = ["${REGISTRY}:${VERSION}-electronics"]
cache-from = ["type=gha,scope=electronics"]
cache-to = ["type=gha,mode=max,scope=electronics"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-electronics"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-electronics,mode=max"]
}

# fullstack — all language tools (backward-compatible tag: latest-fullstack)
Expand All @@ -121,8 +121,8 @@ target "fullstack" {
tags = [
"${REGISTRY}:${VERSION}-fullstack",
]
cache-from = ["type=gha,scope=fullstack"]
cache-to = ["type=gha,mode=max,scope=fullstack"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-fullstack"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-fullstack,mode=max"]
}

# ultimate — fullstack + desktop + KiCad, ngspice, libspnav, poppler
Expand All @@ -135,8 +135,8 @@ target "ultimate" {
tags = [
"${REGISTRY}:${VERSION}-ultimate",
]
cache-from = ["type=gha,scope=ultimate"]
cache-to = ["type=gha,mode=max,scope=ultimate"]
cache-from = ["type=registry,ref=${REGISTRY}:cache-ultimate"]
cache-to = ["type=registry,ref=${REGISTRY}:cache-ultimate,mode=max"]
}

# ── Groups ────────────────────────────────────────────────────────────────────
Expand All @@ -158,18 +158,22 @@ group "release" {

# local-base: base tagged for local scaffold Dockerfile use (FROM ghcr.io/dimmkirr/devcell:base-local)
target "local-base" {
inherits = ["base"]
tags = ["ghcr.io/dimmkirr/devcell:base-local"]
platforms = []
pull = false
inherits = ["base"]
tags = ["ghcr.io/dimmkirr/devcell:base-local"]
platforms = []
pull = false
cache-from = []
cache-to = []
}

# local-ultimate: ultimate profile for local testing (uses local nixhome/)
target "local-ultimate" {
inherits = ["ultimate"]
tags = ["ghcr.io/dimmkirr/devcell:ultimate-local"]
platforms = []
pull = false
inherits = ["ultimate"]
tags = ["ghcr.io/dimmkirr/devcell:ultimate-local"]
platforms = []
pull = false
cache-from = []
cache-to = []
}

# local: load into local Docker daemon (no push, no multi-arch)
Expand Down
41 changes: 32 additions & 9 deletions images/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ RUN \
chown -R ${USER_UID}:${USER_GID} /opt/devcell && \
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

RUN mkdir -p /config /data /opt/asdf /opt/npm-tools /opt/python-tools \
/opt/devcell/.config/home-manager && \
chown -R ${USER_UID}:${USER_GID} /config /data /opt/asdf /opt/npm-tools /opt/python-tools \
RUN mkdir -p /config /data /opt/mise /opt/npm-tools /opt/python-tools \
/opt/devcell/.config/devcell && \
chown -R ${USER_UID}:${USER_GID} /config /data /opt/mise /opt/npm-tools /opt/python-tools \
/opt/devcell/.config

# System-level nix.conf (read by any nix binary regardless of user).
Expand Down Expand Up @@ -88,8 +88,12 @@ RUN nix profile install "github:NixOS/nixpkgs/${NIXPKGS_REV}#home-manager" && \
ln -sfT "$(readlink -f $HOME/.nix-profile)" \
/nix/var/nix/profiles/per-user/devcell/profile

ENV ASDF_DATA_DIR=/opt/asdf
ENV PATH="/opt/asdf/shims:/nix/var/nix/profiles/per-user/devcell/profile/bin:${PATH}"
ENV MISE_DATA_DIR=/opt/mise
# Mise installs go to /opt/mise/installs/<tool>/<ver>/bin. Child stages create
# stable symlinks (/opt/mise/<tool> → installs/<tool>/<ver>) and append to PATH
# so that tools are available without shims (shims need `mise exec` resolution
# which fails in plain /bin/sh RUN steps).
ENV PATH="/nix/var/nix/profiles/per-user/devcell/profile/bin:${PATH}"

# Entrypoint last — changes here don't bust the nix/home-manager cache above
COPY --chmod=755 images/entrypoint.sh /usr/local/bin/entrypoint.sh
Expand All @@ -112,7 +116,13 @@ RUN ARCH=$(uname -m) && \
[ "$ARCH" = "aarch64" ] && ARCH_SUFFIX="-aarch64" || ARCH_SUFFIX="" && \
home-manager switch --flake "/opt/nixhome#devcell-go${ARCH_SUFFIX}" && \
ln -sfT "$(readlink -f $HOME/.nix-profile)" \
/nix/var/nix/profiles/per-user/devcell/profile
/nix/var/nix/profiles/per-user/devcell/profile && \
cd "$HOME" && MISE_DATA_DIR=/opt/mise MISE_YES=1 mise install && \
for tool_dir in /opt/mise/installs/*/; do \
tool=$(basename "$tool_dir"); \
version_dir=$(ls -1d "${tool_dir}"*/ 2>/dev/null | head -1); \
if [ -n "$version_dir" ]; then ln -sfT "$version_dir" "/opt/mise/$tool"; fi; \
done

ENV DEVCELL_PROFILE=devcell-go

Expand All @@ -130,7 +140,12 @@ RUN ARCH=$(uname -m) && \
home-manager switch --flake "/opt/nixhome#devcell-node${ARCH_SUFFIX}" && \
ln -sfT "$(readlink -f $HOME/.nix-profile)" \
/nix/var/nix/profiles/per-user/devcell/profile && \
asdf install nodejs && asdf reshim
cd "$HOME" && MISE_DATA_DIR=/opt/mise MISE_YES=1 mise install && \
for tool_dir in /opt/mise/installs/*/; do \
tool=$(basename "$tool_dir"); \
version_dir=$(ls -1d "${tool_dir}"*/ 2>/dev/null | head -1); \
if [ -n "$version_dir" ]; then ln -sfT "$version_dir" "/opt/mise/$tool"; fi; \
done

ENV DEVCELL_PROFILE=devcell-node

Expand Down Expand Up @@ -193,11 +208,19 @@ RUN ARCH=$(uname -m) && \
home-manager switch --flake "/opt/nixhome#devcell-fullstack${ARCH_SUFFIX}" && \
ln -sfT "$(readlink -f $HOME/.nix-profile)" \
/nix/var/nix/profiles/per-user/devcell/profile && \
asdf install nodejs && asdf reshim
cd "$HOME" && MISE_DATA_DIR=/opt/mise MISE_YES=1 mise install && \
for tool_dir in /opt/mise/installs/*/; do \
tool=$(basename "$tool_dir"); \
version_dir=$(ls -1d "${tool_dir}"*/ 2>/dev/null | head -1); \
if [ -n "$version_dir" ]; then ln -sfT "$version_dir" "/opt/mise/$tool"; fi; \
done && \
test -x /opt/mise/node/bin/npm || { echo "ERROR: npm not found at /opt/mise/node/bin/npm"; ls -la /opt/mise/ 2>/dev/null; exit 1; }

ENV DEVCELL_PROFILE=devcell-fullstack
# Add mise-installed tool bins to PATH (node/npm, go, etc.) via stable symlinks
ENV PATH="/opt/mise/node/bin:/opt/mise/go/bin:${PATH}"

# Agent CLI tools (npm) — requires Node.js from asdf above
# Agent CLI tools (npm) — requires Node.js from mise above
COPY --chown=${USER_UID}:${USER_GID} images/package.json images/package-lock.json* /opt/npm-tools/
RUN cd /opt/npm-tools/ && npm install
ENV PATH="/opt/npm-tools/node_modules/.bin:${PATH}"
Expand Down
Loading
Loading