vibebox 0.0.0 → 0.0.1

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.
package/Dockerfile ADDED
@@ -0,0 +1,85 @@
1
+ FROM ubuntu:24.04
2
+
3
+ ARG TZ=UTC
4
+ ARG NODE_VERSION=24.12.0
5
+ ARG NPM_VERSION=11.7.0
6
+ ARG LOCAL_USER=coder
7
+ ARG LOCAL_UID=1000
8
+ ARG LOCAL_GID=1000
9
+ ARG LOCAL_HOME=/home/coder
10
+
11
+ ENV TZ="$TZ"
12
+
13
+ # Install system dependencies
14
+ RUN apt-get update && apt-get install -y --no-install-recommends \
15
+ curl \
16
+ dnsutils \
17
+ fzf \
18
+ gh \
19
+ git \
20
+ gnupg2 \
21
+ iproute2 \
22
+ jq \
23
+ less \
24
+ lsof \
25
+ net-tools \
26
+ procps \
27
+ python3 \
28
+ python3-pip \
29
+ sudo \
30
+ unzip \
31
+ vim \
32
+ wget \
33
+ zsh \
34
+ && apt-get clean \
35
+ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
36
+
37
+ # Create parent directory for non-standard home paths (e.g., /Users/g on macOS)
38
+ RUN mkdir -p $(dirname $LOCAL_HOME) 2>/dev/null || true
39
+
40
+ # Create group and user
41
+ RUN groupadd -g $LOCAL_GID $LOCAL_USER 2>/dev/null || true && \
42
+ GROUP_NAME=$(getent group $LOCAL_GID | cut -d: -f1) && \
43
+ useradd -m -s /bin/zsh -u $LOCAL_UID -g $GROUP_NAME -d $LOCAL_HOME $LOCAL_USER && \
44
+ echo "$LOCAL_USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
45
+
46
+ USER $LOCAL_USER
47
+ WORKDIR $LOCAL_HOME
48
+
49
+ # Install nvm and Node.js
50
+ ENV NVM_DIR="$LOCAL_HOME/.nvm"
51
+ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash && \
52
+ . "$NVM_DIR/nvm.sh" && \
53
+ nvm install $NODE_VERSION && \
54
+ nvm use $NODE_VERSION && \
55
+ nvm alias default $NODE_VERSION && \
56
+ npm install -g npm@$NPM_VERSION
57
+
58
+ # Install Claude Code and sfw
59
+ RUN curl -fsSL https://claude.ai/install.sh | bash && \
60
+ . "$NVM_DIR/nvm.sh" && npm install -g sfw
61
+
62
+ # Configure zsh
63
+ RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $LOCAL_HOME/.zshrc && \
64
+ echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $LOCAL_HOME/.zshrc && \
65
+ echo 'export PS1="vibebox:%~ %(#.#.$) "' >> $LOCAL_HOME/.zshrc && \
66
+ echo 'setopt PROMPT_SUBST' >> $LOCAL_HOME/.zshrc && \
67
+ echo 'alias npm="sfw npm"' >> $LOCAL_HOME/.zshrc && \
68
+ echo 'alias npx="sfw npx"' >> $LOCAL_HOME/.zshrc && \
69
+ echo '$HOME/.local/bin/port-monitor.sh &!' >> $LOCAL_HOME/.zshrc
70
+
71
+ # Copy container scripts
72
+ COPY --chown=$LOCAL_USER:$LOCAL_GID container-scripts/ $LOCAL_HOME/.local/bin/
73
+ RUN chmod +x $LOCAL_HOME/.local/bin/*.sh
74
+
75
+ # Environment variables
76
+ ENV SHELL=/bin/zsh \
77
+ TERM=xterm-256color \
78
+ COLORTERM=truecolor \
79
+ EDITOR=vim \
80
+ VISUAL=vim \
81
+ DEVCONTAINER=true \
82
+ PATH="$LOCAL_HOME/.local/bin:$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/bin:$PATH" \
83
+ NODE_PATH="$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules"
84
+
85
+ CMD ["/bin/zsh", "-c", "$HOME/.local/bin/startup.sh tail -f /dev/null"]
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026-present Giuseppe Gurgone
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,119 @@
1
- # Vibebox
1
+ # vibebox
2
2
 
3
- Coming soon..
3
+ A minimal CLI for running dev sandboxes and CLI agents in Docker containers. Each workspace gets its own isolated container with automatic credential sync and config isolation.
4
+
5
+ With supply chain attacks on npm becoming increasingly common, sandboxing your dev environment protects your host machine from malicious packages.
6
+
7
+ The architecture supports multiple agents. Currently only Claude Code is integrated.
8
+
9
+ [Why vibebox instead of `docker sandbox`, raw docker, or devcontainers?](#why-vibebox)
10
+
11
+ ## Requirements
12
+
13
+ - Docker Desktop
14
+ - Agent CLI on host (optional - enables shared auth/config across workspaces)
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g vibebox
20
+ ```
21
+
22
+ Add to your shell profile (optional):
23
+
24
+ ```bash
25
+ alias claude="vibebox agent run claude"
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # Agent commands
32
+ vibebox agent run <name> [args] # Run agent in sandbox
33
+ vibebox agent run claude --temp # Temporary workspace, prompts to save on exit
34
+ vibebox agent ls # List agents with install status
35
+ vibebox agent install <name> # Install agent in sandbox
36
+
37
+ # Container commands
38
+ vibebox enter # Shell into sandbox
39
+ vibebox exec <cmd> # Run command in sandbox
40
+ vibebox ls # List sandboxes
41
+ vibebox stop [--all] # Stop sandbox(es)
42
+ vibebox rm [--all] # Remove sandbox(es)
43
+ vibebox rebuild # Rebuild image with current host config
44
+ ```
45
+
46
+ ## Port Management
47
+
48
+ Default ports exposed: 5173, 3000, 3001, 4173, 8080 (dynamic host allocation)
49
+
50
+ ```bash
51
+ vibebox ports list # Show current port mappings
52
+ vibebox ports add 4200 # Add custom port
53
+ vibebox ports remove 3000 # Remove port
54
+ ```
55
+
56
+ When a service starts listening inside the container, you'll see:
57
+ ```
58
+ ● Port 5173 → http://localhost:32770
59
+ ```
60
+
61
+ For unmapped ports:
62
+ ```
63
+ ⊖ Port 9000 listening (not exposed)
64
+ ```
65
+
66
+ ## Image Build
67
+
68
+ The Docker image is built to match your host environment. When you run `vibebox rebuild`, it:
69
+
70
+ 1. Detects host Node.js version (`node --version`)
71
+ 2. Detects host npm version (`npm --version`)
72
+ 3. Detects user info (username, UID, GID, home directory)
73
+ 4. Passes these as build arguments to Docker
74
+
75
+ This ensures the container user and environment mirrors your host setup for seamless file permissions and tooling.
76
+
77
+ ## Version Management
78
+
79
+ When agent versions differ between host and sandbox:
80
+
81
+ ```
82
+ claude version mismatch! Host: 2.1.2, Sandbox: 2.1.0
83
+
84
+ Choose update strategy:
85
+ [1] Sync to higher version (2.1.2)
86
+ [2] Update both to latest
87
+ [N] Cancel
88
+ ```
89
+
90
+ ## Why vibebox?
91
+
92
+ ### vs `docker sandbox`
93
+
94
+ Docker Desktop includes an experimental `docker sandbox` command for running agents (Claude Code, Gemini) in containers. Here's why vibebox takes a different approach:
95
+
96
+ - **Transparency over black box**: `docker sandbox` is opaque. You can't see what it mounts, how it configures the environment, or debug issues. vibebox is simple TypeScript you can read and modify.
97
+ - **No `--dangerously-skip-permissions`**: `docker sandbox` runs Claude with all permissions bypassed. vibebox respects your existing Claude settings and permission model.
98
+ - **Shared credentials**: `docker sandbox` doesn't share auth with your host, so you log in fresh every time you create a sandbox. vibebox syncs credentials bidirectionally with your system keychain.
99
+ - **Works with any Docker runtime**: `docker sandbox` requires Docker Desktop. vibebox works with Docker Engine, Colima, or any docker-compliant api.
100
+ - **Interactive shell access**: `vibebox enter` drops you into a shell inside the container. Useful for debugging, running commands, or working alongside the agent.
101
+ - **Port management**: vibebox automatically exposes common dev ports with dynamic host allocation, shows you the mappings when services start, and lets you add custom ports.
102
+ - **Persistent containers**: Containers persist between sessions. Your installed packages, build artifacts, and environment stay intact until you explicitly remove them.
103
+
104
+ ### vs raw docker
105
+
106
+ - **Zero boilerplate**: No writing Dockerfiles, figuring out mount paths, or managing `docker run` flags. Just `vibebox agent run claude`.
107
+ - **Automatic credential sync**: Credentials are extracted from your system keychain and mounted into the container. No manual token copying.
108
+ - **User matching**: Container user matches your host UID/GID/home path, so file permissions just work.
109
+ - **Built-in port detection**: When a service starts listening, vibebox shows you the mapped URL. No guessing which host port maps where.
110
+
111
+ ### vs devcontainers
112
+
113
+ - **No IDE coupling**: Devcontainers are designed for VS Code. vibebox works from any terminal.
114
+ - **Simpler model**: No `devcontainer.json`, no features, no lifecycle hooks. One command to start.
115
+ - **Agent-first**: Built specifically for CLI agents with credential mounting, not general-purpose dev environments.
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # Vibebox port monitor - detects new listening ports and shows mappings
3
+ # Started in background by .zshrc
4
+
5
+ [[ -z "$VIBEBOX_PROJECT_ROOT" ]] && exit 0
6
+
7
+ mappings_file="$VIBEBOX_PROJECT_ROOT/.vibebox/port-mappings.txt"
8
+ previous=""
9
+
10
+ # Wait for shell to settle
11
+ sleep 2
12
+
13
+ # Get initial state (don't report existing ports)
14
+ previous=$(ss -tln 2>/dev/null | awk 'NR>1 {split($4,a,":"); print a[length(a)]}' | sort -nu | tr '\n' ' ')
15
+
16
+ while true; do
17
+ sleep 3
18
+
19
+ current=$(ss -tln 2>/dev/null | awk 'NR>1 {split($4,a,":"); print a[length(a)]}' | sort -nu | tr '\n' ' ')
20
+
21
+ # Find new ports
22
+ for port in $current; do
23
+ [[ -z "$port" || "$port" == "*" ]] && continue
24
+ if ! echo "$previous" | grep -qw "$port"; then
25
+ if [[ -f "$mappings_file" ]]; then
26
+ mapping=$(grep "^${port}:" "$mappings_file" 2>/dev/null)
27
+ if [[ -n "$mapping" ]]; then
28
+ echo -e "\n ● Port $port → http://localhost:${mapping#*:}"
29
+ else
30
+ echo -e "\n ⊖ Port $port listening (not exposed)"
31
+ fi
32
+ fi
33
+ fi
34
+ done
35
+
36
+ previous="$current"
37
+ done
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Run command with node if the first argument contains a "-" or is not a system command. The last
5
+ # part inside the "{}" is a workaround for the following bug in ash/dash:
6
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264
7
+ if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then
8
+ set -- node "$@"
9
+ fi
10
+
11
+ exec "$@"
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Idle watcher - exits after TIMEOUT seconds of no lock files (except detached.lock)
3
+
4
+ LOCKS_DIR="$HOME/.vibebox/locks"
5
+ DETACHED_LOCK="$LOCKS_DIR/detached.lock"
6
+ IDLE_FILE="$LOCKS_DIR/idle"
7
+ TIMEOUT=${1:-300}
8
+
9
+ # Ensure locks directory exists
10
+ mkdir -p "$LOCKS_DIR"
11
+
12
+ echo "Starting watcher with ${TIMEOUT}s idle timeout"
13
+
14
+ while true; do
15
+ # If detached.lock exists, loop forever
16
+ if [ -f "$DETACHED_LOCK" ]; then
17
+ rm -f "$IDLE_FILE"
18
+ sleep 1
19
+ continue
20
+ fi
21
+
22
+ # Count session lock files (session-*.lock)
23
+ lock_count=$(find "$LOCKS_DIR" -name 'session-*.lock' 2>/dev/null | wc -l)
24
+
25
+ if [ "$lock_count" -eq 0 ]; then
26
+ # No session locks - check/start idle timer
27
+ if [ -f "$IDLE_FILE" ]; then
28
+ idle_start=$(cat "$IDLE_FILE")
29
+ now=$(date +%s)
30
+ elapsed=$((now - idle_start))
31
+ if [ "$elapsed" -ge "$TIMEOUT" ]; then
32
+ echo "Idle timeout of ${TIMEOUT}s reached. Exiting."
33
+ rm -f "$IDLE_FILE"
34
+ exit 0
35
+ fi
36
+ else
37
+ # Start idle timer
38
+ date +%s > "$IDLE_FILE"
39
+ fi
40
+ else
41
+ # Sessions active - reset idle timer
42
+ rm -f "$IDLE_FILE"
43
+ fi
44
+
45
+ sleep 1
46
+ done
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCredentialStore = getCredentialStore;
4
+ exports.readCredentialsFile = readCredentialsFile;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_fs_1 = require("node:fs");
7
+ const node_os_1 = require("node:os");
8
+ function getCredentialStore() {
9
+ switch ((0, node_os_1.platform)()) {
10
+ case "darwin":
11
+ return {
12
+ get: (service, account) => {
13
+ try {
14
+ return (0, node_child_process_1.execSync)(`security find-generic-password -a "${account}" -s "${service}" -w`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ },
20
+ set: (service, account, value) => {
21
+ const hexData = Buffer.from(value, "utf-8").toString("hex");
22
+ const input = `add-generic-password -U -a "${account}" -s "${service}" -X "${hexData}"\n`;
23
+ (0, node_child_process_1.execSync)("security -i", {
24
+ input,
25
+ stdio: ["pipe", "pipe", "pipe"],
26
+ });
27
+ },
28
+ };
29
+ case "win32":
30
+ return {
31
+ get: (service, account) => {
32
+ try {
33
+ const cmd = `powershell -Command "(Get-StoredCredential -Target '${service}:${account}').GetNetworkCredential().Password"`;
34
+ return (0, node_child_process_1.execSync)(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ },
40
+ set: (service, account, value) => {
41
+ const cmd = `powershell -Command "New-StoredCredential -Target '${service}:${account}' -Password '${value}' -Persist LocalMachine"`;
42
+ (0, node_child_process_1.execSync)(cmd);
43
+ },
44
+ };
45
+ default:
46
+ // Linux: secret-tool (libsecret)
47
+ return {
48
+ get: (service, account) => {
49
+ try {
50
+ return (0, node_child_process_1.execSync)(`secret-tool lookup service "${service}" account "${account}"`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ },
56
+ set: (service, account, value) => {
57
+ (0, node_child_process_1.execSync)(`echo -n "${value}" | secret-tool store --label="${service}" service "${service}" account "${account}"`);
58
+ },
59
+ };
60
+ }
61
+ }
62
+ function readCredentialsFile(path) {
63
+ try {
64
+ if (!(0, node_fs_1.existsSync)(path))
65
+ return null;
66
+ return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claude = void 0;
4
+ exports.getCredentialPaths = getCredentialPaths;
5
+ const auth_1 = require("../auth");
6
+ const node_child_process_1 = require("node:child_process");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const node_crypto_1 = require("node:crypto");
11
+ const KEYCHAIN_SERVICES = [
12
+ "Claude Code-credentials",
13
+ "claude-code-credentials",
14
+ "claude.ai",
15
+ ];
16
+ function getKeychainServiceName() {
17
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
18
+ if (configDir) {
19
+ const hash = (0, node_crypto_1.createHash)("sha256")
20
+ .update(configDir)
21
+ .digest("hex")
22
+ .substring(0, 8);
23
+ return `Claude Code-credentials-${hash}`;
24
+ }
25
+ return "Claude Code-credentials";
26
+ }
27
+ function extractFromKeychain() {
28
+ const store = (0, auth_1.getCredentialStore)();
29
+ const user = (0, node_os_1.userInfo)().username;
30
+ for (const svc of KEYCHAIN_SERVICES) {
31
+ const data = store.get(svc, user);
32
+ if (data) {
33
+ try {
34
+ const creds = JSON.parse(data);
35
+ if (creds.claudeAiOauth?.accessToken)
36
+ return creds;
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function compareFreshness(first, second) {
46
+ const firstExpiry = first?.claudeAiOauth?.expiresAt;
47
+ const secondExpiry = second?.claudeAiOauth?.expiresAt;
48
+ if (!first?.claudeAiOauth?.accessToken && !second?.claudeAiOauth?.accessToken) {
49
+ return "unknown";
50
+ }
51
+ if (!first?.claudeAiOauth?.accessToken)
52
+ return "second";
53
+ if (!second?.claudeAiOauth?.accessToken)
54
+ return "first";
55
+ if (firstExpiry === undefined && secondExpiry === undefined)
56
+ return "equal";
57
+ if (firstExpiry === undefined)
58
+ return "second";
59
+ if (secondExpiry === undefined)
60
+ return "first";
61
+ if (firstExpiry > secondExpiry)
62
+ return "first";
63
+ if (secondExpiry > firstExpiry)
64
+ return "second";
65
+ return "equal";
66
+ }
67
+ exports.claude = {
68
+ name: "claude",
69
+ command: "claude",
70
+ configDir: (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude"),
71
+ isInstalledOnHost: () => {
72
+ try {
73
+ (0, node_child_process_1.execSync)("claude --version", { stdio: "pipe" });
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ },
80
+ dockerArgs: ({ workspace, home, containerOnly }) => {
81
+ const claudeDir = (0, node_path_1.join)(home, ".claude");
82
+ const configCopy = (0, node_path_1.join)(workspace, ".vibebox", "claude.json");
83
+ const config = (0, node_path_1.join)(home, ".claude.json");
84
+ // Container-only mode: minimal mounts, container manages its own ~/.claude
85
+ if (containerOnly) {
86
+ return [
87
+ // Mount config copy (not the original, to prevent corruption)
88
+ "-v", `${configCopy}:${config}`,
89
+ ];
90
+ }
91
+ // Host mode: mount ~/.claude with selective hiding for isolation
92
+ const projectFolder = workspace.replace(/\//g, "-");
93
+ const emptyDir = (0, node_path_1.join)(workspace, ".vibebox", "empty");
94
+ const emptyFile = (0, node_path_1.join)(workspace, ".vibebox", "empty-file");
95
+ const credentials = (0, node_path_1.join)(claudeDir, ".credentials.json");
96
+ return [
97
+ // Mount ~/.claude with selective hiding
98
+ "-v", `${claudeDir}:${claudeDir}`,
99
+ "-v", `${emptyDir}:${claudeDir}/projects`,
100
+ "-v", `${emptyFile}:${claudeDir}/history.jsonl`,
101
+ "-v", `${claudeDir}/projects/${projectFolder}:${claudeDir}/projects/${projectFolder}`,
102
+ // Mount credentials
103
+ "-v", `${credentials}:${credentials}`,
104
+ // Mount config copy (not the original, to prevent corruption)
105
+ "-v", `${configCopy}:${config}`,
106
+ ];
107
+ },
108
+ install: (containerName) => {
109
+ console.log("Installing Claude Code...");
110
+ (0, node_child_process_1.execSync)(`docker exec ${containerName} bash -c "curl -fsSL https://claude.ai/install.sh | sh"`, { stdio: "inherit" });
111
+ },
112
+ setup: ({ workspace, containerOnly }) => {
113
+ const home = (0, node_os_1.homedir)();
114
+ const claudeDir = (0, node_path_1.join)(home, ".claude");
115
+ const credFile = (0, node_path_1.join)(claudeDir, ".credentials.json");
116
+ // Ensure directories exist
117
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(workspace, ".vibebox", "empty"), { recursive: true });
118
+ // Create empty overlay file
119
+ const emptyFile = (0, node_path_1.join)(workspace, ".vibebox", "empty-file");
120
+ if (!(0, node_fs_1.existsSync)(emptyFile))
121
+ (0, node_fs_1.writeFileSync)(emptyFile, "");
122
+ // Create minimal config to skip onboarding and trust prompts
123
+ const configCopy = (0, node_path_1.join)(workspace, ".vibebox", "claude.json");
124
+ const minimalConfig = {
125
+ hasCompletedOnboarding: true,
126
+ projects: {
127
+ [workspace]: {
128
+ allowedTools: [],
129
+ hasTrustDialogAccepted: true,
130
+ hasCompletedProjectOnboarding: true,
131
+ },
132
+ },
133
+ };
134
+ (0, node_fs_1.writeFileSync)(configCopy, JSON.stringify(minimalConfig, null, 2));
135
+ // Container-only mode: skip credential sync, agent will prompt for login
136
+ if (containerOnly)
137
+ return;
138
+ // Host mode: sync credentials from system credential store
139
+ (0, node_fs_1.mkdirSync)(claudeDir, { recursive: true });
140
+ // Clean up if credentials file is a directory (Claude bug)
141
+ if ((0, node_fs_1.existsSync)(credFile) && (0, node_fs_1.lstatSync)(credFile).isDirectory()) {
142
+ (0, node_fs_1.rmSync)(credFile, { recursive: true, force: true });
143
+ }
144
+ // Sync credentials from system credential store (macOS only for now)
145
+ if (process.platform === "darwin") {
146
+ const keychainCreds = extractFromKeychain();
147
+ const fileCreds = (0, auth_1.readCredentialsFile)(credFile);
148
+ const freshness = compareFreshness(keychainCreds, fileCreds);
149
+ if (freshness === "first" || freshness === "equal") {
150
+ if (!keychainCreds)
151
+ throw new Error("No credentials. Run: claude auth login");
152
+ (0, node_fs_1.writeFileSync)(credFile, JSON.stringify(keychainCreds, null, 2));
153
+ }
154
+ else if (freshness === "second" && fileCreds) {
155
+ // File is fresher, sync back to keychain
156
+ const store = (0, auth_1.getCredentialStore)();
157
+ store.set(getKeychainServiceName(), (0, node_os_1.userInfo)().username, JSON.stringify(fileCreds));
158
+ }
159
+ else if (!keychainCreds && !fileCreds) {
160
+ throw new Error("No credentials. Run: claude auth login");
161
+ }
162
+ }
163
+ else {
164
+ if (!(0, node_fs_1.existsSync)(credFile)) {
165
+ throw new Error("No credentials. Run: claude auth login");
166
+ }
167
+ }
168
+ },
169
+ versionCommand: "claude --version",
170
+ };
171
+ function getCredentialPaths() {
172
+ return {
173
+ credentials: (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", ".credentials.json"),
174
+ config: (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude.json"),
175
+ };
176
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agents = void 0;
4
+ exports.detectInstalledAgents = detectInstalledAgents;
5
+ exports.isAgentInstalled = isAgentInstalled;
6
+ const node_child_process_1 = require("node:child_process");
7
+ const claude_1 = require("./claude");
8
+ exports.agents = {
9
+ claude: claude_1.claude,
10
+ };
11
+ function detectInstalledAgents() {
12
+ return Object.entries(exports.agents)
13
+ .filter(([_, agent]) => agent.isInstalledOnHost())
14
+ .map(([name]) => name);
15
+ }
16
+ function isAgentInstalled(containerName, agent) {
17
+ try {
18
+ (0, node_child_process_1.execSync)(`docker exec ${containerName} which ${agent.command}`, { stdio: "pipe" });
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/auth.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCredentialPaths = void 0;
4
+ exports.ensureCredentials = ensureCredentials;
5
+ // Re-export from Claude agent for backward compatibility
6
+ const claude_1 = require("./agents/claude");
7
+ Object.defineProperty(exports, "getCredentialPaths", { enumerable: true, get: function () { return claude_1.getCredentialPaths; } });
8
+ function ensureCredentials() {
9
+ // Delegates to Claude agent's setup with current working directory
10
+ claude_1.claude.setup?.({ workspace: process.cwd() });
11
+ }