devlaunch 0.0.8__tar.gz → 0.0.10__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.
@@ -184,3 +184,6 @@ logs/
184
184
 
185
185
  # uv is not this project's package manager (pixi.lock is authoritative)
186
186
  uv.lock
187
+
188
+ # Agent worktrees (local scratch; never committed)
189
+ .claude/worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlaunch
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary: DevLaunch - A streamlined CLI for devpod workspaces
5
5
  Project-URL: Source, https://github.com/blooop/devlaunch
6
6
  Project-URL: Home, https://github.com/blooop/devlaunch
@@ -33,7 +33,7 @@ A streamlined CLI for [devpod](https://devpod.sh) with intuitive autocomplete an
33
33
  [![GitHub pull-requests merged](https://badgen.net/github/merged-prs/blooop/devlaunch)](https://github.com/blooop/devlaunch/pulls?q=is%3Amerged)
34
34
  [![GitHub release](https://img.shields.io/github/release/blooop/devlaunch.svg)](https://GitHub.com/blooop/devlaunch/releases/)
35
35
  [![PyPI](https://img.shields.io/pypi/v/devlaunch)](https://pypi.org/project/devlaunch/)
36
- [![Conda](https://img.shields.io/badge/conda-v0.0.8-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
36
+ [![Conda](https://img.shields.io/badge/conda-v0.0.9-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
37
37
  [![License](https://img.shields.io/github/license/blooop/devlaunch)](https://opensource.org/license/mit/)
38
38
  [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/downloads/)
39
39
  [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
@@ -60,7 +60,7 @@ If `devpod` is not on `PATH`, every command that needs it prints a single instal
60
60
 
61
61
  ### Shell Completions
62
62
 
63
- After installation, set up shell completions:
63
+ After installation, set up shell completions for `dl` and `aid`:
64
64
 
65
65
  ```bash
66
66
  dl --install
@@ -76,6 +76,44 @@ dl <user/repo> <cmd> # Run workspace command (stop, code, etc.)
76
76
  dl <user/repo> -- <command> # Run shell command in workspace
77
77
  ```
78
78
 
79
+ ## aid: start a coding agent in a workspace
80
+
81
+ `aid` is `dl` with a coding agent started for you:
82
+
83
+ ```bash
84
+ aid <user/repo>[@branch] [prompt...] # Open the workspace, start the agent
85
+ ```
86
+
87
+ It is a shortcut, not a second launcher. `aid` rewrites its command line into a
88
+ `dl` one and hands it to `dl` itself, so
89
+
90
+ ```bash
91
+ aid blooop/devlaunch@fix/42 fix the flaky test
92
+ ```
93
+
94
+ is exactly
95
+
96
+ ```bash
97
+ dl blooop/devlaunch@fix/42 -- claude 'fix the flaky test'
98
+ ```
99
+
100
+ That means an `aid` workspace *is* the `dl` workspace: same clone, same workspace
101
+ id, same container — started if stopped, attached to if already running, and never
102
+ rebuilt just because `aid` asked for it. Anything `dl` learns, `aid` gets.
103
+
104
+ | Option | Description |
105
+ |--------|-------------|
106
+ | `--claude`, `--codex`, `--gemini` | Pick the agent (default: `claude`) |
107
+ | `--devcontainer <variant\|path>` | Passed through to `dl` |
108
+ | `DEVLAUNCH_AID_AGENT=<agent>` | Change the default agent |
109
+
110
+ Everything after the workspace is the prompt, flags and all, so it never needs
111
+ quoting to survive `aid`'s own parsing. Managing workspaces — listing, stopping,
112
+ deleting, VS Code — stays with `dl`.
113
+
114
+ The agent's CLI has to be installed in the container; `aid` runs it there, it does
115
+ not install it.
116
+
79
117
  ## Workspace Sources
80
118
 
81
119
  ```bash
@@ -85,6 +123,50 @@ dl user/repo@branch # Create from specific branch
85
123
  dl ./path # Create from local path
86
124
  ```
87
125
 
126
+ ## Workspace IDs
127
+
128
+ `dl user/repo@branch` derives one id that names both the devpod workspace (what you
129
+ see in `dl --ls`) and the clone directory under `~/.cache/devlaunch/repos/`:
130
+
131
+ ```
132
+ <repo-slug>-<branch-slug>-<syllables> at most 38 characters
133
+
134
+ blooop/devlaunch@main -> devlaunch-main-zovomobo
135
+ blooop/devlaunch@feature/auth -> devlaunch-feature-auth-poliseno
136
+ blooop/devlaunch@feature-auth -> devlaunch-feature-auth-nesatabe
137
+ blooop/test_renv@nb4 -> test-renv-nb4-polenita
138
+ kinisi-robotics/kinisi_ros@ags-devcontainer-tooling-support
139
+ -> kinisi-ros-ags-devcontainer-t-lenevere
140
+ blooop/devlaunch@dependabot/github_actions/codecov/codecov-action-6
141
+ -> devlaunch-dependabot-codecov-sifivasa
142
+ ```
143
+
144
+ The eight-character syllable suffix is a hash of the full `(owner, repo, branch)` triple.
145
+ It is what makes the id unique: the readable part is shortened to fit the length limit,
146
+ and shortening it does not affect whether two branches share an id. Long branch names
147
+ drop whole `/`-separated middle segments before losing characters, so the part that
148
+ identifies the branch survives. Note the third and fourth lines above: `feature/auth` and
149
+ `feature-auth` read the same once slugged but are different branches, and they get
150
+ different ids.
151
+
152
+ Owner and repo are matched case-insensitively, the way GitHub treats them, so
153
+ `dl NVIDIA/cuda-samples@main` and `dl nvidia/cuda-samples@main` are the same workspace.
154
+ Branch names are case-sensitive, because git refs are.
155
+
156
+ URL specs (`dl github.com/owner/repo`) get an id in the same shape, with the suffix
157
+ hashed over the URL.
158
+
159
+ The id is also the container hostname, so it stays well inside the 38-character budget
160
+ to leave room for tools that add their own prefixes.
161
+
162
+ Branch names must be safe as both git refs and directory names — a name with a space or
163
+ a leading dash is rejected rather than quietly rewritten.
164
+
165
+ > **Upgrading:** this id format is new. Existing workspaces and clone directories were
166
+ > named by the previous scheme and will get new ids, so `dl user/repo@branch` creates a
167
+ > fresh workspace and leaves the old container behind. Remove stale ones with
168
+ > `dl <old-id> rm`, which still finds and deletes the old clone directory.
169
+
88
170
  ## Workspace Commands
89
171
 
90
172
  | Command | Description |
@@ -161,7 +243,23 @@ stays in place — including one it was given before you set
161
243
  | `dl --prune-worktrees [days]` | Remove unused worktrees (default: 30 days) |
162
244
  | `dl --refresh` | Refresh completion cache |
163
245
  | `dl --help, -h` | Show this help |
164
- | `dl --version` | Show version |
246
+ | `dl --version` | Show version (an editable install also names the tree it runs from) |
247
+
248
+ A released install prints the version and nothing else. An install made in
249
+ editable mode says so and names the checkout it resolves to, so two builds of
250
+ the same version are told apart at a glance:
251
+
252
+ ```bash
253
+ $ dl --version
254
+ dl 0.0.9
255
+
256
+ $ dl-next --version # editable install of a working tree
257
+ dl 0.0.9 (dev, editable from /path/to/your/devlaunch)
258
+ ```
259
+
260
+ `aid --version` reports the same thing under its own name. The provenance comes
261
+ from the installed package's own PEP 610 metadata; an install that records none
262
+ just prints the bare version.
165
263
 
166
264
  ## Examples
167
265
 
@@ -183,6 +281,7 @@ dl blooop/devlaunch stop # Stop workspace
183
281
  - **GitHub Shorthand**: Use `owner/repo` instead of full URLs - automatically expands to `github.com/owner/repo`
184
282
  - **Branch Support**: Specify branches with `owner/repo@branch` syntax
185
283
  - **Fast Autocomplete**: Completion cache for ~3ms response time (vs ~700ms without cache)
284
+ - **One Round-Trip Per Question**: every `devpod` call costs ~0.45s, far more than `dl` itself, so a command reads the workspace list at most once — and `dl <ws> -- <cmd>` skips the extra round-trip that names an interactive prompt, since a one-shot command has none
186
285
 
187
286
  ## Worktree Backend
188
287
 
@@ -218,6 +317,19 @@ After running `dl --install`, you get intelligent tab completion:
218
317
  - File/directory paths when starting with `./`, `/`, or `~`
219
318
  - All global flags (`--ls`, `--install`, etc.) and workspace commands
220
319
 
320
+ ### How the completion cache stays current
321
+
322
+ The data behind completions lives in `~/.cache/devlaunch/completions.json`, and
323
+ building it means a `git ls-remote` per known repo — seconds of work. So it is
324
+ rebuilt in the background at most once an hour (the same interval the worktree
325
+ backend uses for lazy fetches), and at most once per `dl` invocation. Commands
326
+ that change your workspaces (starting, stopping or deleting one) rebuild it as
327
+ soon as they finish, regardless of when it was last built. Commands with no use
328
+ for it — `dl --help`, `dl --version` — do not touch it at all.
329
+
330
+ A branch created on a remote in the last hour may therefore not be offered yet.
331
+ `dl --refresh` rebuilds the cache immediately and ignores the interval.
332
+
221
333
  ## Development
222
334
 
223
335
  This project uses [pixi](https://pixi.sh) for environment management.
@@ -10,7 +10,7 @@ A streamlined CLI for [devpod](https://devpod.sh) with intuitive autocomplete an
10
10
  [![GitHub pull-requests merged](https://badgen.net/github/merged-prs/blooop/devlaunch)](https://github.com/blooop/devlaunch/pulls?q=is%3Amerged)
11
11
  [![GitHub release](https://img.shields.io/github/release/blooop/devlaunch.svg)](https://GitHub.com/blooop/devlaunch/releases/)
12
12
  [![PyPI](https://img.shields.io/pypi/v/devlaunch)](https://pypi.org/project/devlaunch/)
13
- [![Conda](https://img.shields.io/badge/conda-v0.0.8-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
13
+ [![Conda](https://img.shields.io/badge/conda-v0.0.9-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
14
14
  [![License](https://img.shields.io/github/license/blooop/devlaunch)](https://opensource.org/license/mit/)
15
15
  [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue)](https://www.python.org/downloads/)
16
16
  [![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
@@ -37,7 +37,7 @@ If `devpod` is not on `PATH`, every command that needs it prints a single instal
37
37
 
38
38
  ### Shell Completions
39
39
 
40
- After installation, set up shell completions:
40
+ After installation, set up shell completions for `dl` and `aid`:
41
41
 
42
42
  ```bash
43
43
  dl --install
@@ -53,6 +53,44 @@ dl <user/repo> <cmd> # Run workspace command (stop, code, etc.)
53
53
  dl <user/repo> -- <command> # Run shell command in workspace
54
54
  ```
55
55
 
56
+ ## aid: start a coding agent in a workspace
57
+
58
+ `aid` is `dl` with a coding agent started for you:
59
+
60
+ ```bash
61
+ aid <user/repo>[@branch] [prompt...] # Open the workspace, start the agent
62
+ ```
63
+
64
+ It is a shortcut, not a second launcher. `aid` rewrites its command line into a
65
+ `dl` one and hands it to `dl` itself, so
66
+
67
+ ```bash
68
+ aid blooop/devlaunch@fix/42 fix the flaky test
69
+ ```
70
+
71
+ is exactly
72
+
73
+ ```bash
74
+ dl blooop/devlaunch@fix/42 -- claude 'fix the flaky test'
75
+ ```
76
+
77
+ That means an `aid` workspace *is* the `dl` workspace: same clone, same workspace
78
+ id, same container — started if stopped, attached to if already running, and never
79
+ rebuilt just because `aid` asked for it. Anything `dl` learns, `aid` gets.
80
+
81
+ | Option | Description |
82
+ |--------|-------------|
83
+ | `--claude`, `--codex`, `--gemini` | Pick the agent (default: `claude`) |
84
+ | `--devcontainer <variant\|path>` | Passed through to `dl` |
85
+ | `DEVLAUNCH_AID_AGENT=<agent>` | Change the default agent |
86
+
87
+ Everything after the workspace is the prompt, flags and all, so it never needs
88
+ quoting to survive `aid`'s own parsing. Managing workspaces — listing, stopping,
89
+ deleting, VS Code — stays with `dl`.
90
+
91
+ The agent's CLI has to be installed in the container; `aid` runs it there, it does
92
+ not install it.
93
+
56
94
  ## Workspace Sources
57
95
 
58
96
  ```bash
@@ -62,6 +100,50 @@ dl user/repo@branch # Create from specific branch
62
100
  dl ./path # Create from local path
63
101
  ```
64
102
 
103
+ ## Workspace IDs
104
+
105
+ `dl user/repo@branch` derives one id that names both the devpod workspace (what you
106
+ see in `dl --ls`) and the clone directory under `~/.cache/devlaunch/repos/`:
107
+
108
+ ```
109
+ <repo-slug>-<branch-slug>-<syllables> at most 38 characters
110
+
111
+ blooop/devlaunch@main -> devlaunch-main-zovomobo
112
+ blooop/devlaunch@feature/auth -> devlaunch-feature-auth-poliseno
113
+ blooop/devlaunch@feature-auth -> devlaunch-feature-auth-nesatabe
114
+ blooop/test_renv@nb4 -> test-renv-nb4-polenita
115
+ kinisi-robotics/kinisi_ros@ags-devcontainer-tooling-support
116
+ -> kinisi-ros-ags-devcontainer-t-lenevere
117
+ blooop/devlaunch@dependabot/github_actions/codecov/codecov-action-6
118
+ -> devlaunch-dependabot-codecov-sifivasa
119
+ ```
120
+
121
+ The eight-character syllable suffix is a hash of the full `(owner, repo, branch)` triple.
122
+ It is what makes the id unique: the readable part is shortened to fit the length limit,
123
+ and shortening it does not affect whether two branches share an id. Long branch names
124
+ drop whole `/`-separated middle segments before losing characters, so the part that
125
+ identifies the branch survives. Note the third and fourth lines above: `feature/auth` and
126
+ `feature-auth` read the same once slugged but are different branches, and they get
127
+ different ids.
128
+
129
+ Owner and repo are matched case-insensitively, the way GitHub treats them, so
130
+ `dl NVIDIA/cuda-samples@main` and `dl nvidia/cuda-samples@main` are the same workspace.
131
+ Branch names are case-sensitive, because git refs are.
132
+
133
+ URL specs (`dl github.com/owner/repo`) get an id in the same shape, with the suffix
134
+ hashed over the URL.
135
+
136
+ The id is also the container hostname, so it stays well inside the 38-character budget
137
+ to leave room for tools that add their own prefixes.
138
+
139
+ Branch names must be safe as both git refs and directory names — a name with a space or
140
+ a leading dash is rejected rather than quietly rewritten.
141
+
142
+ > **Upgrading:** this id format is new. Existing workspaces and clone directories were
143
+ > named by the previous scheme and will get new ids, so `dl user/repo@branch` creates a
144
+ > fresh workspace and leaves the old container behind. Remove stale ones with
145
+ > `dl <old-id> rm`, which still finds and deletes the old clone directory.
146
+
65
147
  ## Workspace Commands
66
148
 
67
149
  | Command | Description |
@@ -138,7 +220,23 @@ stays in place — including one it was given before you set
138
220
  | `dl --prune-worktrees [days]` | Remove unused worktrees (default: 30 days) |
139
221
  | `dl --refresh` | Refresh completion cache |
140
222
  | `dl --help, -h` | Show this help |
141
- | `dl --version` | Show version |
223
+ | `dl --version` | Show version (an editable install also names the tree it runs from) |
224
+
225
+ A released install prints the version and nothing else. An install made in
226
+ editable mode says so and names the checkout it resolves to, so two builds of
227
+ the same version are told apart at a glance:
228
+
229
+ ```bash
230
+ $ dl --version
231
+ dl 0.0.9
232
+
233
+ $ dl-next --version # editable install of a working tree
234
+ dl 0.0.9 (dev, editable from /path/to/your/devlaunch)
235
+ ```
236
+
237
+ `aid --version` reports the same thing under its own name. The provenance comes
238
+ from the installed package's own PEP 610 metadata; an install that records none
239
+ just prints the bare version.
142
240
 
143
241
  ## Examples
144
242
 
@@ -160,6 +258,7 @@ dl blooop/devlaunch stop # Stop workspace
160
258
  - **GitHub Shorthand**: Use `owner/repo` instead of full URLs - automatically expands to `github.com/owner/repo`
161
259
  - **Branch Support**: Specify branches with `owner/repo@branch` syntax
162
260
  - **Fast Autocomplete**: Completion cache for ~3ms response time (vs ~700ms without cache)
261
+ - **One Round-Trip Per Question**: every `devpod` call costs ~0.45s, far more than `dl` itself, so a command reads the workspace list at most once — and `dl <ws> -- <cmd>` skips the extra round-trip that names an interactive prompt, since a one-shot command has none
163
262
 
164
263
  ## Worktree Backend
165
264
 
@@ -195,6 +294,19 @@ After running `dl --install`, you get intelligent tab completion:
195
294
  - File/directory paths when starting with `./`, `/`, or `~`
196
295
  - All global flags (`--ls`, `--install`, etc.) and workspace commands
197
296
 
297
+ ### How the completion cache stays current
298
+
299
+ The data behind completions lives in `~/.cache/devlaunch/completions.json`, and
300
+ building it means a `git ls-remote` per known repo — seconds of work. So it is
301
+ rebuilt in the background at most once an hour (the same interval the worktree
302
+ backend uses for lazy fetches), and at most once per `dl` invocation. Commands
303
+ that change your workspaces (starting, stopping or deleting one) rebuild it as
304
+ soon as they finish, regardless of when it was last built. Commands with no use
305
+ for it — `dl --help`, `dl --version` — do not touch it at all.
306
+
307
+ A branch created on a remote in the last hour may therefore not be offered yet.
308
+ `dl --refresh` rebuilds the cache immediately and ignores the interval.
309
+
198
310
  ## Development
199
311
 
200
312
  This project uses [pixi](https://pixi.sh) for environment management.
@@ -0,0 +1,212 @@
1
+ """aid - AI Develop: `dl`, with a coding agent started for you.
2
+
3
+ aid is a shortcut, not a second launcher. It rewrites its own command line into
4
+ a `dl` one and hands that to :func:`devlaunch.dl.main`, so:
5
+
6
+ aid owner/repo@branch fix the flaky test
7
+
8
+ is exactly
9
+
10
+ dl owner/repo@branch -- claude 'fix the flaky test'
11
+
12
+ Everything that decides how a workspace is obtained — the bare repo cache, the
13
+ worktree clone, the workspace id, the devpod container, the fast attach to one
14
+ that is already running, the forwarded gh login — happens inside dl, once. There
15
+ is no container machinery in this module, deliberately: an aid that built its own
16
+ would drift from dl and start rebuilding containers dl would have reused.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ import shlex
24
+ import sys
25
+ from dataclasses import dataclass, field
26
+ from typing import Dict, List, Optional
27
+
28
+ from . import dl
29
+
30
+ # Base command per agent. The prompt, when there is one, is appended as a single
31
+ # quoted argument; each of these CLIs takes an initial prompt that way and then
32
+ # drops into its interactive session.
33
+ AGENT_COMMANDS: Dict[str, List[str]] = {
34
+ "claude": ["claude"],
35
+ "codex": ["codex"],
36
+ "gemini": ["gemini", "--prompt-interactive"],
37
+ }
38
+
39
+ # Flags that pick the agent, e.g. `aid --gemini owner/repo ...`.
40
+ AGENT_FLAGS: Dict[str, str] = {f"--{name}": name for name in AGENT_COMMANDS}
41
+
42
+ DEFAULT_AGENT = "claude"
43
+
44
+ # Overrides the default agent for people who do not want to type a flag every
45
+ # time. A --flag on the command line still wins.
46
+ AGENT_ENV_VAR = "DEVLAUNCH_AID_AGENT"
47
+
48
+
49
+ class UsageError(Exception):
50
+ """The aid command line could not be understood."""
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class AidArgs:
55
+ """An aid command line, split into the pieces the dl one is built from.
56
+
57
+ Only ever built by parse_aid_args, which refuses a command line without a
58
+ workspace, so spec is a str here and every field is ready to use.
59
+ """
60
+
61
+ spec: str
62
+ agent: str = DEFAULT_AGENT
63
+ # dl options seen before the spec (`--devcontainer x`), passed through as-is.
64
+ dl_options: List[str] = field(default_factory=list)
65
+ prompt: str = ""
66
+
67
+
68
+ def default_agent(env: Optional[Dict[str, str]] = None) -> str:
69
+ """Return the agent to use when no flag picks one."""
70
+ environ = os.environ if env is None else env
71
+ name = environ.get(AGENT_ENV_VAR, "").strip()
72
+ if not name:
73
+ return DEFAULT_AGENT
74
+ if name not in AGENT_COMMANDS:
75
+ raise UsageError(
76
+ f"{AGENT_ENV_VAR}={name!r} is not a known agent. "
77
+ f"Choose one of: {', '.join(sorted(AGENT_COMMANDS))}."
78
+ )
79
+ return name
80
+
81
+
82
+ def parse_aid_args(argv: List[str], env: Optional[Dict[str, str]] = None) -> AidArgs:
83
+ """Split an aid command line into agent, dl options, workspace spec and prompt.
84
+
85
+ The first argument that is neither an agent flag nor a dl option is the
86
+ workspace spec; everything after it is the prompt, flags and all, so a
87
+ prompt never has to be quoted to protect it from aid's own parsing.
88
+ """
89
+ agent = default_agent(env)
90
+ dl_options: List[str] = []
91
+ spec: Optional[str] = None
92
+ i = 0
93
+ while i < len(argv):
94
+ arg = argv[i]
95
+ if arg in AGENT_FLAGS:
96
+ agent = AGENT_FLAGS[arg]
97
+ i += 1
98
+ continue
99
+ if arg in dl.DL_VALUE_OPTIONS:
100
+ # Take the value with it; dl reports a missing one.
101
+ dl_options.extend(argv[i : i + 2])
102
+ i += 2
103
+ continue
104
+ if arg.startswith("-"):
105
+ dl_options.append(arg)
106
+ i += 1
107
+ continue
108
+ spec = arg
109
+ i += 1
110
+ break
111
+
112
+ if spec is None:
113
+ raise UsageError("aid needs a workspace: aid <user/repo>[@branch] [prompt]")
114
+
115
+ return AidArgs(spec=spec, agent=agent, dl_options=dl_options, prompt=" ".join(argv[i:]))
116
+
117
+
118
+ def build_agent_command(agent: str, prompt: str = "") -> str:
119
+ """Build the shell command that starts the agent inside the workspace.
120
+
121
+ Returned as one shell string because that is what dl's `-- <command>` form
122
+ takes. The prompt is quoted here rather than reassembled by the caller, so
123
+ the words the user typed reach the agent as the single argument they meant.
124
+ """
125
+ try:
126
+ command = list(AGENT_COMMANDS[agent])
127
+ except KeyError:
128
+ raise UsageError(
129
+ f"Unknown agent {agent!r}. Choose one of: {', '.join(sorted(AGENT_COMMANDS))}."
130
+ ) from None
131
+ if not prompt:
132
+ # No prompt to be interactive about: start the agent's plain session.
133
+ # gemini's --prompt-interactive would be a syntax error without one.
134
+ return shlex.quote(command[0])
135
+ command.append(prompt)
136
+ return shlex.join(command)
137
+
138
+
139
+ def build_dl_args(parsed: AidArgs) -> List[str]:
140
+ """Turn a parsed aid command line into the dl one that does the work."""
141
+ return [
142
+ *parsed.dl_options,
143
+ parsed.spec,
144
+ "--",
145
+ build_agent_command(parsed.agent, parsed.prompt),
146
+ ]
147
+
148
+
149
+ def print_help() -> None:
150
+ """Print usage help."""
151
+ agents = ", ".join(f"--{name}" for name in sorted(AGENT_COMMANDS))
152
+ print(
153
+ f"""aid - AI Develop: start a coding agent in a devlaunch workspace
154
+
155
+ aid is a shortcut for `dl <workspace> -- <agent> '<prompt>'`. The workspace is
156
+ opened by dl itself, so it is the same workspace, container and clone that
157
+ `dl <workspace>` gives you — started if it is stopped, attached to if it is
158
+ already running, and never rebuilt just because aid asked for it.
159
+
160
+ Usage:
161
+ aid <user/repo>[@branch] [prompt...] Open the workspace and start the agent
162
+ aid <workspace> [prompt...] Same, for an existing workspace or ./path
163
+
164
+ Options:
165
+ {agents}
166
+ Pick the agent (default: {DEFAULT_AGENT})
167
+ --devcontainer <variant|path> Passed through to dl
168
+ --help, -h Show this help
169
+ --version Show version
170
+
171
+ Environment:
172
+ {AGENT_ENV_VAR}=<agent> Change the default agent
173
+
174
+ Examples:
175
+ aid blooop/devlaunch # Start {DEFAULT_AGENT} in the workspace
176
+ aid blooop/devlaunch@fix/42 fix the bug # Open the branch, hand over the prompt
177
+ aid --gemini ./my-project explain this # Pick a different agent
178
+
179
+ Everything else — listing, stopping, deleting, VS Code — is dl's job:
180
+ dl --help
181
+ """
182
+ )
183
+
184
+
185
+ def main(argv: Optional[List[str]] = None) -> int:
186
+ """Entry point for the aid command."""
187
+ args = sys.argv[1:] if argv is None else list(argv)
188
+
189
+ if not args or args[0] in ("--help", "-h"):
190
+ print_help()
191
+ return 0 if args else 1
192
+
193
+ if args[0] == "--version":
194
+ print(f"aid {dl.get_version()}")
195
+ return 0
196
+
197
+ try:
198
+ parsed = parse_aid_args(args)
199
+ dl_args = build_dl_args(parsed)
200
+ except UsageError as e:
201
+ logging.error(str(e))
202
+ return 1
203
+
204
+ logging.info("aid -> dl %s", shlex.join(dl_args))
205
+ return dl.main(dl_args)
206
+
207
+
208
+ if __name__ == "__main__":
209
+ try:
210
+ sys.exit(main())
211
+ except KeyboardInterrupt:
212
+ sys.exit(130)
@@ -34,7 +34,7 @@ def _completion_file_path() -> pathlib.Path:
34
34
 
35
35
 
36
36
  def install_completions(rc_path: Optional[pathlib.Path] = None) -> int:
37
- """Install or refresh completion scripts for dl."""
37
+ """Install or refresh completion scripts for dl and aid."""
38
38
  completion_path = _completion_file_path().expanduser()
39
39
  rc_target = (rc_path if rc_path is not None else pathlib.Path.home() / ".bashrc").expanduser()
40
40
 
@@ -8,6 +8,10 @@
8
8
  # because temporary COMP_WORDBREAKS modification can have side effects with bash's
9
9
  # internal completion state and doesn't reliably prevent word splitting in all
10
10
  # bash versions. Direct parsing gives us full control over word boundaries.
11
+ #
12
+ # The same function serves `aid`, whose first argument is a dl workspace spec
13
+ # too. Only the flag list and what follows the spec differ, so the two places
14
+ # that care branch on $cmd rather than the script being copied for aid.
11
15
  _dl_completion() {
12
16
  local cur prev opts
13
17
  COMPREPLY=()
@@ -44,8 +48,17 @@ _dl_completion() {
44
48
  fi
45
49
  fi
46
50
 
51
+ # The command being completed: dl or aid.
52
+ local cmd=""
53
+ if (( ${#words[@]} > 0 )); then
54
+ cmd="${words[0]##*/}"
55
+ fi
56
+
47
57
  # Global command options (only valid as first arg)
48
58
  local global_opts="--ls --install --help -h --version --devcontainer"
59
+ if [[ "$cmd" == aid ]]; then
60
+ global_opts="--claude --codex --gemini --devcontainer --help -h --version"
61
+ fi
49
62
 
50
63
  # Workspace subcommands
51
64
  local ws_cmds="stop rm code restart recreate reset --"
@@ -133,8 +146,9 @@ _dl_completion() {
133
146
  return 0
134
147
  fi
135
148
 
136
- # Second argument (after workspace): subcommands
137
- if [[ ${word_count} -eq 3 ]]; then
149
+ # Second argument (after workspace): subcommands. Everything after an aid
150
+ # workspace is the prompt, so there is nothing to offer there.
151
+ if [[ ${word_count} -eq 3 && "$cmd" != aid ]]; then
138
152
  # Don't complete after global flags
139
153
  # Extract the first argument (word after "dl") from the words array
140
154
  local first=""
@@ -155,4 +169,5 @@ _dl_completion() {
155
169
 
156
170
  # Use -o default for better completion behavior
157
171
  complete -o default -F _dl_completion dl
172
+ complete -o default -F _dl_completion aid
158
173
  # end dl completion