dev-bubble 0.7.6__tar.gz → 0.7.9__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.
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/.claude/CLAUDE.md +13 -8
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/CHANGELOG.md +1 -1
- {dev_bubble-0.7.6/dev_bubble.egg-info → dev_bubble-0.7.9}/PKG-INFO +7 -9
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/README.md +6 -8
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/SPEC.md +4 -5
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/__init__.py +1 -1
- dev_bubble-0.7.9/bubble/ai.py +455 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/auth_proxy.py +52 -52
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/clean.py +3 -1
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/cli.py +129 -66
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/security_cmd.py +6 -4
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/settings.py +117 -49
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/config.py +24 -17
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/container_helpers.py +8 -6
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/data/skill.md +11 -1
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/finalization.py +10 -5
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/github_token.py +41 -23
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/builder.py +22 -14
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/base.sh +5 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/lean-toolchain.sh +1 -1
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/lean.sh +3 -1
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/python.sh +2 -2
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/vscode.sh +65 -135
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/provisioning.py +2 -2
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/remote.py +9 -9
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/runtime/base.py +22 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/runtime/incus.py +47 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/security.py +187 -34
- {dev_bubble-0.7.6 → dev_bubble-0.7.9/dev_bubble.egg-info}/PKG-INFO +7 -9
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/dev_bubble.egg-info/SOURCES.txt +2 -2
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/conftest.py +2 -2
- dev_bubble-0.7.9/tests/test_ai.py +727 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_auth_proxy.py +17 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_branch_no_target.py +1 -1
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_claude_projects_symlink.py +28 -28
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_config.py +92 -106
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_github_token.py +163 -24
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_mounts.py +8 -8
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_remote.py +6 -6
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_security.py +260 -151
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_tools.py +54 -18
- dev_bubble-0.7.6/bubble/claude.py +0 -271
- dev_bubble-0.7.6/tests/test_claude.py +0 -393
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/.github/workflows/ci.yml +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/.github/workflows/publish.yml +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/.gitignore +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/LICENSE +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/__main__.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/automation.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/clone.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/cloud.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/cloud_types.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/__init__.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/cloud_cmd.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/completion.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/doctor.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/images.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/infrastructure.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/lifecycle.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/list_cmd.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/relay_cmd.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/remote_cmd.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/commands/status_cmd.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/default_repos.json +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/git_store.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/graphql_validator.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/hooks/__init__.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/hooks/lean.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/hooks/python.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/image_management.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/__init__.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/cloud-init.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/claude.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/codex.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/elan.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/emacs.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/gh.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/neovim.sh +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/images/scripts/tools/pins.json +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/lean.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/lifecycle.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/naming.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/native.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/network.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/notices.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/output.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/relay.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/repo_registry.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/runtime/__init__.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/runtime/colima.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/setup.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/skill.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/spinner.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/target.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/token_store.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/tools.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/tunnel.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/bubble/vscode.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/config/com.bubble.git-update.plist +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/config/com.bubble.image-refresh.plist +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/config/com.bubble.relay-daemon.plist +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/conftest.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/dev_bubble.egg-info/dependency_links.txt +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/dev_bubble.egg-info/entry_points.txt +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/dev_bubble.egg-info/requires.txt +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/dev_bubble.egg-info/top_level.txt +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/pyproject.toml +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/setup.cfg +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_build_lock.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_cloud.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_completion.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_customize.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_editor.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_git_store.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_graphql_validator.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_hooks.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_integration.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_lifecycle.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_list_columns.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_list_remote.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_multi_target.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_naming.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_native.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_network.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_notices.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_relay.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_repo_registry.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_skill.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_spinner.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_status.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_target.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_tunnel.py +0 -0
- {dev_bubble-0.7.6 → dev_bubble-0.7.9}/tests/test_vscode.py +0 -0
|
@@ -121,14 +121,19 @@ Bubbles can run on a remote machine instead of locally. The `--ssh HOST` flag (o
|
|
|
121
121
|
Users can place a `customize.sh` script at `~/.bubble/customize.sh` to run custom setup in all container images. The script runs as root as the final step when building any image (base, lean, lean-v4.X.Y). This lets users add tools, dotfiles, shell config, etc. without forking image scripts. The script's content hash is tracked in `~/.bubble/customize-hash`; on `bubble open`, if the hash differs from the stored value, a background rebuild of the base image is triggered (same pattern as VS Code commit hash drift). Code is in `builder.py` (`customize_hash()`, `_run_customize_script()`).
|
|
122
122
|
|
|
123
123
|
### GitHub Auth Proxy
|
|
124
|
-
The auth proxy (`auth_proxy.py`) provides repo-scoped GitHub authentication without injecting the host's token into containers. It's an HTTP reverse proxy that runs on the host
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
124
|
+
The auth proxy (`auth_proxy.py`) provides repo-scoped GitHub authentication without injecting the host's token into containers. It's an HTTP reverse proxy that runs on the host. The access level is controlled by the unified `github` security setting (`security.py`), which picks one level from a graduated escalation ladder:
|
|
125
|
+
|
|
126
|
+
| `github` level | Behavior |
|
|
127
|
+
|----------------|----------|
|
|
128
|
+
| `off` | no GitHub access at all |
|
|
129
|
+
| `basic` | git push/pull only (proxy rewrites, repo-scoped) |
|
|
130
|
+
| `rest` | + repo-scoped REST API |
|
|
131
|
+
| `allowlist-read-graphql` | + allowlisted GraphQL queries |
|
|
132
|
+
| `allowlist-write-graphql` | + allowlisted GraphQL mutations (default) |
|
|
133
|
+
| `write-graphql` | + arbitrary GraphQL, no allowlist filtering |
|
|
134
|
+
| `direct` | inject the raw token, no proxy |
|
|
135
|
+
|
|
136
|
+
`auto` defaults to `allowlist-write-graphql`. The old `github-auth`, `github-api`, and `github-token-inject` settings are deprecated but migrated automatically.
|
|
132
137
|
|
|
133
138
|
**Git flow:** Container git → `url.insteadOf` rewrites to `http://127.0.0.1:7654/git/...` → proxy validates `X-Bubble-Token` header → checks path matches allowed `owner/repo` → adds `Authorization: token <real-token>` → forwards to `https://github.com` → returns response.
|
|
134
139
|
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
- Templated Claude prompts for issues and PRs (#33)
|
|
219
219
|
- PR bubbles now auto-inject a Claude prompt that checks CI status and summarizes PR comments
|
|
220
220
|
- User-customizable prompt templates via `~/.bubble/templates/issue.txt` and `~/.bubble/templates/pr.txt`
|
|
221
|
-
- Issue template placeholders: `{owner}`, `{repo}`, `{issue_num}`, `{title}`, `{body}`, `{comments}`, `{comments_section}`, `{branch}`
|
|
221
|
+
- Issue template placeholders: `{owner}`, `{repo}`, `{issue_num}`, `{title}`, `{body}`, `{comments}`, `{comments_section}`, `{branch}`, `{instructions}`
|
|
222
222
|
- PR template placeholders: `{owner}`, `{repo}`, `{pr_num}`, `{title}`, `{body}`, `{branch}`
|
|
223
223
|
- Falls back to built-in defaults when no custom template exists
|
|
224
224
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dev-bubble
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.9
|
|
4
4
|
Summary: Containerized development environments powered by Incus
|
|
5
5
|
Author-email: Kim Morrison <kim@tqft.net>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -43,7 +43,7 @@ We assume that you work using VSCode, emacs, or neovim, and that you collaborate
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
# Install
|
|
46
|
-
uv tool install
|
|
46
|
+
uv tool install git+https://github.com/kim-em/bubble.git
|
|
47
47
|
|
|
48
48
|
# Open a bubble for a GitHub PR — just paste the URL, and you get a containerized VSCode window!
|
|
49
49
|
bubble https://github.com/leanprover-community/mathlib4/pull/35219
|
|
@@ -124,11 +124,9 @@ bubble pop mathlib4-pr-35219
|
|
|
124
124
|
## Development Install
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
# Install from
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# For development
|
|
131
|
-
uv pip install -e '.[dev]'
|
|
127
|
+
# Install from a local clone (editable — always uses latest code)
|
|
128
|
+
git clone https://github.com/kim-em/bubble.git
|
|
129
|
+
uv tool install --force --editable bubble
|
|
132
130
|
```
|
|
133
131
|
|
|
134
132
|
## How It Works
|
|
@@ -263,10 +261,10 @@ bubble mathlib4/issues/42
|
|
|
263
261
|
|
|
264
262
|
Claude is instructed to read the issue, implement a fix on the `issue-<number>` branch, and open a PR. This turns `bubble 42` (for an issue) into an autonomous coding agent workflow.
|
|
265
263
|
|
|
266
|
-
You can also provide a custom prompt for any bubble via the `
|
|
264
|
+
You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
|
|
267
265
|
|
|
268
266
|
```bash
|
|
269
|
-
|
|
267
|
+
BUBBLE_AI_PROMPT="Refactor the parser module" bubble leanprover/lean4
|
|
270
268
|
```
|
|
271
269
|
|
|
272
270
|
Requirements: Claude Code must be installed in the container (see tool settings above), the `gh` CLI must be available on the host (for fetching issue/PR metadata), and the default VS Code editor must be used. With `--shell` or `--no-interactive`, the prompt is not injected. If `gh` is unavailable or the API call fails, bubble proceeds without injecting a prompt.
|
|
@@ -9,7 +9,7 @@ We assume that you work using VSCode, emacs, or neovim, and that you collaborate
|
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
# Install
|
|
12
|
-
uv tool install
|
|
12
|
+
uv tool install git+https://github.com/kim-em/bubble.git
|
|
13
13
|
|
|
14
14
|
# Open a bubble for a GitHub PR — just paste the URL, and you get a containerized VSCode window!
|
|
15
15
|
bubble https://github.com/leanprover-community/mathlib4/pull/35219
|
|
@@ -90,11 +90,9 @@ bubble pop mathlib4-pr-35219
|
|
|
90
90
|
## Development Install
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
# Install from
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# For development
|
|
97
|
-
uv pip install -e '.[dev]'
|
|
93
|
+
# Install from a local clone (editable — always uses latest code)
|
|
94
|
+
git clone https://github.com/kim-em/bubble.git
|
|
95
|
+
uv tool install --force --editable bubble
|
|
98
96
|
```
|
|
99
97
|
|
|
100
98
|
## How It Works
|
|
@@ -229,10 +227,10 @@ bubble mathlib4/issues/42
|
|
|
229
227
|
|
|
230
228
|
Claude is instructed to read the issue, implement a fix on the `issue-<number>` branch, and open a PR. This turns `bubble 42` (for an issue) into an autonomous coding agent workflow.
|
|
231
229
|
|
|
232
|
-
You can also provide a custom prompt for any bubble via the `
|
|
230
|
+
You can also provide a custom prompt for any bubble via the `BUBBLE_AI_PROMPT` environment variable:
|
|
233
231
|
|
|
234
232
|
```bash
|
|
235
|
-
|
|
233
|
+
BUBBLE_AI_PROMPT="Refactor the parser module" bubble leanprover/lean4
|
|
236
234
|
```
|
|
237
235
|
|
|
238
236
|
Requirements: Claude Code must be installed in the container (see tool settings above), the `gh` CLI must be available on the host (for fetching issue/PR metadata), and the default VS Code editor must be used. With `--shell` or `--no-interactive`, the prompt is not injected. If `gh` is unavailable or the API call fails, bubble proceeds without injecting a prompt.
|
|
@@ -56,7 +56,7 @@ equivalent to `bubble open <url>`.
|
|
|
56
56
|
| `-b`, `--new-branch` | string | | Create a new branch |
|
|
57
57
|
| `--base` | string | | Base branch for `-b` |
|
|
58
58
|
| `--mount` | string (repeatable) | | Mount host dir into container |
|
|
59
|
-
| `--
|
|
59
|
+
| `--ai-config/--no-ai-config` | flag | enabled | Mount AI provider configs read-only |
|
|
60
60
|
| `--claude-credentials/--no-claude-credentials` | flag | enabled | Mount Claude credentials |
|
|
61
61
|
| `--codex-credentials/--no-codex-credentials` | flag | enabled | Mount Codex credentials |
|
|
62
62
|
| `--ssh HOST` | string | | Run on remote host |
|
|
@@ -912,7 +912,7 @@ validation already confirmed the repo exists).
|
|
|
912
912
|
|
|
913
913
|
### 10.1 Claude Code config mounting
|
|
914
914
|
|
|
915
|
-
When `--
|
|
915
|
+
When `--ai-config` is enabled (default), mount specific items from
|
|
916
916
|
`~/.claude/` into `/home/user/.claude/` read-only:
|
|
917
917
|
- `CLAUDE.md`, `settings.json`, `skills/`, `keybindings.json`, `commands/`
|
|
918
918
|
|
|
@@ -1087,11 +1087,10 @@ Configuration is managed through dedicated subcommands rather than a generic
|
|
|
1087
1087
|
get/set interface:
|
|
1088
1088
|
|
|
1089
1089
|
- `bubble tools set TOOL yes|no|auto` — configure tool installation
|
|
1090
|
-
- `bubble
|
|
1091
|
-
- `bubble codex credentials on|off` — toggle Codex credential mounting
|
|
1090
|
+
- `bubble ai credentials on|off [--provider claude|codex]` — toggle AI credential mounting
|
|
1092
1091
|
- `bubble security set NAME on|off|auto` — configure security settings
|
|
1093
1092
|
- `bubble config set KEY VALUE` — set security settings (alias)
|
|
1094
|
-
- `bubble config symlink-
|
|
1093
|
+
- `bubble config symlink-ai-projects` — symlink AI projects directory
|
|
1095
1094
|
|
|
1096
1095
|
---
|
|
1097
1096
|
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""AI provider integration for bubble containers.
|
|
2
|
+
|
|
3
|
+
Handles prompt generation and task injection for the configured preferred
|
|
4
|
+
AI provider (default: Claude Code). Provider-neutral where possible;
|
|
5
|
+
provider-specific details (binary names, env vars, prompt file paths)
|
|
6
|
+
are dispatched via the ``[ai] preferred`` config key.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import shlex
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .config import DATA_DIR
|
|
16
|
+
from .runtime.base import ContainerRuntime
|
|
17
|
+
|
|
18
|
+
# Provider-specific task commands.
|
|
19
|
+
# Each reads the prompt from .vscode/ai-prompt.txt, runs the AI tool
|
|
20
|
+
# with autonomous permissions, then deletes the prompt file.
|
|
21
|
+
_TASK_COMMANDS = {
|
|
22
|
+
"claude": (
|
|
23
|
+
"test -f .vscode/ai-prompt.txt && ANTHROPIC_API_KEY= CLAUDECODE="
|
|
24
|
+
' claude --dangerously-skip-permissions "$(cat .vscode/ai-prompt.txt)"'
|
|
25
|
+
" && rm -f .vscode/ai-prompt.txt"
|
|
26
|
+
),
|
|
27
|
+
"codex": (
|
|
28
|
+
"test -f .vscode/ai-prompt.txt &&"
|
|
29
|
+
' codex --approval-mode full-auto "$(cat .vscode/ai-prompt.txt)"'
|
|
30
|
+
" && rm -f .vscode/ai-prompt.txt"
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
SUPPORTED_PROVIDERS = frozenset(_TASK_COMMANDS)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _task_command_for(provider: str) -> str:
|
|
38
|
+
"""Return the VS Code task shell command for the given AI provider.
|
|
39
|
+
|
|
40
|
+
Raises ``ValueError`` for unknown providers so typos are caught early.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
return _TASK_COMMANDS[provider]
|
|
44
|
+
except KeyError:
|
|
45
|
+
supported = ", ".join(sorted(SUPPORTED_PROVIDERS))
|
|
46
|
+
raise ValueError(f"Unknown AI provider {provider!r} (supported: {supported})") from None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
AI_TASK_COMMAND = _TASK_COMMANDS["claude"]
|
|
50
|
+
|
|
51
|
+
TEMPLATES_DIR = DATA_DIR / "templates"
|
|
52
|
+
|
|
53
|
+
# Ordered autonomy levels. Each level implies all lower levels.
|
|
54
|
+
AUTONOMY_LEVELS = ("read", "plan", "implement", "pr", "merge")
|
|
55
|
+
|
|
56
|
+
# Valid values for the second_opinion setting.
|
|
57
|
+
SECOND_OPINION_VALUES = ("auto", "on", "off")
|
|
58
|
+
|
|
59
|
+
# Issue prompt instructions keyed by autonomy level.
|
|
60
|
+
_ISSUE_INSTRUCTIONS = {
|
|
61
|
+
"read": (
|
|
62
|
+
"Please read and understand the issue. "
|
|
63
|
+
"Summarize the problem and any relevant context, but take no further action."
|
|
64
|
+
),
|
|
65
|
+
"plan": (
|
|
66
|
+
"Please read and understand the issue, then propose a plan to fix it. "
|
|
67
|
+
"Describe what files need to change and how, but do not implement anything yet."
|
|
68
|
+
),
|
|
69
|
+
"implement": (
|
|
70
|
+
"Please claim this issue (assign it to yourself if possible), "
|
|
71
|
+
"then implement a fix or feature as described. "
|
|
72
|
+
"Work on a branch named `{branch}`. Do not commit or open a PR."
|
|
73
|
+
),
|
|
74
|
+
"pr": (
|
|
75
|
+
"Please claim this issue (assign it to yourself if possible), "
|
|
76
|
+
"then implement a fix or feature as described. "
|
|
77
|
+
"Work on a branch named `{branch}`, and open a PR when done."
|
|
78
|
+
),
|
|
79
|
+
"merge": (
|
|
80
|
+
"Please claim this issue (assign it to yourself if possible), "
|
|
81
|
+
"then implement a fix or feature as described. "
|
|
82
|
+
"Work on a branch named `{branch}`, open a PR, "
|
|
83
|
+
"rebase onto the default branch, watch CI, and merge when it passes."
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_SECOND_OPINION_SUFFIX = (
|
|
88
|
+
"\n\nBefore proceeding, get a second opinion from another AI "
|
|
89
|
+
"(e.g. Codex) to review your approach."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Default issue prompt template. Placeholders:
|
|
93
|
+
# {owner}, {repo}, {issue_num}, {title}, {body}, {comments},
|
|
94
|
+
# {comments_section} (pre-formatted, empty when no comments), {branch},
|
|
95
|
+
# {instructions}
|
|
96
|
+
_DEFAULT_ISSUE_TEMPLATE = (
|
|
97
|
+
'Please read and understand GitHub issue #{issue_num}: "{title}".\n'
|
|
98
|
+
"\n"
|
|
99
|
+
"Issue description:\n"
|
|
100
|
+
"{body}\n"
|
|
101
|
+
"{comments_section}"
|
|
102
|
+
"\n{instructions}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Default PR prompt template. Placeholders:
|
|
106
|
+
# {owner}, {repo}, {pr_num}, {title}, {body}, {branch}
|
|
107
|
+
_DEFAULT_PR_TEMPLATE = (
|
|
108
|
+
'You are working on PR #{pr_num}: "{title}" in {owner}/{repo}'
|
|
109
|
+
" on branch `{branch}`.\n"
|
|
110
|
+
"\n"
|
|
111
|
+
"PR description:\n"
|
|
112
|
+
"{body}\n"
|
|
113
|
+
"\n"
|
|
114
|
+
"Please:\n"
|
|
115
|
+
"1. Check the CI status for this PR using the GitHub API.\n"
|
|
116
|
+
"2. Build a numbered table of all PR comments (both review-level and"
|
|
117
|
+
" inline) with columns for: comment number, author, a summary of the"
|
|
118
|
+
" comment, and whether it has a response yet.\n"
|
|
119
|
+
"\n"
|
|
120
|
+
"This gives an overview of where things stand with this PR."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _load_template(kind: str) -> str | None:
|
|
125
|
+
"""Load a custom template from ~/.bubble/templates/<kind>.txt.
|
|
126
|
+
|
|
127
|
+
Returns the template string, or None if no custom template exists.
|
|
128
|
+
Falls back gracefully on permission errors or encoding issues.
|
|
129
|
+
"""
|
|
130
|
+
path = TEMPLATES_DIR / f"{kind}.txt"
|
|
131
|
+
if path.is_file():
|
|
132
|
+
try:
|
|
133
|
+
return path.read_text()
|
|
134
|
+
except (OSError, UnicodeError):
|
|
135
|
+
from .output import detail
|
|
136
|
+
|
|
137
|
+
detail(f"Warning: could not read template {path}, using default.", err=True)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Matches simple {name} placeholders — no attribute access, indexing, or format specs.
|
|
142
|
+
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _render_template(template: str, **kwargs) -> str:
|
|
146
|
+
"""Render a template by substituting simple {name} placeholders.
|
|
147
|
+
|
|
148
|
+
Only plain identifiers are replaced (e.g. {title}, {pr_num}).
|
|
149
|
+
Attribute access ({x.y}), indexing ({x[0]}), and format specs ({x:>10})
|
|
150
|
+
are left untouched. Unknown placeholders are also left as-is.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def _replace(m: re.Match) -> str:
|
|
154
|
+
key = m.group(1)
|
|
155
|
+
if key in kwargs:
|
|
156
|
+
return str(kwargs[key])
|
|
157
|
+
return m.group(0) # leave unknown placeholders as-is
|
|
158
|
+
|
|
159
|
+
return _PLACEHOLDER_RE.sub(_replace, template)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _fetch_github_item(owner: str, repo: str, endpoint: str, jq: str) -> str | None:
|
|
163
|
+
"""Fetch a GitHub API endpoint via gh CLI, returning stdout or None on failure."""
|
|
164
|
+
try:
|
|
165
|
+
result = subprocess.run(
|
|
166
|
+
["gh", "api", f"repos/{owner}/{repo}/{endpoint}", "--jq", jq],
|
|
167
|
+
capture_output=True,
|
|
168
|
+
text=True,
|
|
169
|
+
timeout=15,
|
|
170
|
+
)
|
|
171
|
+
if result.returncode != 0:
|
|
172
|
+
return None
|
|
173
|
+
return result.stdout
|
|
174
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _resolve_second_opinion(second_opinion: str, config: dict | None = None) -> bool:
|
|
179
|
+
"""Resolve the second_opinion setting to a boolean.
|
|
180
|
+
|
|
181
|
+
'on' → True, 'off' → False, 'auto' → True if codex will be
|
|
182
|
+
available in the container (checked via tool resolution, not host PATH).
|
|
183
|
+
"""
|
|
184
|
+
if second_opinion == "on":
|
|
185
|
+
return True
|
|
186
|
+
if second_opinion == "off":
|
|
187
|
+
return False
|
|
188
|
+
# auto: check if codex is resolved as an enabled tool
|
|
189
|
+
if config is not None:
|
|
190
|
+
from .tools import resolve_tools
|
|
191
|
+
|
|
192
|
+
return "codex" in resolve_tools(config)
|
|
193
|
+
# Fallback when no config available: check host PATH
|
|
194
|
+
import shutil
|
|
195
|
+
|
|
196
|
+
return shutil.which("codex") is not None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def generate_issue_prompt(
|
|
200
|
+
owner: str,
|
|
201
|
+
repo: str,
|
|
202
|
+
issue_num: str,
|
|
203
|
+
branch: str,
|
|
204
|
+
autonomy: str = "plan",
|
|
205
|
+
second_opinion: str = "auto",
|
|
206
|
+
config: dict | None = None,
|
|
207
|
+
) -> str | None:
|
|
208
|
+
"""Fetch GitHub issue details and generate an AI prompt.
|
|
209
|
+
|
|
210
|
+
Returns the prompt string, or None if the issue can't be fetched.
|
|
211
|
+
Uses a custom template from ~/.bubble/templates/issue.txt if present,
|
|
212
|
+
otherwise falls back to the built-in default.
|
|
213
|
+
|
|
214
|
+
The autonomy level controls what action instructions are included:
|
|
215
|
+
read, plan, implement, pr, merge.
|
|
216
|
+
"""
|
|
217
|
+
raw = _fetch_github_item(owner, repo, f"issues/{issue_num}", ".title,.body")
|
|
218
|
+
if raw is None:
|
|
219
|
+
return None
|
|
220
|
+
lines = raw.split("\n", 1)
|
|
221
|
+
title = lines[0] if lines else ""
|
|
222
|
+
body = lines[1].strip() if len(lines) > 1 else ""
|
|
223
|
+
|
|
224
|
+
# Fetch comments (first 4000 chars)
|
|
225
|
+
comments_text = ""
|
|
226
|
+
raw_comments = _fetch_github_item(owner, repo, f"issues/{issue_num}/comments", ".[].body")
|
|
227
|
+
if raw_comments and raw_comments.strip():
|
|
228
|
+
comments_text = raw_comments.strip()[:4000]
|
|
229
|
+
|
|
230
|
+
comments_section = ""
|
|
231
|
+
if comments_text:
|
|
232
|
+
comments_section = f"\nComments:\n{comments_text}\n"
|
|
233
|
+
|
|
234
|
+
# Build instructions from autonomy level
|
|
235
|
+
if autonomy not in AUTONOMY_LEVELS:
|
|
236
|
+
autonomy = "plan"
|
|
237
|
+
instructions = _ISSUE_INSTRUCTIONS[autonomy]
|
|
238
|
+
# The instructions may contain {branch} placeholder
|
|
239
|
+
instructions = instructions.format(branch=branch)
|
|
240
|
+
|
|
241
|
+
# Append second opinion request if enabled
|
|
242
|
+
if _resolve_second_opinion(second_opinion, config=config):
|
|
243
|
+
instructions += _SECOND_OPINION_SUFFIX
|
|
244
|
+
|
|
245
|
+
custom_template = _load_template("issue")
|
|
246
|
+
template = custom_template or _DEFAULT_ISSUE_TEMPLATE
|
|
247
|
+
|
|
248
|
+
prompt = _render_template(
|
|
249
|
+
template,
|
|
250
|
+
owner=owner,
|
|
251
|
+
repo=repo,
|
|
252
|
+
issue_num=issue_num,
|
|
253
|
+
title=title,
|
|
254
|
+
body=body,
|
|
255
|
+
comments=comments_text,
|
|
256
|
+
comments_section=comments_section,
|
|
257
|
+
branch=branch,
|
|
258
|
+
instructions=instructions,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# If a custom template didn't use {instructions}, append them so
|
|
262
|
+
# autonomy/second-opinion settings are never silently lost.
|
|
263
|
+
if custom_template and "{instructions}" not in custom_template:
|
|
264
|
+
prompt = prompt.rstrip() + "\n\n" + instructions
|
|
265
|
+
|
|
266
|
+
return prompt
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def generate_pr_prompt(owner: str, repo: str, pr_num: str, branch: str) -> str | None:
|
|
270
|
+
"""Fetch GitHub PR details and generate an AI prompt.
|
|
271
|
+
|
|
272
|
+
Returns the prompt string, or None if the PR can't be fetched.
|
|
273
|
+
Uses a custom template from ~/.bubble/templates/pr.txt if present,
|
|
274
|
+
otherwise falls back to the built-in default.
|
|
275
|
+
"""
|
|
276
|
+
raw = _fetch_github_item(owner, repo, f"pulls/{pr_num}", ".title,.body")
|
|
277
|
+
if raw is None:
|
|
278
|
+
return None
|
|
279
|
+
lines = raw.split("\n", 1)
|
|
280
|
+
title = lines[0] if lines else ""
|
|
281
|
+
body = lines[1].strip() if len(lines) > 1 else ""
|
|
282
|
+
|
|
283
|
+
template = _load_template("pr") or _DEFAULT_PR_TEMPLATE
|
|
284
|
+
return _render_template(
|
|
285
|
+
template,
|
|
286
|
+
owner=owner,
|
|
287
|
+
repo=repo,
|
|
288
|
+
pr_num=pr_num,
|
|
289
|
+
title=title,
|
|
290
|
+
body=body,
|
|
291
|
+
branch=branch,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# Known-safe keys to copy from the host's ~/.claude.json.
|
|
296
|
+
# Only cosmetic/UX settings — no credentials, MCP config, or host-specific paths.
|
|
297
|
+
_CLAUDE_JSON_SAFE_KEYS = frozenset(
|
|
298
|
+
{
|
|
299
|
+
"theme",
|
|
300
|
+
"hasCompletedOnboarding",
|
|
301
|
+
"numStartups",
|
|
302
|
+
"preferredNotifChannel",
|
|
303
|
+
"autoUpdaterStatus",
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def setup_claude_settings(
|
|
309
|
+
runtime: ContainerRuntime,
|
|
310
|
+
container: str,
|
|
311
|
+
project_dir: str,
|
|
312
|
+
):
|
|
313
|
+
"""Pre-populate ~/.claude.json in the container to skip the first-run wizard.
|
|
314
|
+
|
|
315
|
+
Copies allowlisted settings (theme, onboarding state, etc.) from the host's
|
|
316
|
+
~/.claude.json if it exists. Always ensures hasCompletedOnboarding=True
|
|
317
|
+
and pre-trusts the project directory. This runs for ALL bubbles, not just
|
|
318
|
+
those with AI task injection.
|
|
319
|
+
|
|
320
|
+
Best-effort: failures are logged but do not abort bubble creation.
|
|
321
|
+
"""
|
|
322
|
+
host_claude_json = Path.home() / ".claude.json"
|
|
323
|
+
|
|
324
|
+
# Extract only allowlisted keys from host settings
|
|
325
|
+
settings: dict = {}
|
|
326
|
+
if host_claude_json.is_file():
|
|
327
|
+
try:
|
|
328
|
+
host_data = json.loads(host_claude_json.read_text())
|
|
329
|
+
if isinstance(host_data, dict):
|
|
330
|
+
settings = {k: v for k, v in host_data.items() if k in _CLAUDE_JSON_SAFE_KEYS}
|
|
331
|
+
except (OSError, json.JSONDecodeError):
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# Ensure onboarding is marked complete
|
|
335
|
+
settings["hasCompletedOnboarding"] = True
|
|
336
|
+
n = settings.get("numStartups", 0)
|
|
337
|
+
settings["numStartups"] = (n if isinstance(n, int) else 0) + 1
|
|
338
|
+
|
|
339
|
+
# Pre-trust the project directory
|
|
340
|
+
settings["projects"] = {
|
|
341
|
+
project_dir: {"hasTrustDialogAccepted": True, "allowedTools": []},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Write to container (best-effort — don't abort bubble creation on failure)
|
|
345
|
+
settings_json = shlex.quote(json.dumps(settings, indent=2))
|
|
346
|
+
try:
|
|
347
|
+
runtime.exec(
|
|
348
|
+
container,
|
|
349
|
+
["su", "-", "user", "-c", f"printf '%s' {settings_json} > ~/.claude.json"],
|
|
350
|
+
)
|
|
351
|
+
except Exception:
|
|
352
|
+
from .output import detail
|
|
353
|
+
|
|
354
|
+
detail("Warning: could not pre-populate Claude Code settings.", err=True)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def inject_ai_task(
|
|
358
|
+
runtime: ContainerRuntime,
|
|
359
|
+
container: str,
|
|
360
|
+
project_dir: str,
|
|
361
|
+
prompt: str,
|
|
362
|
+
config: dict | None = None,
|
|
363
|
+
quiet: bool = False,
|
|
364
|
+
):
|
|
365
|
+
"""Inject AI auto-start task into a container's VS Code configuration.
|
|
366
|
+
|
|
367
|
+
Dispatches to the configured preferred AI provider (default: Claude).
|
|
368
|
+
|
|
369
|
+
- Writes prompt to .vscode/ai-prompt.txt
|
|
370
|
+
- Creates/updates .vscode/tasks.json with AI task (runOn: folderOpen)
|
|
371
|
+
- Configures .vscode/settings.json for automatic tasks
|
|
372
|
+
- Adds generated files to git exclude
|
|
373
|
+
- Pre-trusts the project directory in the preferred provider's config
|
|
374
|
+
"""
|
|
375
|
+
provider = "claude"
|
|
376
|
+
if config:
|
|
377
|
+
provider = config.get("ai", {}).get("preferred", "claude")
|
|
378
|
+
|
|
379
|
+
task_command = _task_command_for(provider)
|
|
380
|
+
|
|
381
|
+
q_dir = shlex.quote(project_dir)
|
|
382
|
+
q_prompt = shlex.quote(prompt)
|
|
383
|
+
|
|
384
|
+
# Create .vscode directory
|
|
385
|
+
runtime.exec(container, ["su", "-", "user", "-c", f"mkdir -p {q_dir}/.vscode"])
|
|
386
|
+
|
|
387
|
+
# Write prompt to file
|
|
388
|
+
runtime.exec(
|
|
389
|
+
container,
|
|
390
|
+
["su", "-", "user", "-c", f"printf '%s' {q_prompt} > {q_dir}/.vscode/ai-prompt.txt"],
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Create or update tasks.json with AI task
|
|
394
|
+
ai_task = {
|
|
395
|
+
"label": "AI",
|
|
396
|
+
"type": "shell",
|
|
397
|
+
"command": task_command,
|
|
398
|
+
"runOptions": {"runOn": "folderOpen"},
|
|
399
|
+
"presentation": {"reveal": "always", "panel": "dedicated"},
|
|
400
|
+
}
|
|
401
|
+
tasks_json_str = shlex.quote(json.dumps(ai_task))
|
|
402
|
+
|
|
403
|
+
# Script: if tasks.json exists, add AI task (removing old Claude and AI labels);
|
|
404
|
+
# otherwise create new file
|
|
405
|
+
script = (
|
|
406
|
+
f"cd {q_dir} && "
|
|
407
|
+
f"if [ -f .vscode/tasks.json ]; then "
|
|
408
|
+
f' python3 -c "'
|
|
409
|
+
f"import json,sys; "
|
|
410
|
+
f"t=json.load(open('.vscode/tasks.json')); "
|
|
411
|
+
f"t['tasks']=[x for x in t.get('tasks',[])"
|
|
412
|
+
f" if x.get('label') not in ('Claude','AI')]+[json.loads(sys.argv[1])]; "
|
|
413
|
+
f"json.dump(t,open('.vscode/tasks.json','w'),indent=2)"
|
|
414
|
+
f'" {tasks_json_str}; '
|
|
415
|
+
f"else "
|
|
416
|
+
f' python3 -c "'
|
|
417
|
+
f"import json,sys; "
|
|
418
|
+
f"json.dump({{'version':'2.0.0','tasks':[json.loads(sys.argv[1])]}},open('.vscode/tasks.json','w'),indent=2)"
|
|
419
|
+
f'" {tasks_json_str}; '
|
|
420
|
+
f"fi"
|
|
421
|
+
)
|
|
422
|
+
runtime.exec(container, ["su", "-", "user", "-c", script])
|
|
423
|
+
|
|
424
|
+
# Configure settings.json for automatic tasks
|
|
425
|
+
settings_script = (
|
|
426
|
+
f"cd {q_dir} && "
|
|
427
|
+
f'python3 -c "'
|
|
428
|
+
f"import json,os; "
|
|
429
|
+
f"p='.vscode/settings.json'; "
|
|
430
|
+
f"s=json.load(open(p)) if os.path.exists(p) else {{}}; "
|
|
431
|
+
f"s['terminal.integrated.defaultLocation']='editor'; "
|
|
432
|
+
f"s['task.allowAutomaticTasks']='on'; "
|
|
433
|
+
f"json.dump(s,open(p,'w'),indent=2)"
|
|
434
|
+
f'"'
|
|
435
|
+
)
|
|
436
|
+
runtime.exec(container, ["su", "-", "user", "-c", settings_script])
|
|
437
|
+
|
|
438
|
+
# Add generated files to git exclude
|
|
439
|
+
exclude_script = (
|
|
440
|
+
f"cd {q_dir} && "
|
|
441
|
+
f"GIT_DIR=$(git rev-parse --git-dir) && "
|
|
442
|
+
f"mkdir -p $GIT_DIR/info && "
|
|
443
|
+
f"for f in .vscode/ai-prompt.txt .vscode/claude-prompt.txt"
|
|
444
|
+
f" .vscode/settings.json .vscode/tasks.json; do "
|
|
445
|
+
f' grep -qxF "$f" $GIT_DIR/info/exclude 2>/dev/null'
|
|
446
|
+
f' || echo "$f" >> $GIT_DIR/info/exclude; '
|
|
447
|
+
f"done"
|
|
448
|
+
)
|
|
449
|
+
runtime.exec(container, ["su", "-", "user", "-c", exclude_script])
|
|
450
|
+
|
|
451
|
+
if not quiet:
|
|
452
|
+
from .output import detail
|
|
453
|
+
|
|
454
|
+
label = provider.capitalize()
|
|
455
|
+
detail(f"{label} task injected (will start on VS Code folder open).")
|