devlaunch 0.0.13__tar.gz → 0.0.14__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.
- {devlaunch-0.0.13 → devlaunch-0.0.14}/PKG-INFO +34 -3
- {devlaunch-0.0.13 → devlaunch-0.0.14}/README.md +33 -2
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/dl.py +7 -1
- devlaunch-0.0.14/devlaunch/tools.py +219 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/pyproject.toml +1 -1
- {devlaunch-0.0.13 → devlaunch-0.0.14}/.gitignore +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/LICENSE +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/__init__.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/aid.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/completion.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/completion_loader.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/completions/__init__.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/completions/dl.bash +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/devpod_ssh.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/gh_auth.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/tty_session.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/workspace_id.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/__init__.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/branch_manager.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/config.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/migration.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/models.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/repo_manager.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/storage.py +0 -0
- {devlaunch-0.0.13 → devlaunch-0.0.14}/devlaunch/worktree/workspace_clone.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlaunch
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.14
|
|
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
|
|
@@ -246,8 +246,9 @@ whichever answers first, and hands it to the container as `GH_TOKEN`. That reach
|
|
|
246
246
|
any image and any container user, unlike a bind-mount of `~/.config/gh`, and it
|
|
247
247
|
works whether the host keeps its token in `hosts.yml` or in a keyring. The token
|
|
248
248
|
is passed to devpod through a private file and through devpod's own environment,
|
|
249
|
-
never on a command line, so it does not appear in `ps`.
|
|
250
|
-
|
|
249
|
+
never on a command line, so it does not appear in `ps`. `dl` installs `gh` itself
|
|
250
|
+
(see [Tools in every workspace](#tools-in-every-workspace)), so the login has
|
|
251
|
+
something to be spent on whatever the image ships. Check a workspace with:
|
|
251
252
|
|
|
252
253
|
```bash
|
|
253
254
|
dl <workspace> -- gh auth status
|
|
@@ -278,6 +279,36 @@ that is *already running* skips that step, and the token it was given at startup
|
|
|
278
279
|
stays in place — including one it was given before you set
|
|
279
280
|
`DEVLAUNCH_NO_GH_TOKEN`. Run `dl <workspace> restart` to replace it.
|
|
280
281
|
|
|
282
|
+
## Tools in every workspace
|
|
283
|
+
|
|
284
|
+
`gh` and `claude` are available in every workspace `dl` opens, in every kind of
|
|
285
|
+
session — an interactive `dl <workspace>`, a one-shot `dl <workspace> -- <command>`,
|
|
286
|
+
and `aid`. The repo's `devcontainer.json` does not have to provide them, and most
|
|
287
|
+
do not: `dl` launches arbitrary repos, so a guarantee that depended on the image
|
|
288
|
+
would not be a guarantee.
|
|
289
|
+
|
|
290
|
+
They are installed with `pixi global` on `devpod up`, and put on the PATH of a
|
|
291
|
+
login shell through whichever of `~/.bash_profile`, `~/.bash_login` or `~/.profile`
|
|
292
|
+
bash actually reads — it sources only the first of those that exists, so an image
|
|
293
|
+
shipping a `~/.bash_profile` never reads `~/.profile`. A workspace that already has both is left alone —
|
|
294
|
+
the check runs first, so the cost after the first launch is one round-trip and no
|
|
295
|
+
network. If `pixi` is missing from the image, `dl` installs that too.
|
|
296
|
+
|
|
297
|
+
An install that fails costs the workspace its tools, not its launch: `dl` logs a
|
|
298
|
+
warning and hands you the session anyway.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
DEVLAUNCH_NO_TOOLS=1 dl someone/repo
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Variable | Description |
|
|
305
|
+
|----------|-------------|
|
|
306
|
+
| `DEVLAUNCH_NO_TOOLS=1` | Do not install `gh` or `claude` into workspaces |
|
|
307
|
+
|
|
308
|
+
Attaching to a workspace that is *already running* skips `devpod up`, and so skips
|
|
309
|
+
this too. A workspace started by something other than `dl` — or created before this
|
|
310
|
+
existed — picks the tools up on its next `dl <workspace> restart`.
|
|
311
|
+
|
|
281
312
|
## Global Commands
|
|
282
313
|
|
|
283
314
|
| Command | Description |
|
|
@@ -224,8 +224,9 @@ whichever answers first, and hands it to the container as `GH_TOKEN`. That reach
|
|
|
224
224
|
any image and any container user, unlike a bind-mount of `~/.config/gh`, and it
|
|
225
225
|
works whether the host keeps its token in `hosts.yml` or in a keyring. The token
|
|
226
226
|
is passed to devpod through a private file and through devpod's own environment,
|
|
227
|
-
never on a command line, so it does not appear in `ps`.
|
|
228
|
-
|
|
227
|
+
never on a command line, so it does not appear in `ps`. `dl` installs `gh` itself
|
|
228
|
+
(see [Tools in every workspace](#tools-in-every-workspace)), so the login has
|
|
229
|
+
something to be spent on whatever the image ships. Check a workspace with:
|
|
229
230
|
|
|
230
231
|
```bash
|
|
231
232
|
dl <workspace> -- gh auth status
|
|
@@ -256,6 +257,36 @@ that is *already running* skips that step, and the token it was given at startup
|
|
|
256
257
|
stays in place — including one it was given before you set
|
|
257
258
|
`DEVLAUNCH_NO_GH_TOKEN`. Run `dl <workspace> restart` to replace it.
|
|
258
259
|
|
|
260
|
+
## Tools in every workspace
|
|
261
|
+
|
|
262
|
+
`gh` and `claude` are available in every workspace `dl` opens, in every kind of
|
|
263
|
+
session — an interactive `dl <workspace>`, a one-shot `dl <workspace> -- <command>`,
|
|
264
|
+
and `aid`. The repo's `devcontainer.json` does not have to provide them, and most
|
|
265
|
+
do not: `dl` launches arbitrary repos, so a guarantee that depended on the image
|
|
266
|
+
would not be a guarantee.
|
|
267
|
+
|
|
268
|
+
They are installed with `pixi global` on `devpod up`, and put on the PATH of a
|
|
269
|
+
login shell through whichever of `~/.bash_profile`, `~/.bash_login` or `~/.profile`
|
|
270
|
+
bash actually reads — it sources only the first of those that exists, so an image
|
|
271
|
+
shipping a `~/.bash_profile` never reads `~/.profile`. A workspace that already has both is left alone —
|
|
272
|
+
the check runs first, so the cost after the first launch is one round-trip and no
|
|
273
|
+
network. If `pixi` is missing from the image, `dl` installs that too.
|
|
274
|
+
|
|
275
|
+
An install that fails costs the workspace its tools, not its launch: `dl` logs a
|
|
276
|
+
warning and hands you the session anyway.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
DEVLAUNCH_NO_TOOLS=1 dl someone/repo
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| Variable | Description |
|
|
283
|
+
|----------|-------------|
|
|
284
|
+
| `DEVLAUNCH_NO_TOOLS=1` | Do not install `gh` or `claude` into workspaces |
|
|
285
|
+
|
|
286
|
+
Attaching to a workspace that is *already running* skips `devpod up`, and so skips
|
|
287
|
+
this too. A workspace started by something other than `dl` — or created before this
|
|
288
|
+
existed — picks the tools up on its next `dl <workspace> restart`.
|
|
289
|
+
|
|
259
290
|
## Global Commands
|
|
260
291
|
|
|
261
292
|
| Command | Description |
|
|
@@ -33,7 +33,7 @@ from dataclasses import dataclass
|
|
|
33
33
|
from urllib.parse import urlparse
|
|
34
34
|
from urllib.request import url2pathname
|
|
35
35
|
|
|
36
|
-
from . import devpod_ssh, gh_auth, tty_session
|
|
36
|
+
from . import devpod_ssh, gh_auth, tools, tty_session
|
|
37
37
|
from .completion import install_completions
|
|
38
38
|
from .workspace_id import TARGET_LENGTH, WorkspaceId, slug, source_workspace_id, validate_ref_name
|
|
39
39
|
from .worktree.config import get_worktree_config
|
|
@@ -1062,6 +1062,12 @@ def workspace_up(
|
|
|
1062
1062
|
# `up` creates and starts workspaces, so any snapshot of `devpod list`
|
|
1063
1063
|
# taken before it is now out of date.
|
|
1064
1064
|
invalidate_workspace_list_cache()
|
|
1065
|
+
# The tools a session always needs, for whatever repo this is: `up` is the
|
|
1066
|
+
# one path that runs for every workspace dl opens and is already slow
|
|
1067
|
+
# enough to absorb a round-trip. Only after a successful `up` -- there is
|
|
1068
|
+
# no container to install into otherwise.
|
|
1069
|
+
if result.returncode == 0 and identity:
|
|
1070
|
+
tools.ensure_tools(identity, run_devpod)
|
|
1065
1071
|
return result
|
|
1066
1072
|
|
|
1067
1073
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Put the tools a session always needs into every workspace devlaunch opens.
|
|
2
|
+
|
|
3
|
+
`gh` and `claude` are not optional extras for the way these workspaces get used:
|
|
4
|
+
`dl` already forwards the host's GitHub login into every container, which is
|
|
5
|
+
worth nothing when the container has no `gh` to spend it, and `aid` exists to
|
|
6
|
+
run `claude` in there. Both currently arrive only when the repo's own
|
|
7
|
+
devcontainer.json arranges them -- this repo does, through
|
|
8
|
+
`.devcontainer/claude-code/`, which is why `claude` is present in its workspaces
|
|
9
|
+
and `gh` (a project pixi dependency, reachable only as `pixi run gh`) is not.
|
|
10
|
+
|
|
11
|
+
A guarantee that depends on the repo is not a guarantee. `dl` launches arbitrary
|
|
12
|
+
repos, so the tools have to come from the invocation, the same argument
|
|
13
|
+
gh_auth makes for the token and workspace_ssh's login shell makes for PATH.
|
|
14
|
+
|
|
15
|
+
Installing them costs a devpod round-trip on `up`, which already runs for
|
|
16
|
+
seconds, and nothing at all on the attach paths -- the script exits before doing
|
|
17
|
+
any work when both tools are already there, and `up` is the only caller.
|
|
18
|
+
|
|
19
|
+
Two consequences worth knowing:
|
|
20
|
+
|
|
21
|
+
- A workspace that is already running when `dl` reaches it skips `up` entirely
|
|
22
|
+
(the fast-attach path), so it is not topped up. That covers workspaces started
|
|
23
|
+
by something other than `dl`, and ones created before this existed; both get
|
|
24
|
+
the tools on their next `dl <ws> restart` or `up`.
|
|
25
|
+
- Provisioning is a convenience, so a failed install costs the workspace its
|
|
26
|
+
tools and not its launch: an install that fails is logged and the session
|
|
27
|
+
starts anyway. The exception is a devpod that has gone missing between `up`
|
|
28
|
+
and here, which dl treats as fatal everywhere else and which this does not
|
|
29
|
+
make an exception of.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
import shlex
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from typing import List, Optional, Sequence
|
|
37
|
+
|
|
38
|
+
# Set this to opt a machine out of installing tools into workspaces.
|
|
39
|
+
DISABLE_VAR = "DEVLAUNCH_NO_TOOLS"
|
|
40
|
+
|
|
41
|
+
_FALSEY = ("", "0", "false", "no")
|
|
42
|
+
|
|
43
|
+
# The claude package lives in a personal channel rather than conda-forge.
|
|
44
|
+
BLOOOP_CHANNEL = "https://prefix.dev/blooop"
|
|
45
|
+
|
|
46
|
+
# Which file a bash login shell will actually read. bash tries ~/.bash_profile,
|
|
47
|
+
# ~/.bash_login and ~/.profile in that order and sources only the first that
|
|
48
|
+
# exists, so appending to ~/.profile in an image that ships a ~/.bash_profile
|
|
49
|
+
# writes to a file nothing reads.
|
|
50
|
+
_PROFILE_RESOLUTION = "\n".join(
|
|
51
|
+
[
|
|
52
|
+
'if [ -f "$HOME/.bash_profile" ]; then PROFILE="$HOME/.bash_profile"',
|
|
53
|
+
'elif [ -f "$HOME/.bash_login" ]; then PROFILE="$HOME/.bash_login"',
|
|
54
|
+
'else PROFILE="$HOME/.profile"',
|
|
55
|
+
"fi",
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class Tool:
|
|
62
|
+
"""A binary a session must be able to run, and the pixi package providing it.
|
|
63
|
+
|
|
64
|
+
`command` is what a shell has to find on PATH, which is not always the
|
|
65
|
+
package name -- `claude` ships in `claude-shim` -- so both are recorded
|
|
66
|
+
rather than one being derived from the other.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
command: str
|
|
70
|
+
package: str
|
|
71
|
+
channel: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def install_args(self) -> List[str]:
|
|
75
|
+
"""The `pixi global install` arguments that provide this tool."""
|
|
76
|
+
if self.channel:
|
|
77
|
+
return ["--channel", self.channel, self.package]
|
|
78
|
+
return [self.package]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
REQUIRED_TOOLS: Sequence[Tool] = (
|
|
82
|
+
Tool(command="gh", package="gh"),
|
|
83
|
+
Tool(command="claude", package="claude-shim", channel=BLOOOP_CHANNEL),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def provisioning_disabled() -> bool:
|
|
88
|
+
"""Whether the user opted this machine out of installing tools."""
|
|
89
|
+
return os.environ.get(DISABLE_VAR, "").strip().lower() not in _FALSEY
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _install_line(tool: Tool) -> str:
|
|
93
|
+
args = " ".join(shlex.quote(arg) for arg in tool.install_args)
|
|
94
|
+
return (
|
|
95
|
+
f"if ! command -v {shlex.quote(tool.command)} >/dev/null 2>&1; then\n"
|
|
96
|
+
f' echo "devlaunch: installing {tool.command}"\n'
|
|
97
|
+
f" pixi global install {args} || failed=1\n"
|
|
98
|
+
f"fi"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def provision_script(tools: Sequence[Tool] = REQUIRED_TOOLS) -> str:
|
|
103
|
+
"""The shell script that makes `tools` available in a workspace.
|
|
104
|
+
|
|
105
|
+
Idempotent and cheap on the common path: every tool already on PATH is
|
|
106
|
+
skipped, so a workspace that has been provisioned before does nothing but
|
|
107
|
+
answer. It runs under a login shell (see ensure_tools), which is what puts
|
|
108
|
+
an earlier run's ~/.pixi/bin on PATH -- checked from a non-login shell every
|
|
109
|
+
tool would look missing and be reinstalled on every launch.
|
|
110
|
+
|
|
111
|
+
Exits 0 unless an install actually failed, so "nothing to do" and "all
|
|
112
|
+
installs worked" are the same answer to the caller.
|
|
113
|
+
"""
|
|
114
|
+
all_present = " && ".join(
|
|
115
|
+
f"command -v {shlex.quote(tool.command)} >/dev/null 2>&1" for tool in tools
|
|
116
|
+
)
|
|
117
|
+
installs = "\n".join(_install_line(tool) for tool in tools)
|
|
118
|
+
# The trampoline pixi writes into ~/.pixi/bin does not work for packages
|
|
119
|
+
# that ship a shell script, which is why the env's own bin directory is
|
|
120
|
+
# added too -- the same workaround .devcontainer/claude-code/install.sh
|
|
121
|
+
# carries, for the same package.
|
|
122
|
+
profile_lines = "\n".join(
|
|
123
|
+
[
|
|
124
|
+
# bash reads exactly one of these on login, in this order, and
|
|
125
|
+
# stops at the first that exists -- so an image shipping a
|
|
126
|
+
# ~/.bash_profile means ~/.profile is never sourced at all. Writing
|
|
127
|
+
# to the wrong one leaves the tools installed and unreachable, and
|
|
128
|
+
# (since the check above is `command -v`) reinstalled from scratch
|
|
129
|
+
# on every single launch.
|
|
130
|
+
_PROFILE_RESOLUTION,
|
|
131
|
+
'grep -q "\\.pixi/bin" "$PROFILE" 2>/dev/null || '
|
|
132
|
+
'echo \'export PATH="$HOME/.pixi/bin:$PATH"\' >> "$PROFILE" || failed=1',
|
|
133
|
+
'grep -q "pixi/envs/claude-shim" "$PROFILE" 2>/dev/null || '
|
|
134
|
+
'echo \'[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && '
|
|
135
|
+
'export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"\' >> "$PROFILE" || failed=1',
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
return "\n".join(
|
|
139
|
+
[
|
|
140
|
+
"set -u",
|
|
141
|
+
# Everything this script prints is progress, and progress is not
|
|
142
|
+
# the answer to anything: `dl <ws> -- cmd > file` on a workspace
|
|
143
|
+
# that needs provisioning must put the command's output in the
|
|
144
|
+
# file and nothing else. pixi writes to stdout too, so redirect
|
|
145
|
+
# once here rather than per line.
|
|
146
|
+
"exec >&2",
|
|
147
|
+
"failed=0",
|
|
148
|
+
# Everything already there: leave without touching pixi, the
|
|
149
|
+
# profile, or the network. Every launch after the first takes this.
|
|
150
|
+
f"if {all_present}; then exit 0; fi",
|
|
151
|
+
_pixi_bootstrap(),
|
|
152
|
+
installs,
|
|
153
|
+
profile_lines,
|
|
154
|
+
'exit "$failed"',
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _pixi_bootstrap() -> str:
|
|
160
|
+
"""Install pixi if the image has none, since every tool here comes from it.
|
|
161
|
+
|
|
162
|
+
An arbitrary repo's container is not required to carry pixi, and without it
|
|
163
|
+
the guarantee this module makes would hold only for images that happen to
|
|
164
|
+
have it. Failure is left to the install steps to report: they will fail for
|
|
165
|
+
a reason the log can name.
|
|
166
|
+
"""
|
|
167
|
+
return "\n".join(
|
|
168
|
+
[
|
|
169
|
+
"if ! command -v pixi >/dev/null 2>&1; then",
|
|
170
|
+
' echo "devlaunch: installing pixi"',
|
|
171
|
+
" curl -fsSL https://pixi.sh/install.sh | bash >/dev/null 2>&1 || true",
|
|
172
|
+
' export PATH="$HOME/.pixi/bin:$PATH"',
|
|
173
|
+
"fi",
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def ensure_tools(workspace: str, runner, tools: Sequence[Tool] = REQUIRED_TOOLS) -> bool:
|
|
179
|
+
"""Make `tools` available in `workspace`. Returns whether they now are.
|
|
180
|
+
|
|
181
|
+
`runner` is dl.run_devpod, passed in rather than imported to keep this
|
|
182
|
+
module off dl's import cycle and testable without a devpod.
|
|
183
|
+
|
|
184
|
+
The payload goes through `bash -lc` for the same reason workspace_ssh wraps
|
|
185
|
+
its own: devpod runs --command under a shell that sources no profile, so
|
|
186
|
+
PATH would be missing the pixi directory this module itself installs into.
|
|
187
|
+
|
|
188
|
+
Output is not captured. A cold install downloads pixi and two packages and
|
|
189
|
+
takes tens of seconds, which with nothing on the terminal reads as a hung
|
|
190
|
+
`dl`; the script's own progress lines are the answer to that, and they are
|
|
191
|
+
worth nothing in a buffer. A workspace that needs no work stays silent
|
|
192
|
+
because the script prints nothing on that path.
|
|
193
|
+
|
|
194
|
+
Not every failure is swallowed: DevpodNotInstalled is deliberately not an
|
|
195
|
+
OSError (see dl.DevpodNotInstalled) so that it is never mistaken for a
|
|
196
|
+
failure of the thing being attempted, and it keeps that meaning here.
|
|
197
|
+
"""
|
|
198
|
+
if provisioning_disabled():
|
|
199
|
+
logging.debug("%s is set; not installing tools into %s", DISABLE_VAR, workspace)
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
script = provision_script(tools)
|
|
203
|
+
command = f"bash -lc {shlex.quote(script)}"
|
|
204
|
+
try:
|
|
205
|
+
result = runner(["ssh", workspace, "--command", command])
|
|
206
|
+
except OSError as e:
|
|
207
|
+
logging.debug("Could not install tools into %s: %s", workspace, e)
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
if result.returncode != 0:
|
|
211
|
+
# Named, not raised: the workspace is up and the user asked for a
|
|
212
|
+
# session, not for an install.
|
|
213
|
+
logging.warning(
|
|
214
|
+
"Could not install %s into %s; the session will start without them.",
|
|
215
|
+
" and ".join(tool.command for tool in tools),
|
|
216
|
+
workspace,
|
|
217
|
+
)
|
|
218
|
+
return False
|
|
219
|
+
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|