devs-common 4.0.7__tar.gz → 5.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {devs_common-4.0.7 → devs_common-5.0.1}/PKG-INFO +1 -1
  2. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/core/container.py +60 -2
  3. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/Dockerfile +65 -5
  4. devs_common-5.0.1/devs_common/templates/README-vscode-settings.md +48 -0
  5. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/devcontainer.json +19 -50
  6. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/extensions/devs-bridge-drop.vsix +0 -0
  7. devs_common-5.0.1/devs_common/templates/machine-settings.json +60 -0
  8. devs_common-5.0.1/devs_common/templates/machine-settings.tmux.json +6 -0
  9. devs_common-5.0.1/devs_common/templates/register-vscode-extensions.js +33 -0
  10. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/post-create-wrapper.sh +2 -0
  11. devs_common-5.0.1/devs_common/templates/scripts/post-start.sh +43 -0
  12. devs_common-5.0.1/devs_common/templates/scripts/setup-vscode-settings.sh +54 -0
  13. devs_common-5.0.1/devs_common/templates/scripts/start-tailscale.sh +158 -0
  14. devs_common-5.0.1/devs_common/templates/scripts/ts.sh +4 -0
  15. devs_common-5.0.1/devs_common/templates/sudo-scripts/setup-magicdns.sh +31 -0
  16. devs_common-5.0.1/devs_common/templates/sudo-scripts/start-tailscaled.sh +36 -0
  17. devs_common-5.0.1/devs_common/templates/sudo-scripts/ts-cli.sh +5 -0
  18. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/devcontainer.py +3 -0
  19. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common.egg-info/PKG-INFO +1 -1
  20. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common.egg-info/SOURCES.txt +11 -0
  21. {devs_common-4.0.7 → devs_common-5.0.1}/pyproject.toml +1 -1
  22. {devs_common-4.0.7 → devs_common-5.0.1}/LICENSE +0 -0
  23. {devs_common-4.0.7 → devs_common-5.0.1}/README.md +0 -0
  24. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/__init__.py +0 -0
  25. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/config.py +0 -0
  26. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/core/__init__.py +0 -0
  27. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/core/project.py +0 -0
  28. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/core/workspace.py +0 -0
  29. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/devs_config.py +0 -0
  30. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/exceptions.py +0 -0
  31. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/extensions/README.md +0 -0
  32. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/extensions/install.sh +0 -0
  33. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/copy-ssh.sh +0 -0
  34. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/install-bridge-drop.sh +0 -0
  35. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/python-venv-integration.sh +0 -0
  36. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/screenshot-webapp.sh +0 -0
  37. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/setup-workspace.sh +0 -0
  38. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/start-tunnel.sh +0 -0
  39. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/tunnel-status.sh +0 -0
  40. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/scripts/webapp-screenshot.py +0 -0
  41. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/sudo-scripts/init-firewall.sh +0 -0
  42. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/sudo-scripts/setup-devs-env.sh +0 -0
  43. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/sudo-scripts/setup-vscode-cache.sh +0 -0
  44. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/sudo-scripts/start-services.sh +0 -0
  45. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/templates/vscode-settings-reference.md +0 -0
  46. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/__init__.py +0 -0
  47. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/config_hash.py +0 -0
  48. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/console.py +0 -0
  49. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/devcontainer_template.py +0 -0
  50. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/docker_client.py +0 -0
  51. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/file_utils.py +0 -0
  52. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/git_utils.py +0 -0
  53. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common/utils/repo_cache.py +0 -0
  54. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common.egg-info/dependency_links.txt +0 -0
  55. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common.egg-info/requires.txt +0 -0
  56. {devs_common-4.0.7 → devs_common-5.0.1}/devs_common.egg-info/top_level.txt +0 -0
  57. {devs_common-4.0.7 → devs_common-5.0.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 4.0.7
3
+ Version: 5.0.1
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -332,6 +332,7 @@ class ContainerManager:
332
332
  # Container exists but not running with matching config, just restart it
333
333
  console.print(f" 🔄 Restarting stopped container...")
334
334
  self.docker.start_container(existing_container['name'])
335
+ self._run_post_start(existing_container['name'])
335
336
  console.print(f" ✅ Container restarted successfully")
336
337
  return True
337
338
  else:
@@ -422,6 +423,50 @@ class ContainerManager:
422
423
 
423
424
  raise ContainerError(f"Failed to ensure container running for {dev_name}: {e}")
424
425
 
426
+ def _run_post_start(self, container_name: str) -> None:
427
+ """Run the container's postStart hook after a raw Docker start.
428
+
429
+ ``postStartCommand`` is a devcontainer-CLI concept: it fires on ``devcontainer
430
+ up`` and on a Dev Containers attach, but a plain ``docker start`` (which is how
431
+ we resume an already-built container) runs only PID 1 and skips it entirely.
432
+ Without this the hook's whole point is lost on the commonest restart path —
433
+ tailscaled and the tailnet node stay down, and VS Code settings aren't
434
+ re-deployed. The script is idempotent, so running it again is harmless. Never
435
+ raises: a container without the devs template simply has no such script.
436
+ """
437
+ try:
438
+ self.docker.exec_command(container_name, "/usr/local/bin/post-start.sh")
439
+ except Exception:
440
+ pass # not a devs-template container / exec unavailable
441
+
442
+ def _deregister_tailnet_node(self, container_name: str, container_id: str) -> None:
443
+ """Best-effort tailnet cleanup when a container is destroyed (not just stopped).
444
+
445
+ Ephemeral Tailscale nodes auto-reap when the container stops, but that lags a
446
+ few minutes — long enough that an immediate recreate gets a clash-suffixed name
447
+ (…-eamonn-1). A graceful ``tailscale logout`` while the container is STILL UP
448
+ deregisters the node now, freeing the name for reuse. Then prune the writeback
449
+ handshake file start-tailscale.sh left in the env mount (nothing else reaps it;
450
+ the ``devs vscode`` resolver only reads files for containers that still exist, so
451
+ a missed prune is self-correcting). Never raises — teardown must not be blocked
452
+ by tailnet cleanup, and non-tailnet containers simply no-op here.
453
+ """
454
+ # 1. Graceful logout while the daemon is still alive (frees the tailnet name now).
455
+ try:
456
+ self.docker.exec_command(container_name, "sudo -n /usr/local/bin/ts-cli.sh logout")
457
+ except Exception:
458
+ pass # no tailscale in this container / daemon down / exec unavailable
459
+
460
+ # 2. Prune the handshake file (keyed by the container's short id == its hostname).
461
+ try:
462
+ nodes_dir = get_env_mount_path(self.project.info.name) / "ts-nodes"
463
+ if nodes_dir.is_dir():
464
+ for handshake in nodes_dir.glob("*.json"):
465
+ if container_id and container_id.startswith(handshake.stem):
466
+ handshake.unlink()
467
+ except Exception:
468
+ pass
469
+
425
470
  def stop_container(self, dev_name: str, remove: bool = True) -> bool:
426
471
  """Stop a container by labels, optionally removing it.
427
472
 
@@ -442,6 +487,11 @@ class ContainerManager:
442
487
  for container_info in existing_containers:
443
488
  container_name = container_info['name']
444
489
 
490
+ if remove:
491
+ # Free the tailnet node + handshake BEFORE stopping — logout
492
+ # needs the container's tailscaled still running.
493
+ self._deregister_tailnet_node(container_name, container_info.get('id', ''))
494
+
445
495
  try:
446
496
  self.docker.stop_container(container_name)
447
497
  except DockerError as stop_e:
@@ -600,7 +650,10 @@ class ContainerManager:
600
650
  for container in containers:
601
651
  try:
602
652
  console.print(f" 🗑️ Removing aborted container: {container.name} ({container.status})")
603
-
653
+
654
+ # Best-effort tailnet cleanup (logout if still up + prune handshake file).
655
+ self._deregister_tailnet_node(container.name, container.container_id or '')
656
+
604
657
  # Stop container first if it's running
605
658
  if container.status.lower() in ['running', 'restarting']:
606
659
  console.print(f" 🛑 Stopping running container: {container.name}")
@@ -664,6 +717,7 @@ class ContainerManager:
664
717
  container_id = existing_containers[0].get('name', container_name)
665
718
  console.print(f" ▶️ Starting stopped container: {container_id}")
666
719
  self.docker.start_container(container_id)
720
+ self._run_post_start(container_id)
667
721
  else:
668
722
  # Ensure container is running (may create/restart as needed, but never auto-rebuild)
669
723
  if not self.ensure_container_running(dev_name, workspace_dir, check_rebuild=False, debug=debug, live=live, extra_env=extra_env):
@@ -888,7 +942,11 @@ class ContainerManager:
888
942
  return self.exec_command(
889
943
  dev_name=dev_name,
890
944
  workspace_dir=workspace_dir,
891
- command="codex --full-auto",
945
+ # `--full-auto` was removed from the Codex CLI (passing it now aborts with
946
+ # "unexpected argument"). This is the flag Codex intends for externally
947
+ # sandboxed environments, which a devcontainer is — and it matches the
948
+ # `claude --dangerously-skip-permissions` alias sitting beside it.
949
+ command="codex --dangerously-bypass-approvals-and-sandbox",
892
950
  stdin_input=prompt,
893
951
  debug=debug,
894
952
  stream=stream,
@@ -35,6 +35,7 @@ RUN apt update && apt install -y less \
35
35
  vim \
36
36
  nano \
37
37
  htop \
38
+ tmux \
38
39
  && apt clean \
39
40
  && rm -rf /var/lib/apt/lists/*
40
41
 
@@ -87,11 +88,33 @@ RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhisto
87
88
  ENV DEVCONTAINER=true
88
89
 
89
90
  # Create workspace and config directories and set permissions
90
- RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig /home/node/.vscode-server/extensions && \
91
+ RUN mkdir -p /workspaces /home/node/claudeconfig /home/node/codexconfig \
92
+ /home/node/.vscode-server/extensions \
93
+ /home/node/.vscode-server/data/Machine && \
91
94
  echo '[]' > /home/node/.vscode-server/extensions/extensions.json && \
92
95
  chown -R node:node /workspaces /home/node /home/node/.vscode-server && \
93
96
  chmod 755 /workspaces
94
97
 
98
+ # Pre-install workspace-kind VS Code extensions so they're available on first connect.
99
+ # devcontainer.json's extensions list is only auto-installed by the Dev Containers
100
+ # extension; over an SSH remote it's silently skipped.
101
+ RUN for EXT in vscode-tmux-auto-reattach devs-bridge-drop; do \
102
+ curl -fSL --retry 3 --compressed \
103
+ "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ideonate/vsextensions/${EXT}/latest/vspackage" \
104
+ -o /tmp/ext.vsix && \
105
+ unzip -q /tmp/ext.vsix "extension/package.json" -d /tmp/ext-meta && \
106
+ VERSION=$(node -e "console.log(require('/tmp/ext-meta/extension/package.json').version)") && \
107
+ EXT_DIR="/home/node/.vscode-server/extensions/ideonate.${EXT}-${VERSION}" && \
108
+ mkdir -p "${EXT_DIR}" && \
109
+ unzip -q /tmp/ext.vsix "extension/*" -d /tmp/ext && \
110
+ cp -r /tmp/ext/extension/. "${EXT_DIR}/" && \
111
+ rm -rf /tmp/ext.vsix /tmp/ext /tmp/ext-meta; \
112
+ done && \
113
+ chown -R node:node /home/node/.vscode-server/extensions/
114
+ COPY register-vscode-extensions.js /tmp/
115
+ RUN node /tmp/register-vscode-extensions.js && rm /tmp/register-vscode-extensions.js && \
116
+ chown node:node /home/node/.vscode-server/extensions/extensions.json
117
+
95
118
  WORKDIR /workspaces
96
119
 
97
120
  # Install git-delta for better git diffs
@@ -122,8 +145,10 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
122
145
  # Install Claude CLI via native installer
123
146
  RUN curl -fsSL https://claude.ai/install.sh | bash
124
147
 
125
- # Install OpenAI Codex CLI
126
- RUN npm install -g @openai/codex
148
+ # Install OpenAI Codex CLI. Pinned: an unpinned install silently followed upstream
149
+ # flag changes, which is how the old `--full-auto` alias below ended up broken.
150
+ ARG CODEX_CLI_VERSION=0.144.6
151
+ RUN npm install -g "@openai/codex@${CODEX_CLI_VERSION}"
127
152
 
128
153
  # Install cloudflared for tunnel support (proxies all HTTP methods correctly)
129
154
  # Install VS Code CLI for tunnel support (needs root for /usr/local/bin)
@@ -143,13 +168,26 @@ RUN ARCH=$(dpkg --print-architecture) && \
143
168
  rm /tmp/vscode-cli.tar.gz && \
144
169
  chmod +x /usr/local/bin/code
145
170
 
171
+ # Install Tailscale (static binaries) so each devcontainer can join the tailnet
172
+ # as its own node in userspace-networking mode — see scripts/start-tailscale.sh.
173
+ # Pinned for reproducible builds; bump as needed.
174
+ ARG TAILSCALE_VERSION=1.98.4
175
+ RUN ARCH=$(dpkg --print-architecture) && \
176
+ curl -fSL --retry 5 --retry-delay 5 --retry-all-errors --connect-timeout 30 \
177
+ "https://pkgs.tailscale.com/stable/tailscale_${TAILSCALE_VERSION}_${ARCH}.tgz" -o /tmp/tailscale.tgz && \
178
+ tar -xzf /tmp/tailscale.tgz -C /tmp && \
179
+ mv "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}/tailscale" "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}/tailscaled" /usr/local/bin/ && \
180
+ rm -rf /tmp/tailscale.tgz "/tmp/tailscale_${TAILSCALE_VERSION}_${ARCH}" && \
181
+ chmod +x /usr/local/bin/tailscale /usr/local/bin/tailscaled
182
+
146
183
  # Set up environment variables and aliases for Claude and Codex
147
184
  RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
148
185
  echo "export CLAUDE_CONFIG_DIR=\"/home/node/claudeconfig\"" >> "$shell_rc" && \
149
186
  echo "export CODEX_CONFIG_HOME=\"/home/node/codexconfig\"" >> "$shell_rc" && \
150
187
  echo "export PATH=\"/home/node/.local/bin:\$PATH\"" >> "$shell_rc" && \
151
188
  echo "alias claude=\"claude --dangerously-skip-permissions\"" >> "$shell_rc" && \
152
- echo "alias codex=\"/usr/local/share/npm-global/bin/codex --full-auto\"" >> "$shell_rc" && \
189
+ echo "alias codex=\"/usr/local/share/npm-global/bin/codex --dangerously-bypass-approvals-and-sandbox\"" >> "$shell_rc" && \
190
+ echo "alias codex-normal=\"/usr/local/share/npm-global/bin/codex\"" >> "$shell_rc" && \
153
191
  echo "" >> "$shell_rc" && \
154
192
  echo "# Auto-activate Python venv if it exists" >> "$shell_rc" && \
155
193
  echo "if [ -f /home/node/.devs-venv/workspace-venv/bin/activate ]; then" >> "$shell_rc" && \
@@ -159,11 +197,33 @@ RUN for shell_rc in /home/node/.zshrc /home/node/.bashrc; do \
159
197
  echo "export TEST_RESULTS_FOLDER=\"/home/node/bridge/test-results\"" >> "$shell_rc"; \
160
198
  done
161
199
 
200
+ # tmux defaults: mouse scrolling, deep scrollback, and live tab titles. Pairs with the
201
+ # ideonate.vscode-tmux-auto-reattach extension (liveTitles mode). Present regardless of
202
+ # DEVS_TMUX — this only shapes tmux when you actually run it.
203
+ RUN printf '%s\n' \
204
+ 'set -g mouse on' \
205
+ 'set -g history-limit 50000' \
206
+ 'bind m set -g mouse \; display "mouse #{?mouse,on,off}"' \
207
+ 'set -g set-titles on' \
208
+ "set -g set-titles-string '#{session_name}: #{pane_current_command}'" \
209
+ > /home/node/.tmux.conf && \
210
+ chown node:node /home/node/.tmux.conf
211
+
212
+ # VS Code settings live here rather than in devcontainer.json's
213
+ # customizations.vscode.settings, because that block is only applied by the Dev
214
+ # Containers extension — a Remote-SSH connection (e.g. over Tailscale SSH) never reads
215
+ # devcontainer.json. Baking them as machine settings covers both paths. Both files also
216
+ # land in /etc/devcontainer-config/ so scripts/setup-vscode-settings.sh can re-deploy
217
+ # them at container start with the optional tmux overlay (DEVS_TMUX=1) applied.
218
+ # See templates/README-vscode-settings.md.
219
+ COPY --chown=node:node machine-settings.json /home/node/.vscode-server/data/Machine/settings.json
220
+ COPY machine-settings.json machine-settings.tmux.json /etc/devcontainer-config/
162
221
 
163
222
  # Copy and set up scripts
164
223
  COPY scripts/ /usr/local/bin/
165
224
  COPY sudo-scripts/ /usr/local/bin/
166
- RUN chmod +x /usr/local/bin/*.sh /usr/local/bin/*.py
225
+ RUN chmod +x /usr/local/bin/*.sh /usr/local/bin/*.py && \
226
+ ln -sf /usr/local/bin/ts.sh /usr/local/bin/ts
167
227
  COPY sudo-scripts/ /tmp/sudo-scripts-list/
168
228
  RUN echo "DEBUG: Files that need sudo:" && ls -la /tmp/sudo-scripts-list/ && \
169
229
  { echo "# Allow node user to run sudo scripts without password"; \
@@ -0,0 +1,48 @@
1
+ # VS Code settings in this template
2
+
3
+ `machine-settings.json` is baked into the image at
4
+ `~/.vscode-server/data/Machine/settings.json` (see the Dockerfile), **not** put in
5
+ `devcontainer.json` under `customizations.vscode.settings`.
6
+
7
+ Why: that `customizations` block is applied by the **Dev Containers extension only**. A
8
+ Remote-SSH connection — which is how you reach a container over Tailscale SSH — never reads
9
+ `devcontainer.json`, so it got neither settings nor extensions. Machine settings cover both
10
+ paths. For the same reason the `ideonate.*` extensions are unzipped into the image and
11
+ registered via `register-vscode-extensions.js`.
12
+
13
+ **Don't add a `settings` block back to `devcontainer.json`**: the Dev Containers extension
14
+ writes it over the same machine-settings file, and the two would silently drift.
15
+
16
+ ## tmux (opt-in, off by default)
17
+
18
+ `machine-settings.json` *defines* a `tmux` terminal profile and the
19
+ `ideonate.vscode-tmux-auto-reattach` settings, but leaves the default profile as `zsh` and
20
+ `tmuxAutoReattach.runOnStartup` as `false`. Nothing changes unless you ask for it.
21
+
22
+ To turn it on, set `DEVS_TMUX=1` — via `DEVS.yml` `env_vars:`, `devs start … --env
23
+ DEVS_TMUX=1`, or a mounted `~/.devs/envs/<project>/.env`. On each container start,
24
+ `scripts/setup-vscode-settings.sh` merges `machine-settings.tmux.json` over the base file,
25
+ which flips the default profile to tmux and enables auto-reattach. Unset it and the next
26
+ start reverts to the base file — no rebuild needed either way.
27
+
28
+ You can still open a tmux terminal manually from the profile dropdown with `DEVS_TMUX`
29
+ unset; the profile is always defined.
30
+
31
+ ### Notes on the tmux settings
32
+
33
+ - The profile's args are a bare `new-session` — deliberately **not** `new-session -A -s
34
+ <name>`. The `-A` form attaches to the named session if it exists, so every "New
35
+ Terminal" lands you back in the *same* session instead of opening a new one. Reattaching
36
+ to existing sessions is the auto-reattach extension's job, not the profile's.
37
+ - `~/.tmux.conf` (written in the Dockerfile) sets `mouse on`, 50k scrollback, and
38
+ `set-titles` so tab titles show the running command. Existing sessions keep their old
39
+ config — `tmux source-file ~/.tmux.conf` then start a new session, or `tmux kill-server`.
40
+ - With `mouse on`, click-drag and double-click go to tmux rather than VS Code.
41
+ `terminal.integrated.macOptionClickForcesSelection` (on in the base file) lets
42
+ Option-click/drag bypass tmux for native selection. `prefix + m` toggles mouse mode.
43
+ - `tmuxAutoReattach.shellPath=/bin/sh` runs the reattach as the terminal's own process.
44
+ Without it the terminal starts the default profile (`tmux new-session`) first and the
45
+ attach nests inside that tmux.
46
+ - `terminal.integrated.enablePersistentSessions: false` (tmux overlay) stops VS Code
47
+ reviving old terminal tabs on reload — each would launch the default profile before the
48
+ extension can reattach, again causing nested tmux.
@@ -22,55 +22,14 @@
22
22
  "ms-python.black-formatter",
23
23
  "ms-python.isort",
24
24
  "Anthropic.claude-code",
25
- "ideonate.devs-bridge-drop"
26
- ],
27
- "settings": {
28
- "editor.formatOnSave": false,
29
- "editor.defaultFormatter": "esbenp.prettier-vscode",
30
- "editor.codeActionsOnSave": {
31
- "source.fixAll.eslint": "explicit"
32
- },
33
- "editor.detectIndentation": false,
34
- "editor.insertSpaces": true,
35
- "editor.tabSize": 2,
36
- "[python]": {
37
- "editor.formatOnSave": true,
38
- "editor.formatOnType": true,
39
- "editor.tabSize": 4,
40
- "editor.codeActionsOnSave": {
41
- "source.organizeImports": "never"
42
- }
43
- },
44
- "python.defaultInterpreterPath": "/home/node/.devs-venv/workspace-venv/bin/python",
45
- "python.terminal.activateEnvironment": true,
46
- "python.interpreter.infoVisibility": "never",
47
- "python.venvFolders": [".devs-venv"],
48
- "python.languageServer": "Pylance",
49
- "python.analysis.typeCheckingMode": "off",
50
- "python.analysis.autoImportCompletions": true,
51
- "python.analysis.diagnosticSeverityOverrides": {
52
- "reportUnusedImport": "information",
53
- "reportUnusedVariable": "information"
54
- },
55
- "pylint.enabled": false,
56
- "gitlens.mode": "zen",
57
- "python.analysis.diagnosticMode": "openFilesOnly",
58
- "settingsSync.ignoredExtensions": [],
59
- "settingsSync.keybindingsPerPlatform": false,
60
- "extensions.autoCheckUpdates": false,
61
- "extensions.autoUpdate": false,
62
- "extensions.ignoreRecommendations": true,
63
- "terminal.integrated.defaultProfile.linux": "zsh",
64
- "terminal.integrated.profiles.linux": {
65
- "bash": {
66
- "path": "bash",
67
- "icon": "terminal-bash"
68
- },
69
- "zsh": {
70
- "path": "zsh"
71
- }
72
- }
73
- }
25
+ "ideonate.devs-bridge-drop",
26
+ "ideonate.vscode-tmux-auto-reattach"
27
+ ]
28
+ // VS Code settings live in .devcontainer/machine-settings.json — baked into
29
+ // the image via Dockerfile so they apply for both Dev Containers and SSH
30
+ // remote (Tailscale SSH never reads devcontainer.json). Don't add a
31
+ // "settings" block back here: the Dev Containers extension writes it over
32
+ // the same machine-settings file, and the two would drift.
74
33
  }
75
34
  },
76
35
  "remoteUser": "node",
@@ -84,7 +43,11 @@
84
43
  // "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-extensions,target=/home/node/.vscode-server/extensions,type=volume",
85
44
  // .vscode-server includes extension state which we don't want to keep
86
45
  // "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-data,target=/home/node/.vscode-server,type=volume",
87
- "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-downloads,target=/home/node/.vscode-extensions-cache-volume,type=volume"
46
+ "source=${localEnv:WORKSPACE_FOLDER_NAME}-vscode-downloads,target=/home/node/.vscode-extensions-cache-volume,type=volume",
47
+ // Persist the tailnet node identity so the container keeps one stable 100.x
48
+ // and hostname across rebuilds — otherwise every rebuild mints a new node and
49
+ // your saved VS Code SSH host stops resolving.
50
+ "source=${localEnv:WORKSPACE_FOLDER_NAME}-tailscale-state,target=/var/lib/tailscale,type=volume"
88
51
  ],
89
52
  "remoteEnv": {
90
53
  "NODE_OPTIONS": "--max-old-space-size=4096",
@@ -93,13 +56,19 @@
93
56
  "LC_ALL": "en_US.UTF-8",
94
57
  "GIT_REMOTE_URL": "${localEnv:GIT_REMOTE_URL}",
95
58
  "WORKSPACE_FOLDER_NAME": "${localEnv:WORKSPACE_FOLDER_NAME}",
59
+ "DEVCONTAINER_NAME": "${localEnv:DEVCONTAINER_NAME}",
60
+ "DEVS_PROJECT_NAME": "${localEnv:DEVS_PROJECT_NAME}",
96
61
  "DEVS_DEBUG": "${localEnv:DEVS_DEBUG}",
97
62
  "DEVS_LIVE_MODE": "${localEnv:DEVS_LIVE_MODE}",
63
+ // Opt-in: makes tmux the default terminal profile and enables auto-reattach.
64
+ // Off unless set to 1/true/yes — see scripts/setup-vscode-settings.sh.
65
+ "DEVS_TMUX": "${localEnv:DEVS_TMUX}",
98
66
  "DEVS_ENV_MOUNT_PATH": "${localEnv:DEVS_ENV_MOUNT_PATH}",
99
67
  "DEVS_BRIDGE_MOUNT_PATH": "${localEnv:DEVS_BRIDGE_MOUNT_PATH}"
100
68
  },
101
69
  "workspaceFolder": "/workspaces/${localEnv:WORKSPACE_FOLDER_NAME}",
102
70
  "postCreateCommand": "/usr/local/bin/post-create-wrapper.sh",
71
+ "postStartCommand": "/usr/local/bin/post-start.sh",
103
72
  "forwardPorts": [],
104
73
  "portsAttributes": {
105
74
  "3306": {
@@ -0,0 +1,60 @@
1
+ {
2
+ "editor.formatOnSave": false,
3
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
4
+ "editor.codeActionsOnSave": {
5
+ "source.fixAll.eslint": "explicit"
6
+ },
7
+ "editor.detectIndentation": false,
8
+ "editor.insertSpaces": true,
9
+ "editor.tabSize": 2,
10
+ "[python]": {
11
+ "editor.formatOnSave": true,
12
+ "editor.formatOnType": true,
13
+ "editor.tabSize": 4,
14
+ "editor.codeActionsOnSave": {
15
+ "source.organizeImports": "never"
16
+ }
17
+ },
18
+ "python.defaultInterpreterPath": "/home/node/.devs-venv/workspace-venv/bin/python",
19
+ "python.terminal.activateEnvironment": true,
20
+ "python.interpreter.infoVisibility": "never",
21
+ "python.venvFolders": [".devs-venv"],
22
+ "python.languageServer": "Pylance",
23
+ "python.analysis.typeCheckingMode": "off",
24
+ "python.analysis.autoImportCompletions": true,
25
+ "python.analysis.diagnosticSeverityOverrides": {
26
+ "reportUnusedImport": "information",
27
+ "reportUnusedVariable": "information"
28
+ },
29
+ "pylint.enabled": false,
30
+ "gitlens.mode": "zen",
31
+ "python.analysis.diagnosticMode": "openFilesOnly",
32
+ "settingsSync.ignoredExtensions": [],
33
+ "settingsSync.keybindingsPerPlatform": false,
34
+ "extensions.autoCheckUpdates": false,
35
+ "extensions.autoUpdate": false,
36
+ "extensions.ignoreRecommendations": true,
37
+ "terminal.integrated.defaultProfile.linux": "zsh",
38
+ "terminal.integrated.profiles.linux": {
39
+ "bash": {
40
+ "path": "bash",
41
+ "icon": "terminal-bash"
42
+ },
43
+ "zsh": {
44
+ "path": "zsh"
45
+ },
46
+ "tmux": {
47
+ "path": "tmux",
48
+ "args": ["new-session"]
49
+ }
50
+ },
51
+ "terminal.integrated.macOptionClickForcesSelection": true,
52
+ "tmuxAutoReattach.runOnStartup": false,
53
+ "tmuxAutoReattach.skipIfTerminalsOpen": true,
54
+ "tmuxAutoReattach.listCommand": "tmux list-sessions -F \"#{session_name}\"",
55
+ "tmuxAutoReattach.attachCommand": "tmux attach -t {session}",
56
+ "tmuxAutoReattach.shellPath": "/bin/sh",
57
+ "tmuxAutoReattach.shellArgs": ["-c"],
58
+ "tmuxAutoReattach.liveTitles": true,
59
+ "tmuxAutoReattach.titleCommand": ""
60
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "terminal.integrated.defaultProfile.linux": "tmux",
3
+ "tmuxAutoReattach.runOnStartup": true,
4
+ "terminal.integrated.tabs.title": "${sequence}",
5
+ "terminal.integrated.enablePersistentSessions": false
6
+ }
@@ -0,0 +1,33 @@
1
+ // Writes extensions.json so VS Code SSH remote discovers pre-installed extensions.
2
+ // VS Code trusts this registry rather than scanning the directory.
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const extDir = "/home/node/.vscode-server/extensions";
7
+
8
+ const entries = fs
9
+ .readdirSync(extDir)
10
+ .filter(
11
+ (d) =>
12
+ !d.endsWith(".json") && fs.statSync(path.join(extDir, d)).isDirectory(),
13
+ )
14
+ .flatMap((dir) => {
15
+ try {
16
+ const pkg = JSON.parse(
17
+ fs.readFileSync(path.join(extDir, dir, "package.json"), "utf8"),
18
+ );
19
+ return [
20
+ {
21
+ identifier: { id: `${pkg.publisher}.${pkg.name}` },
22
+ version: pkg.version,
23
+ location: { $mid: 1, path: path.join(extDir, dir), scheme: "file" },
24
+ relativeLocation: dir,
25
+ metadata: { installedTimestamp: 1700000000000, source: "vsix" },
26
+ },
27
+ ];
28
+ } catch (e) {
29
+ return [];
30
+ }
31
+ });
32
+
33
+ fs.writeFileSync(path.join(extDir, "extensions.json"), JSON.stringify(entries));
@@ -37,6 +37,8 @@ run_step "setup-devs-env.sh" "/usr/local/bin/setup-devs-env.sh"
37
37
  #run_step "init-firewall.sh" "sudo /usr/local/bin/init-firewall.sh"
38
38
  run_step "setup-workspace.sh" "/usr/local/bin/setup-workspace.sh"
39
39
  run_step "start-services.sh" "sudo /usr/local/bin/start-services.sh"
40
+ # Optional: join the tailnet + serve a port. Non-fatal — no auth key => no-op.
41
+ run_step "start-tailscale.sh" "/usr/local/bin/start-tailscale.sh" || true
40
42
 
41
43
  # GitHub auth setup is optional, so handle it separately
42
44
  echo "📋 Running gh auth setup-git..."
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # postStartCommand orchestrator (runs as the node user on every container start).
3
+ # Unprivileged: env/gating logic, then hands privileged work to root sudo-scripts.
4
+ #
5
+ # Why this runs on EVERY start (not just postCreate): a plain container restart
6
+ # (host reboot, `docker start`) re-runs only PID 1, not the devcontainer
7
+ # create-lifecycle. So tailscaled, the tailnet node and Tailscale SSH are all gone
8
+ # after a restart — and a VS Code Remote-SSH connection to that node then has
9
+ # nothing to reconnect to. Re-running the bring-up here restores it automatically.
10
+ # Every step is idempotent, so running it again right after postCreate (on first
11
+ # create) is a harmless no-op.
12
+ set -uo pipefail
13
+
14
+ # Same env source start-tailscale.sh reads TS_ENABLE/TS_AUTHKEY from.
15
+ if [ -f /home/node/.devs-env/.env ]; then
16
+ set -a; . /home/node/.devs-env/.env; set +a
17
+ fi
18
+
19
+ # VS Code machine settings (+ optional tmux overlay, off unless DEVS_TMUX=1). Not
20
+ # Tailscale-gated — it configures the editor regardless of how you connect. Must come
21
+ # before the TS_ENABLE gate below, which exits when Tailscale is off.
22
+ /usr/local/bin/setup-vscode-settings.sh || true
23
+
24
+ # Gate on the same master switch as the rest of the Tailscale wiring.
25
+ case "${TS_ENABLE:-}" in
26
+ 1|true|yes) ;;
27
+ *)
28
+ echo "ℹ️ Tailscale not enabled (TS_ENABLE=${TS_ENABLE:-unset}) — skipping tailnet setup."
29
+ exit 0 ;;
30
+ esac
31
+
32
+ # Re-join the tailnet (idempotent): ensures tailscaled is running — self-healing a
33
+ # stale socket left by the previous boot, see sudo-scripts/start-tailscaled.sh —
34
+ # then re-runs `tailscale up` (+ serve/funnel). The hostname derives from the same
35
+ # remoteEnv (DEVS_PROJECT_NAME/DEVCONTAINER_NAME) the create path uses, and the
36
+ # persisted /var/lib/tailscale state reclaims the same identity, so the node's
37
+ # name stays stable across restarts and rebuilds.
38
+ /usr/local/bin/start-tailscale.sh || true
39
+
40
+ # Prepend the MagicDNS resolver so tailnet split-DNS names resolve from inside the
41
+ # container. Must run after the bring-up above (it no-ops unless tailscaled is up),
42
+ # and on every start because Docker regenerates /etc/resolv.conf each time.
43
+ sudo -n /usr/local/bin/setup-magicdns.sh || true
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Deploy VS Code machine settings, applying the optional tmux overlay.
3
+ #
4
+ # Settings live in ~/.vscode-server/data/Machine/settings.json rather than in
5
+ # devcontainer.json's customizations.vscode.settings, because that block is applied by
6
+ # the Dev Containers extension only — a Remote-SSH connection (e.g. over Tailscale SSH)
7
+ # never reads devcontainer.json. See templates/README-vscode-settings.md.
8
+ #
9
+ # The Dockerfile already bakes the base file, so a container is correctly configured even
10
+ # if this never runs. This script exists for the DEVS_TMUX toggle: it re-deploys the base
11
+ # and merges machine-settings.tmux.json over it when tmux is requested, so switching is a
12
+ # container restart rather than an image rebuild.
13
+ #
14
+ # DEVS_TMUX "1"/"true"/"yes" => tmux becomes the default terminal profile and
15
+ # ideonate.vscode-tmux-auto-reattach reattaches sessions on window load.
16
+ # OFF by default: the tmux profile is defined either way, so you can pick
17
+ # it from the terminal dropdown without opting in globally.
18
+ set -uo pipefail
19
+
20
+ BASE=/etc/devcontainer-config/machine-settings.json
21
+ OVERLAY=/etc/devcontainer-config/machine-settings.tmux.json
22
+ DEST=/home/node/.vscode-server/data/Machine/settings.json
23
+
24
+ [ -f "$BASE" ] || exit 0
25
+
26
+ # Same env source the other devs container scripts read their switches from.
27
+ if [ -f /home/node/.devs-env/.env ]; then
28
+ set -a; . /home/node/.devs-env/.env; set +a
29
+ fi
30
+
31
+ mkdir -p "$(dirname "$DEST")"
32
+
33
+ case "${DEVS_TMUX:-}" in
34
+ 1|true|yes)
35
+ if [ -f "$OVERLAY" ] && node -e '
36
+ const fs = require("fs");
37
+ const [base, overlay, dest] = process.argv.slice(1);
38
+ const merged = {
39
+ ...JSON.parse(fs.readFileSync(base, "utf8")),
40
+ ...JSON.parse(fs.readFileSync(overlay, "utf8")),
41
+ };
42
+ fs.writeFileSync(dest, JSON.stringify(merged, null, 2) + "\n");
43
+ ' "$BASE" "$OVERLAY" "$DEST"; then
44
+ echo "🖥️ VS Code settings deployed (tmux enabled — DEVS_TMUX=${DEVS_TMUX})."
45
+ else
46
+ cp "$BASE" "$DEST"
47
+ echo "⚠️ tmux overlay merge failed — deployed base VS Code settings instead."
48
+ fi
49
+ ;;
50
+ *)
51
+ cp "$BASE" "$DEST"
52
+ echo "🖥️ VS Code settings deployed (tmux off; set DEVS_TMUX=1 to enable)."
53
+ ;;
54
+ esac
@@ -0,0 +1,158 @@
1
+ #!/bin/bash
2
+ # Join this devcontainer to the user's tailnet as its own node, and optionally
3
+ # publish a port over HTTPS (serve/funnel) and/or accept Tailscale SSH. Invoked (as
4
+ # the node user) from post-create-wrapper.sh and post-start.sh — the latter so the
5
+ # node comes back after a restart. Safe no-op unless TS_ENABLE=1.
6
+ #
7
+ # This orchestrator runs unprivileged and does the env/gating/hostname logic; the
8
+ # privileged bits go through root sudo-scripts (start-tailscaled.sh / ts-cli.sh),
9
+ # matching the init-firewall pattern. tailscaled runs as root in userspace
10
+ # networking mode — no TUN device needed, and root is what lets Tailscale SSH open
11
+ # login sessions.
12
+ #
13
+ # Config — all via env. The natural place to set these is the per-project env
14
+ # file ~/.devs/envs/<project>/.env on the host, which devs mounts at
15
+ # /home/node/.devs-env/.env and which this script sources below:
16
+ # TS_ENABLE master switch, STRICTLY OPT-IN. Off unless set to
17
+ # "1"/"true"/"yes". A key alone does NOT enable Tailscale, so an
18
+ # auth key can live in the shared default .env harmlessly until a
19
+ # given project sets TS_ENABLE=1.
20
+ # TS_AUTHKEY PREFERRED key: a reusable, tagged, NON-ephemeral key from the
21
+ # Tailscale admin console. Non-ephemeral is deliberate — the node
22
+ # survives going offline (WAN blip / host sleep) and just reconnects,
23
+ # so you can rely on SSH-ing into it. (An *ephemeral* node is instead
24
+ # garbage-collected by the control plane shortly after it disconnects;
25
+ # if that happens while the container keeps running, tailscaled wedges
26
+ # retrying a dead node key — "PollNetMap: initial fetch failed 404:
27
+ # node not found" — until the next bring-up.)
28
+ # TS_AUTHKEY_EPHEMERAL
29
+ # FALLBACK key, used only when TS_AUTHKEY is unset: a reusable,
30
+ # tagged, EPHEMERAL key. Gives a throwaway node that auto-cleans when
31
+ # the container goes away (no stale-node clutter), accepting the
32
+ # mid-life-GC caveat above. Natural home: the shared default .env, so
33
+ # any container still joins the tailnet even without a per-project
34
+ # stable key. With NEITHER set (and TS_ENABLE=1) => warn + exit 0.
35
+ # TS_HOSTNAME tailnet hostname. Default: <DEVS_PROJECT_NAME>-<DEVCONTAINER_NAME>
36
+ # (e.g. myorg-myapp-alice) -> <name>.<tailnet>.ts.net.
37
+ # Dev name alone isn't unique across projects, hence the prefix.
38
+ # TS_TAGS advertised tags (default: tag:devcontainer). Must match the
39
+ # tagOwners in your tailnet ACL and the auth key's tag.
40
+ # TS_SERVE_PORT if set, serve this local port over HTTPS on the node's name.
41
+ # TS_FUNNEL "1"/"true": expose TS_SERVE_PORT to the PUBLIC internet
42
+ # (Funnel) instead of tailnet-only serve.
43
+ # TS_SSH "1"/"true": accept Tailscale SSH into the container (needs an
44
+ # `ssh` rule in the tailnet ACL allowing the `node` user).
45
+ set -uo pipefail
46
+
47
+ # Pull in secrets/config from the mounted devs env file, if present.
48
+ if [ -f /home/node/.devs-env/.env ]; then
49
+ set -a; . /home/node/.devs-env/.env; set +a
50
+ fi
51
+
52
+ # Master switch — strictly opt-in, off by default (like the firewall). A key
53
+ # alone does NOT enable it, so a tailnet auth key can sit in the shared default
54
+ # .env harmlessly until a project explicitly sets TS_ENABLE=1.
55
+ case "${TS_ENABLE:-}" in
56
+ 1|true|yes) ;;
57
+ *)
58
+ echo "ℹ️ Tailscale not enabled (TS_ENABLE=${TS_ENABLE:-unset}) — skipping."
59
+ echo " Set TS_ENABLE=1 (+ TS_AUTHKEY) to join the tailnet."
60
+ exit 0 ;;
61
+ esac
62
+
63
+ # Pick the auth key: prefer the stable (non-ephemeral) TS_AUTHKEY; fall back to an
64
+ # ephemeral throwaway key. KEY_MODE is just for logging — the node's actual
65
+ # ephemerality is baked into whichever key was minted, not set here.
66
+ if [ -n "${TS_AUTHKEY:-}" ]; then
67
+ AUTHKEY="$TS_AUTHKEY"; KEY_MODE="stable (non-ephemeral)"
68
+ elif [ -n "${TS_AUTHKEY_EPHEMERAL:-}" ]; then
69
+ AUTHKEY="$TS_AUTHKEY_EPHEMERAL"; KEY_MODE="ephemeral (fallback)"
70
+ else
71
+ echo "⚠️ TS_ENABLE is set but no key found — skipping Tailscale."
72
+ echo " Set TS_AUTHKEY (reusable, tagged, non-ephemeral) for a stable node,"
73
+ echo " or TS_AUTHKEY_EPHEMERAL (reusable, tagged, ephemeral) for a throwaway one,"
74
+ echo " in a mounted .env (e.g. ~/.devs/envs/default/.env)."
75
+ exit 0
76
+ fi
77
+
78
+ SSH_ON=""
79
+ case "${TS_SSH:-}" in 1|true|yes) SSH_ON=1 ;; esac
80
+
81
+ # tailscale CLI against the root daemon, via the NOPASSWD sudo-script.
82
+ TS=(sudo -n /usr/local/bin/ts-cli.sh)
83
+
84
+ # Derive a DNS-safe hostname. Prefer an explicit override; else <project>-<dev>
85
+ # (unique across projects, stable in both live and copy modes); else fall back.
86
+ if [ -n "${TS_HOSTNAME:-}" ]; then
87
+ RAW_NAME="$TS_HOSTNAME"
88
+ elif [ -n "${DEVS_PROJECT_NAME:-}" ] && [ -n "${DEVCONTAINER_NAME:-}" ]; then
89
+ RAW_NAME="${DEVS_PROJECT_NAME}-${DEVCONTAINER_NAME}"
90
+ else
91
+ RAW_NAME="${WORKSPACE_FOLDER_NAME:-${DEVCONTAINER_NAME:-devcontainer}}"
92
+ fi
93
+ HOSTNAME_TS=$(printf '%s' "$RAW_NAME" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]-')
94
+ HOSTNAME_TS="${HOSTNAME_TS:-devcontainer}"
95
+ # Namespace every devs node under a single `devs-` prefix so one `Host devs-*`
96
+ # block in ~/.ssh/config covers all repos with this devcontainer structure.
97
+ # Idempotent: strip any existing prefix first so we never get `devs-devs-`.
98
+ HOSTNAME_TS="devs-${HOSTNAME_TS#devs-}"
99
+ TAGS="${TS_TAGS:-tag:devcontainer}"
100
+
101
+ # Start the daemon (root, detached, idempotent) if not already up.
102
+ echo "🔌 Ensuring tailscaled is running (userspace networking, root)…"
103
+ sudo -n /usr/local/bin/start-tailscaled.sh
104
+
105
+ up_args=(--authkey="$AUTHKEY" --hostname="$HOSTNAME_TS"
106
+ --advertise-tags="$TAGS" --accept-dns=false --accept-routes=false)
107
+ [ -n "$SSH_ON" ] && up_args+=(--ssh)
108
+
109
+ echo "🔗 tailscale up as '$HOSTNAME_TS' [$KEY_MODE] (tags: $TAGS${SSH_ON:+, ssh})…"
110
+ if ! "${TS[@]}" up "${up_args[@]}"; then
111
+ echo "⚠️ tailscale up failed — see /var/log/tailscaled.log. Container continues without Tailscale."
112
+ exit 0
113
+ fi
114
+
115
+ dnsname=$("${TS[@]}" status --json 2>/dev/null | jq -r '.Self.DNSName // empty' | sed 's/\.$//')
116
+ echo "✅ On tailnet as: ${dnsname:-$HOSTNAME_TS}"
117
+ [ -n "$SSH_ON" ] && echo "🔑 Tailscale SSH on — connect with: ssh node@${dnsname:-$HOSTNAME_TS}"
118
+
119
+ # Writeback handshake: record this node's resolved tailnet identity into the
120
+ # host-shared env mount (a rw bind mount), keyed by our docker container id (the
121
+ # container's $HOSTNAME, which `docker inspect --format '{{.Config.Hostname}}'`
122
+ # returns on the host). Lets `devs vscode` join container -> tailnet node by
123
+ # container id with a cheap `docker inspect` (no `docker exec`) and use the REAL
124
+ # resolved name — immune to tailnet clash-suffixing (…-eamonn-1) and TS_HOSTNAME
125
+ # overrides. Pruned by `devs clean`; stale files are simply never read.
126
+ if [ -n "${dnsname:-}" ] && [ -d /home/node/.devs-env ]; then
127
+ cid="${HOSTNAME:-$(hostname 2>/dev/null)}"
128
+ ssh_flag=false; [ -n "$SSH_ON" ] && ssh_flag=true
129
+ nodes_dir=/home/node/.devs-env/ts-nodes
130
+ if mkdir -p "$nodes_dir" 2>/dev/null && \
131
+ jq -n --arg dnsname "$dnsname" --arg short "${dnsname%%.*}" \
132
+ --arg container_id "$cid" --arg dev "${DEVCONTAINER_NAME:-}" \
133
+ --argjson ssh "$ssh_flag" \
134
+ '{dnsname:$dnsname, short:$short, container_id:$container_id, dev:$dev, ssh:$ssh}' \
135
+ > "$nodes_dir/$cid.json" 2>/dev/null; then
136
+ echo "📝 Tailnet handshake → .devs-env/ts-nodes/$cid.json"
137
+ else
138
+ echo "⚠️ Could not write tailnet handshake file (continuing)."
139
+ fi
140
+ fi
141
+
142
+ # Optionally publish a port.
143
+ if [ -n "${TS_SERVE_PORT:-}" ]; then
144
+ case "${TS_FUNNEL:-}" in
145
+ 1|true|yes)
146
+ echo "🌐 funnel (PUBLIC internet) → :$TS_SERVE_PORT"
147
+ "${TS[@]}" funnel --bg "$TS_SERVE_PORT" \
148
+ || echo "⚠️ funnel failed (needs Funnel enabled + the funnel node attribute in ACL)"
149
+ ;;
150
+ *)
151
+ echo "🔒 serve (tailnet only) → :$TS_SERVE_PORT"
152
+ "${TS[@]}" serve --bg "$TS_SERVE_PORT" \
153
+ || echo "⚠️ serve failed (needs HTTPS certificates enabled on the tailnet)"
154
+ ;;
155
+ esac
156
+ "${TS[@]}" serve status 2>/dev/null || true
157
+ fi
158
+ exit 0
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # Convenience wrapper: talk to this container's (root) tailscaled via the NOPASSWD
3
+ # sudo-script. Symlinked to `ts` on PATH, e.g. `ts status`, `ts serve --bg 5173`.
4
+ exec sudo -n /usr/local/bin/ts-cli.sh "$@"
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+ # Root helper (NOPASSWD sudo-script): make the container resolve the Tailscale
3
+ # split-DNS sample-app hostnames by prepending the MagicDNS resolver
4
+ # (100.100.100.100) to /etc/resolv.conf. See the "Split-DNS / MagicDNS" section
5
+ # of the devs Tailscale docs (docs/tailscale-setup.md) for the why.
6
+ set -uo pipefail
7
+
8
+ RESOLV=/etc/resolv.conf
9
+ MAGICDNS=100.100.100.100
10
+ SOCK=/var/run/tailscale/tailscaled.sock
11
+
12
+ # No-op unless Tailscale is actually up — otherwise we'd point the resolver at an
13
+ # unreachable server and stall every lookup behind a timeout.
14
+ if [ ! -S "$SOCK" ] || ! /usr/local/bin/tailscale --socket="$SOCK" status >/dev/null 2>&1; then
15
+ echo "ℹ️ Tailscale not up — skipping MagicDNS resolv.conf setup."
16
+ exit 0
17
+ fi
18
+
19
+ # Already prepended? (an uncommented `nameserver 100.100.100.100` line)
20
+ if grep -qE "^[[:space:]]*nameserver[[:space:]]+${MAGICDNS}([[:space:]]|\$)" "$RESOLV"; then
21
+ echo "✅ MagicDNS resolver already present in $RESOLV — nothing to do."
22
+ exit 0
23
+ fi
24
+
25
+ # Prepend as the first nameserver. glibc reads nameserver lines in order and, on
26
+ # MagicDNS SERVFAIL for non-tailnet names, falls through to the ISP servers that
27
+ # remain below. /etc/resolv.conf is a Docker bind mount, so we rewrite it in place
28
+ # (truncate + write) rather than mv'ing a new file over the mounted inode.
29
+ new=$({ printf 'nameserver %s\n' "$MAGICDNS"; cat "$RESOLV"; })
30
+ printf '%s\n' "$new" > "$RESOLV"
31
+ echo "✅ Prepended MagicDNS resolver ($MAGICDNS) to $RESOLV for tailnet split-DNS."
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # Root helper (NOPASSWD sudo-script): start tailscaled as a detached daemon if it
3
+ # isn't already running. Runs as root so Tailscale SSH can open login sessions;
4
+ # uses userspace networking so no TUN device is required. Idempotent.
5
+ set -uo pipefail
6
+
7
+ SOCK="/var/run/tailscale/tailscaled.sock"
8
+
9
+ # Idempotency / self-heal. Only skip if a tailscaled is ACTUALLY running. A plain
10
+ # container restart (host reboot, `docker start`) re-runs only PID 1 — the daemon
11
+ # process is gone, but its socket file persists on the container's writable layer.
12
+ # A naive "socket exists => already up" check then sees that stale socket, skips
13
+ # startup, and every subsequent `tailscale up` fails with "failed to connect to
14
+ # local tailscaled". So probe for a live daemon; if the socket is stale, remove it
15
+ # and start fresh.
16
+ if [ -S "$SOCK" ]; then
17
+ if pgrep -x tailscaled >/dev/null 2>&1; then
18
+ exit 0 # a real daemon is running — nothing to do
19
+ fi
20
+ echo "Removing stale tailscaled socket (no tailscaled process is running)…"
21
+ rm -f "$SOCK"
22
+ fi
23
+
24
+ # setsid + </dev/null detaches the daemon from the calling exec session so it
25
+ # survives after postCreate (or whoever invoked it) exits.
26
+ setsid /usr/local/bin/tailscaled \
27
+ --tun=userspace-networking \
28
+ --socket="$SOCK" \
29
+ --statedir=/var/lib/tailscale \
30
+ </dev/null >>/var/log/tailscaled.log 2>&1 &
31
+ disown 2>/dev/null || true
32
+
33
+ for _ in $(seq 1 30); do
34
+ [ -S "$SOCK" ] && break
35
+ sleep 0.5
36
+ done
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # Root helper (NOPASSWD sudo-script): run the tailscale CLI against the container's
3
+ # root daemon socket. The node-user `ts` wrapper calls this; start-tailscale.sh
4
+ # uses it for up/serve/status.
5
+ exec /usr/local/bin/tailscale --socket=/var/run/tailscale/tailscaled.sock "$@"
@@ -54,6 +54,9 @@ def prepare_devcontainer_environment(
54
54
  'DEVCONTAINER_NAME': dev_name,
55
55
  'GIT_REMOTE_URL': git_remote_url,
56
56
  'WORKSPACE_FOLDER_NAME': workspace_folder_name,
57
+ # org-repo form (e.g. "myorg-myapp"); used to build a tailnet
58
+ # hostname like <project>-<dev>, stable across live/copy modes.
59
+ 'DEVS_PROJECT_NAME': project_name,
57
60
  })
58
61
 
59
62
  # Set environment mount path (uses project-specific if exists, else default)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devs-common
3
- Version: 4.0.7
3
+ Version: 5.0.1
4
4
  Summary: Shared utilities for devs package ecosystem
5
5
  Author: Dan Lester
6
6
  License-Expression: MIT
@@ -15,7 +15,11 @@ devs_common/core/container.py
15
15
  devs_common/core/project.py
16
16
  devs_common/core/workspace.py
17
17
  devs_common/templates/Dockerfile
18
+ devs_common/templates/README-vscode-settings.md
18
19
  devs_common/templates/devcontainer.json
20
+ devs_common/templates/machine-settings.json
21
+ devs_common/templates/machine-settings.tmux.json
22
+ devs_common/templates/register-vscode-extensions.js
19
23
  devs_common/templates/vscode-settings-reference.md
20
24
  devs_common/templates/extensions/README.md
21
25
  devs_common/templates/extensions/devs-bridge-drop.vsix
@@ -23,16 +27,23 @@ devs_common/templates/extensions/install.sh
23
27
  devs_common/templates/scripts/copy-ssh.sh
24
28
  devs_common/templates/scripts/install-bridge-drop.sh
25
29
  devs_common/templates/scripts/post-create-wrapper.sh
30
+ devs_common/templates/scripts/post-start.sh
26
31
  devs_common/templates/scripts/python-venv-integration.sh
27
32
  devs_common/templates/scripts/screenshot-webapp.sh
33
+ devs_common/templates/scripts/setup-vscode-settings.sh
28
34
  devs_common/templates/scripts/setup-workspace.sh
35
+ devs_common/templates/scripts/start-tailscale.sh
29
36
  devs_common/templates/scripts/start-tunnel.sh
37
+ devs_common/templates/scripts/ts.sh
30
38
  devs_common/templates/scripts/tunnel-status.sh
31
39
  devs_common/templates/scripts/webapp-screenshot.py
32
40
  devs_common/templates/sudo-scripts/init-firewall.sh
33
41
  devs_common/templates/sudo-scripts/setup-devs-env.sh
42
+ devs_common/templates/sudo-scripts/setup-magicdns.sh
34
43
  devs_common/templates/sudo-scripts/setup-vscode-cache.sh
35
44
  devs_common/templates/sudo-scripts/start-services.sh
45
+ devs_common/templates/sudo-scripts/start-tailscaled.sh
46
+ devs_common/templates/sudo-scripts/ts-cli.sh
36
47
  devs_common/utils/__init__.py
37
48
  devs_common/utils/config_hash.py
38
49
  devs_common/utils/console.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devs-common"
7
- version = "4.0.7"
7
+ version = "5.0.1"
8
8
  description = "Shared utilities for devs package ecosystem"
9
9
  authors = [
10
10
  {name = "Dan Lester"},
File without changes
File without changes
File without changes