modsmith-cli 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,8 @@
1
+ """Modsmith's command line: modpacks, mods and packs for the official Minecraft launcher."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("modsmith-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
@@ -0,0 +1,6 @@
1
+ """Run the command line with ``python -m modsmith_cli``."""
2
+
3
+ from modsmith_cli.app import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
modsmith_cli/app.py ADDED
@@ -0,0 +1,224 @@
1
+ """The ``modsmith`` command and its global options."""
2
+
3
+ import io
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Annotated, TextIO
8
+
9
+ import typer
10
+ from rich.logging import RichHandler
11
+
12
+ from . import __version__
13
+ from .commands.config import config_app
14
+ from .commands.contents import (
15
+ add_command,
16
+ adopt_command,
17
+ disable_command,
18
+ enable_command,
19
+ pin_command,
20
+ remove_command,
21
+ show_command,
22
+ unpin_command,
23
+ )
24
+ from .commands.discovery import info_command, loaders_command
25
+ from .commands.install import create_command, install_command
26
+ from .commands.instance import delete_command, import_command, move_command
27
+ from .commands.list import list_command
28
+ from .commands.search import search_command
29
+ from .commands.update import update_command
30
+ from .context import AppContext, ColorMode, Services
31
+ from .core.paths import AppPaths
32
+ from .errors import reported
33
+ from .target import examples
34
+
35
+ app = typer.Typer(
36
+ help=(
37
+ "Install Minecraft modpacks, mods, resource packs and shaders "
38
+ "for the official launcher."
39
+ ),
40
+ no_args_is_help=True,
41
+ pretty_exceptions_enable=False,
42
+ rich_markup_mode=None,
43
+ )
44
+
45
+
46
+ def _show_version(value: bool) -> None:
47
+ if value:
48
+ typer.echo(f"modsmith {__version__}")
49
+ raise typer.Exit
50
+
51
+
52
+ @app.callback()
53
+ def root(
54
+ ctx: typer.Context,
55
+ instances_dir: Annotated[
56
+ Path | None,
57
+ typer.Option("--instances-dir", help="Use this instances folder for this run."),
58
+ ] = None,
59
+ game_dir: Annotated[
60
+ Path | None,
61
+ typer.Option("--game-dir", help="Use this game directory for this run."),
62
+ ] = None,
63
+ verbose: Annotated[
64
+ int,
65
+ typer.Option(
66
+ "--verbose", "-v", count=True, help="Show more detail. Repeat for more."
67
+ ),
68
+ ] = 0,
69
+ color: Annotated[
70
+ ColorMode,
71
+ typer.Option("--color", help="When to use colour. NO_COLOR also turns it off."),
72
+ ] = ColorMode.AUTO,
73
+ version: Annotated[
74
+ bool,
75
+ typer.Option(
76
+ "--version",
77
+ callback=_show_version,
78
+ is_eager=True,
79
+ help="Show the version and exit.",
80
+ ),
81
+ ] = False,
82
+ ) -> None:
83
+ """Set up the run before any command."""
84
+ services = ctx.obj if isinstance(ctx.obj, Services) else Services()
85
+ environ = services.environ
86
+ paths = (
87
+ AppPaths.from_environment()
88
+ if environ is None
89
+ else AppPaths.from_environment(environ)
90
+ )
91
+ overrides: dict[str, object] = {}
92
+ if instances_dir is not None:
93
+ overrides["instances_dir"] = instances_dir.expanduser()
94
+ if game_dir is not None:
95
+ overrides["game_dir"] = game_dir.expanduser()
96
+ context = AppContext(paths, services, overrides, verbose=verbose, color=color)
97
+ if verbose:
98
+ level = logging.INFO if verbose == 1 else logging.DEBUG
99
+ handler = RichHandler(console=context.err, show_path=False)
100
+ logging.basicConfig(level=level, handlers=[handler], force=True)
101
+ ctx.obj = context
102
+
103
+
104
+ app.command(
105
+ "install",
106
+ epilog=examples(
107
+ "modsmith install fabulously-optimized",
108
+ "modsmith install curseforge:all-the-mods-9 --name atm9",
109
+ "modsmith install Downloads/pack.mrpack --all-optional",
110
+ "modsmith install curseforge:some-pack --from Downloads/manual",
111
+ ),
112
+ )(reported(install_command))
113
+ app.command(
114
+ "create",
115
+ epilog=examples(
116
+ "modsmith create vanilla --minecraft 1.21.1",
117
+ "modsmith create modded --minecraft 1.21.1 --loader fabric",
118
+ "modsmith create old --minecraft 1.20.1 --loader forge --loader-version 47.3.0",
119
+ ),
120
+ )(reported(create_command))
121
+ app.command("list")(reported(list_command))
122
+ app.command(
123
+ "search",
124
+ epilog=examples(
125
+ "modsmith search sodium",
126
+ 'modsmith search "fabulously optimized" --type modpack',
127
+ "modsmith search shaders --type shader --sort downloads",
128
+ "modsmith search jei --platform all --minecraft 1.20.1 --loader forge",
129
+ ),
130
+ )(reported(search_command))
131
+ app.command("show", epilog=examples("modsmith show", "modsmith show survival"))(
132
+ reported(show_command)
133
+ )
134
+ app.command(
135
+ "add",
136
+ epilog=examples(
137
+ "modsmith add survival sodium lithium",
138
+ "modsmith add survival curseforge:jei",
139
+ "modsmith add survival complementary-reimagined --type shader",
140
+ ),
141
+ )(reported(add_command))
142
+ app.command(
143
+ "remove",
144
+ epilog=examples(
145
+ "modsmith remove survival sodium", "modsmith remove iris --keep-deps"
146
+ ),
147
+ )(reported(remove_command))
148
+ app.command("enable", epilog=examples("modsmith enable survival sodium"))(
149
+ reported(enable_command)
150
+ )
151
+ app.command(
152
+ "disable",
153
+ epilog=examples(
154
+ "modsmith disable survival sodium",
155
+ "modsmith disable survival mods/OptiFine.jar",
156
+ ),
157
+ )(reported(disable_command))
158
+ app.command("pin", epilog=examples("modsmith pin survival sodium"))(
159
+ reported(pin_command)
160
+ )
161
+ app.command("unpin", epilog=examples("modsmith unpin survival sodium"))(
162
+ reported(unpin_command)
163
+ )
164
+ app.command(
165
+ "adopt",
166
+ epilog=examples("modsmith adopt survival", "modsmith adopt survival -p modrinth"),
167
+ )(reported(adopt_command))
168
+ app.command(
169
+ "info",
170
+ epilog=examples(
171
+ "modsmith info sodium",
172
+ "modsmith info curseforge:jei --minecraft 1.20.1 --loader forge",
173
+ "modsmith info fabulously-optimized --type modpack",
174
+ ),
175
+ )(reported(info_command))
176
+ app.command(
177
+ "loaders",
178
+ epilog=examples(
179
+ "modsmith loaders fabric 1.21.1", "modsmith loaders forge 1.20.1 --all"
180
+ ),
181
+ )(reported(loaders_command))
182
+ app.command(
183
+ "update",
184
+ epilog=examples(
185
+ "modsmith update survival",
186
+ "modsmith update survival --dry-run",
187
+ "modsmith update survival --pack-only --optional shaders",
188
+ "modsmith update atm9 --file Downloads/atm9-1.2.zip",
189
+ ),
190
+ )(reported(update_command))
191
+ app.command(
192
+ "import",
193
+ epilog=examples(
194
+ 'modsmith import "My Fabric world"', "modsmith import old-pack --dry-run"
195
+ ),
196
+ )(reported(import_command))
197
+ app.command(
198
+ "move",
199
+ epilog=examples(
200
+ "modsmith move survival creative", "modsmith move survival D:/Games/survival"
201
+ ),
202
+ )(reported(move_command))
203
+ app.command("delete", epilog=examples("modsmith delete survival"))(
204
+ reported(delete_command)
205
+ )
206
+ app.add_typer(config_app, name="config")
207
+
208
+
209
+ def tolerate_unencodable(*streams: TextIO) -> None:
210
+ """Write characters an output cannot encode as ``?`` instead of failing.
211
+
212
+ Piped or redirected output uses the system code page, so that the shell
213
+ reading it decodes it correctly. On Windows that code page often cannot
214
+ hold the emoji found in project names and summaries.
215
+ """
216
+ for stream in streams:
217
+ if isinstance(stream, io.TextIOWrapper):
218
+ stream.reconfigure(errors="replace")
219
+
220
+
221
+ def main() -> None:
222
+ """Run the command line."""
223
+ tolerate_unencodable(sys.stdout, sys.stderr)
224
+ app()
@@ -0,0 +1 @@
1
+ """The commands, one module per group; `modsmith_cli.app` puts them together."""
@@ -0,0 +1,81 @@
1
+ """``modsmith config``: show and change settings."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.table import Table
7
+
8
+ from ..context import get
9
+ from ..core import config as settings
10
+ from ..errors import reported
11
+
12
+ config_app = typer.Typer(
13
+ help="Show and change settings. With no command, show them all.",
14
+ invoke_without_command=True,
15
+ )
16
+
17
+ Key = Annotated[str, typer.Argument(help="The setting's name.", show_default=False)]
18
+
19
+
20
+ @config_app.callback()
21
+ @reported
22
+ def show_all(ctx: typer.Context) -> None:
23
+ """Show every setting, its value and where the value comes from."""
24
+ if ctx.invoked_subcommand is not None:
25
+ return
26
+ app = get(ctx)
27
+ values = app.config.__dict__
28
+ origins = settings.sources(app.paths, app.environ)
29
+ table = Table(box=None, pad_edge=False)
30
+ table.add_column("Setting", no_wrap=True)
31
+ # Paths fold rather than shorten, so they can always be copied whole.
32
+ table.add_column("Value", overflow="fold")
33
+ table.add_column("From", overflow="fold")
34
+ for key in settings.SETTINGS:
35
+ origin = "command line" if key in app.overrides else origins[key]
36
+ table.add_row(key, settings.shown(key, values[key]), origin)
37
+ app.out.print(table)
38
+
39
+
40
+ @config_app.command("get")
41
+ @reported
42
+ def get_value(ctx: typer.Context, key: Key) -> None:
43
+ """Print one setting's value."""
44
+ app = get(ctx)
45
+ settings.setting(key)
46
+ app.out.print(settings.shown(key, app.config.__dict__[key]), highlight=False)
47
+
48
+
49
+ @config_app.command("set")
50
+ @reported
51
+ def set_value(
52
+ ctx: typer.Context,
53
+ key: Key,
54
+ value: Annotated[str, typer.Argument(help="The new value.", show_default=False)],
55
+ ) -> None:
56
+ """Save a setting to the configuration file."""
57
+ app = get(ctx)
58
+ settings.save_setting(app.paths, key, value)
59
+ app.out.print(f"Saved {key}.")
60
+ variable = settings.SETTINGS[key].variable
61
+ if variable and app.environ.get(variable):
62
+ app.err.print(
63
+ f"${variable} is set, so it still takes precedence.", style="yellow"
64
+ )
65
+
66
+
67
+ @config_app.command("unset")
68
+ @reported
69
+ def unset_value(ctx: typer.Context, key: Key) -> None:
70
+ """Remove a setting from the configuration file, so its default applies."""
71
+ app = get(ctx)
72
+ settings.save_setting(app.paths, key, None)
73
+ app.out.print(f"Removed {key}.")
74
+
75
+
76
+ @config_app.command("path")
77
+ @reported
78
+ def show_path(ctx: typer.Context) -> None:
79
+ """Print where the configuration file is."""
80
+ app = get(ctx)
81
+ app.out.print(str(app.paths.config_file), highlight=False, soft_wrap=True)
@@ -0,0 +1,332 @@
1
+ """Commands that look at and change what an instance holds."""
2
+
3
+ from collections.abc import Sequence
4
+ from enum import StrEnum
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from modsmith import metadata
9
+ from modsmith.errors import DependencyError, InstanceError, ModsmithError
10
+ from modsmith.instances import Changes, Instance, Plan
11
+ from modsmith.platforms import ProjectType
12
+ from rich.table import Table
13
+ from rich.text import Text
14
+
15
+ from ..context import AppContext, get
16
+ from ..core.config import PLATFORMS
17
+ from ..core.contents import find_contents
18
+ from ..core.errors import AppError
19
+ from ..core.platforms import explained
20
+ from ..core.refs import parse_project
21
+ from ..core.registry import resolve_instance, split_target
22
+ from ..errors import report
23
+ from ..target import DryRun, InstanceName, Yes
24
+ from ..ui import cancellable, confirm, progress, show_plan
25
+ from .install import From, Overwrite, loader_label
26
+
27
+ SOURCES = {"pack": "pack", "addition": "added", "user": "yours"}
28
+
29
+
30
+ class AddedKind(StrEnum):
31
+ """The kinds of projects that can be added to an instance."""
32
+
33
+ MOD = "mod"
34
+ RESOURCEPACK = "resourcepack"
35
+ SHADER = "shader"
36
+
37
+
38
+ ADDED_KINDS: dict[AddedKind, ProjectType] = {
39
+ AddedKind.MOD: "mod",
40
+ AddedKind.RESOURCEPACK: "resourcepack",
41
+ AddedKind.SHADER: "shader",
42
+ }
43
+
44
+ Items = Annotated[
45
+ list[str],
46
+ typer.Argument(
47
+ help="The instance, then what to act on. Leave the instance out if you "
48
+ "have only one.",
49
+ show_default=False,
50
+ ),
51
+ ]
52
+
53
+
54
+ def _heading(name: str, instance: Instance) -> str:
55
+ state = instance.state
56
+ if state is None:
57
+ return name
58
+ loader = loader_label(state.loader_kind, state.loader_version)
59
+ pack = f", from {state.pack_name} {state.pack_version}" if state.pack_name else ""
60
+ return f"{name}: Minecraft {state.minecraft} with {loader}{pack}"
61
+
62
+
63
+ def show_command(ctx: typer.Context, name: InstanceName = None) -> None:
64
+ """List the mods, resource packs and shaders in an instance."""
65
+ app = get(ctx)
66
+ instance, _ = resolve_instance(name, app.config, app.paths)
67
+ app.out.print(Text(_heading(instance.path.name, instance), style="bold"))
68
+ contents = instance.contents()
69
+ if not contents:
70
+ app.out.print("It holds no mods, resource packs or shaders yet.")
71
+ return
72
+ table = Table(box=None, pad_edge=False)
73
+ for column in ("Name", "Version", "From", "State"):
74
+ table.add_column(column, no_wrap=column != "Name")
75
+ table.add_column("File", overflow="fold")
76
+ for item in contents:
77
+ try:
78
+ info = metadata.read(instance.path / item.location)
79
+ except OSError:
80
+ info = None
81
+ label = (item.addition.name if item.addition else None) or (
82
+ info.name if info else item.path.rsplit("/", 1)[-1]
83
+ )
84
+ flags = [] if item.enabled else ["off"]
85
+ if item.addition and item.addition.pinned:
86
+ flags.append("pinned")
87
+ table.add_row(
88
+ label,
89
+ (info.version if info else None) or "",
90
+ SOURCES[item.source],
91
+ ", ".join(flags),
92
+ item.location,
93
+ )
94
+ app.out.print(table)
95
+
96
+
97
+ def _notes(instance: Instance, plan: Plan) -> list[Text]:
98
+ """Say what a plan leaves out, and why it adds what nobody asked for."""
99
+ state = instance.state
100
+ held = {item.path for item in state.additions} if state else set[str]()
101
+ changing = {*plan.changes.added, *plan.changes.updated}
102
+ notes = [
103
+ Text(f" = {item.name} is already in the pack", style="dim")
104
+ for item in plan.skipped
105
+ ]
106
+ for item in plan.additions:
107
+ if item.path in changing and not item.requested:
108
+ notes.append(
109
+ Text(f" {item.name} comes too, because a mod needs it", style="dim")
110
+ )
111
+ elif item.requested and item.path not in changing and item.path in held:
112
+ notes.append(Text(f" = {item.name} is already added", style="dim"))
113
+ notes += [
114
+ Text(f" ? {name} has no release for this instance", style="yellow")
115
+ for name in plan.unavailable
116
+ ]
117
+ return notes
118
+
119
+
120
+ def add_command(
121
+ ctx: typer.Context,
122
+ items: Items,
123
+ kind: Annotated[
124
+ AddedKind, typer.Option("--type", "-t", help="The kind of project.")
125
+ ] = AddedKind.MOD,
126
+ optional: Annotated[
127
+ bool, typer.Option("--optional", help="Also add optional dependencies.")
128
+ ] = False,
129
+ pre: Annotated[
130
+ bool, typer.Option("--pre", help="Allow alpha and beta releases.")
131
+ ] = False,
132
+ overwrite: Overwrite = False,
133
+ from_folders: From = None,
134
+ yes: Yes = False,
135
+ dry_run: DryRun = False,
136
+ ) -> None:
137
+ """Add mods, resource packs or shaders to an instance, with what they need."""
138
+ app = get(ctx)
139
+ instance, _, names = split_target(items, app.config, app.paths)
140
+ if not names:
141
+ raise AppError("Name at least one project to add.")
142
+ refs = [parse_project(name, app.config.default_platform) for name in names]
143
+ release_only = not (pre or app.config.allow_prereleases)
144
+ plans: list[tuple[str, Plan]] = []
145
+ for platform_name in PLATFORMS:
146
+ wanted = [ref.name for ref in refs if ref.platform == platform_name]
147
+ if not wanted:
148
+ continue
149
+ with (
150
+ explained(platform_name, wanted),
151
+ app.platform(platform_name) as platform,
152
+ ):
153
+ plan = instance.plan(
154
+ platform,
155
+ wanted,
156
+ project_type=ADDED_KINDS[kind],
157
+ include_optional=optional,
158
+ release_only=release_only,
159
+ )
160
+ plans.append((platform_name, plan))
161
+
162
+ title = f"Add to {instance.path.name}"
163
+ changes = Changes(
164
+ added=tuple(path for _, plan in plans for path in plan.changes.added),
165
+ updated=tuple(path for _, plan in plans for path in plan.changes.updated),
166
+ removed=tuple(path for _, plan in plans for path in plan.changes.removed),
167
+ conflicts=tuple(path for _, plan in plans for path in plan.changes.conflicts),
168
+ )
169
+ sizes = {item.path: item.entry.size for _, plan in plans for item in plan.additions}
170
+ for _, plan in plans:
171
+ if not plan.ok:
172
+ raise DependencyError(plan)
173
+ notes = [note for _, plan in plans for note in _notes(instance, plan)]
174
+ pending = show_plan(app, changes, sizes, title=title, notes=notes)
175
+ if not pending or dry_run:
176
+ return
177
+ confirm(app, "Add them?", yes=yes)
178
+ with cancellable() as token, progress(app) as update:
179
+ for _, plan in plans:
180
+ instance.add(
181
+ plan,
182
+ overwrite=overwrite,
183
+ adopt_from=from_folders or (),
184
+ progress=update,
185
+ cancel=token,
186
+ )
187
+ app.out.print(f"Added to {instance.path.name}.")
188
+
189
+
190
+ def remove_command(
191
+ ctx: typer.Context,
192
+ items: Items,
193
+ keep_dependencies: Annotated[
194
+ bool,
195
+ typer.Option(
196
+ "--keep-deps", help="Keep dependencies even if nothing needs them."
197
+ ),
198
+ ] = False,
199
+ yes: Yes = False,
200
+ dry_run: DryRun = False,
201
+ ) -> None:
202
+ """Remove projects you added, and the dependencies nothing else needs."""
203
+ app = get(ctx)
204
+ instance, _, names = split_target(items, app.config, app.paths)
205
+ if not names:
206
+ raise AppError("Name at least one project to remove.")
207
+ try:
208
+ preview = instance.remove(
209
+ *names, keep_dependencies=keep_dependencies, dry_run=True
210
+ )
211
+ except InstanceError as e:
212
+ raise AppError(
213
+ str(e) + ".",
214
+ hint="Files from the pack cannot be removed, but you can turn them off "
215
+ "with: modsmith disable",
216
+ ) from e
217
+ show_plan(app, preview, title=f"Remove from {instance.path.name}")
218
+ if dry_run:
219
+ return
220
+ confirm(app, "Remove them?", yes=yes)
221
+ instance.remove(*names, keep_dependencies=keep_dependencies)
222
+ app.out.print(f"Removed from {instance.path.name}.")
223
+
224
+
225
+ def _toggle(ctx: typer.Context, items: list[str], *, enabled: bool) -> None:
226
+ app = get(ctx)
227
+ instance, _, names = split_target(items, app.config, app.paths)
228
+ if not names:
229
+ raise AppError("Name at least one file or project.")
230
+ targets = find_contents(instance, names)
231
+ paths = [item.path for item in targets]
232
+ if enabled:
233
+ instance.enable(*paths)
234
+ else:
235
+ instance.disable(*paths)
236
+ verb = "Turned on" if enabled else "Turned off"
237
+ for path in paths:
238
+ app.out.print(f"{verb} {path}.")
239
+
240
+
241
+ def enable_command(ctx: typer.Context, items: Items) -> None:
242
+ """Turn mods, resource packs or shaders back on."""
243
+ _toggle(ctx, items, enabled=True)
244
+
245
+
246
+ def disable_command(ctx: typer.Context, items: Items) -> None:
247
+ """Turn mods, resource packs or shaders off, without removing them."""
248
+ _toggle(ctx, items, enabled=False)
249
+
250
+
251
+ def _pin(ctx: typer.Context, items: list[str], *, pinned: bool) -> None:
252
+ app = get(ctx)
253
+ instance, _, names = split_target(items, app.config, app.paths)
254
+ if not names:
255
+ raise AppError("Name at least one project.")
256
+ for name in names:
257
+ try:
258
+ addition = instance.pin(name, pinned)
259
+ except ModsmithError as e:
260
+ raise AppError(
261
+ f"{name!r} was not added on its own, so it cannot be pinned.",
262
+ hint="Pack files follow the pack's own updates.",
263
+ ) from e
264
+ state = "stays at its release" if pinned else "follows updates again"
265
+ app.out.print(f"{addition.name} {state}.")
266
+
267
+
268
+ def pin_command(ctx: typer.Context, items: Items) -> None:
269
+ """Keep projects you added at their current release."""
270
+ _pin(ctx, items, pinned=True)
271
+
272
+
273
+ def unpin_command(ctx: typer.Context, items: Items) -> None:
274
+ """Let pinned projects update again."""
275
+ _pin(ctx, items, pinned=False)
276
+
277
+
278
+ class AdoptFrom(StrEnum):
279
+ """Which platforms may recognise files."""
280
+
281
+ MODRINTH = "modrinth"
282
+ CURSEFORGE = "curseforge"
283
+ ALL = "all"
284
+
285
+
286
+ def adopt_files(app: AppContext, instance: Instance, names: Sequence[str]) -> int:
287
+ """Ask each platform to recognise the player's files, and say what it found.
288
+
289
+ With several platforms, one that cannot be used is skipped with a note.
290
+
291
+ Returns:
292
+ How many files were recognised.
293
+ """
294
+ found = 0
295
+ for platform_name in names:
296
+ try:
297
+ with (
298
+ cancellable() as token,
299
+ progress(app) as update,
300
+ explained(platform_name),
301
+ app.platform(platform_name) as platform,
302
+ ):
303
+ adopted = instance.adopt(platform, progress=update, cancel=token)
304
+ except AppError as e:
305
+ if len(names) == 1:
306
+ raise
307
+ report(e, app.err, prefix=f"Skipped {platform_name}: ")
308
+ continue
309
+ for item in adopted:
310
+ app.out.print(f" {item.name}: {item.path}")
311
+ found += len(adopted)
312
+ if found:
313
+ files = "1 file" if found == 1 else f"{found} files"
314
+ app.out.print(f"{files} can now be updated and removed like added ones.")
315
+ else:
316
+ app.out.print("No unrecognised files were found on these platforms.")
317
+ return found
318
+
319
+
320
+ def adopt_command(
321
+ ctx: typer.Context,
322
+ name: InstanceName = None,
323
+ where: Annotated[
324
+ AdoptFrom,
325
+ typer.Option("--platform", "-p", help="Which platforms may recognise files."),
326
+ ] = AdoptFrom.ALL,
327
+ ) -> None:
328
+ """Recognise files you put in an instance, so they can be updated and removed."""
329
+ app = get(ctx)
330
+ instance, _ = resolve_instance(name, app.config, app.paths)
331
+ names = list(PLATFORMS) if where == AdoptFrom.ALL else [where.value]
332
+ adopt_files(app, instance, names)