kanon-cli 1.0.0__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,433 @@
1
+ """Shared marketplace operations for Claude Code plugin install/uninstall.
2
+
3
+ Provides functions for locating the claude binary, discovering marketplace
4
+ entries and plugins, and orchestrating plugin install/uninstall lifecycles.
5
+ Used by both ``core.install`` (install) and ``core.clean`` (uninstall).
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import pathlib
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+
15
+
16
+ def locate_claude_binary() -> str:
17
+ """Locate the claude CLI binary on $PATH.
18
+
19
+ Uses shutil.which("claude") to find the binary.
20
+
21
+ Returns:
22
+ Absolute path to the claude binary.
23
+
24
+ Raises:
25
+ SystemExit: Exits with code 1 if claude is not found on $PATH.
26
+ """
27
+ path = shutil.which("claude")
28
+ if path is None:
29
+ print(
30
+ "Error: claude binary not found on $PATH. Ensure claude is installed and available.",
31
+ file=sys.stderr,
32
+ )
33
+ sys.exit(1)
34
+ return os.path.abspath(path)
35
+
36
+
37
+ def get_marketplace_dir(globals_dict: dict[str, str]) -> pathlib.Path:
38
+ """Return Path to marketplace directory from globals_dict.
39
+
40
+ Args:
41
+ globals_dict: Parsed .kanon globals containing CLAUDE_MARKETPLACES_DIR.
42
+
43
+ Returns:
44
+ pathlib.Path to the marketplace directory.
45
+ """
46
+ env_value = globals_dict.get("CLAUDE_MARKETPLACES_DIR", "")
47
+ if env_value:
48
+ return pathlib.Path(env_value)
49
+ return pathlib.Path.home() / ".claude-marketplaces"
50
+
51
+
52
+ def discover_marketplace_entries(marketplace_dir: pathlib.Path) -> list[pathlib.Path]:
53
+ """Discover marketplace entries in the given directory.
54
+
55
+ Returns sorted list of non-hidden entries that are directories or
56
+ symlinks to directories. Hidden entries (dot-prefixed) are excluded.
57
+ Broken symlinks are logged as warnings and excluded.
58
+
59
+ Args:
60
+ marketplace_dir: Path to the marketplace directory.
61
+
62
+ Returns:
63
+ Alphabetically sorted list of Path objects.
64
+ """
65
+ entries = []
66
+ for entry in sorted(marketplace_dir.iterdir()):
67
+ if entry.name.startswith("."):
68
+ continue
69
+ if entry.is_symlink() and not entry.exists():
70
+ print(
71
+ f"Warning: Broken symlink detected and skipped: {entry}",
72
+ file=sys.stderr,
73
+ )
74
+ continue
75
+ if entry.is_dir():
76
+ entries.append(entry)
77
+ return entries
78
+
79
+
80
+ def read_marketplace_name(marketplace_path: pathlib.Path) -> str:
81
+ """Read marketplace name from .claude-plugin/marketplace.json.
82
+
83
+ Args:
84
+ marketplace_path: Path to the marketplace directory.
85
+
86
+ Returns:
87
+ The 'name' field from marketplace.json.
88
+
89
+ Raises:
90
+ FileNotFoundError: If marketplace.json does not exist.
91
+ KeyError: If 'name' field is missing.
92
+ json.JSONDecodeError: If file is not valid JSON.
93
+ """
94
+ manifest_path = marketplace_path / ".claude-plugin" / "marketplace.json"
95
+ with manifest_path.open() as f:
96
+ data = json.load(f)
97
+ return data["name"]
98
+
99
+
100
+ def discover_plugins(marketplace_path: pathlib.Path) -> list[tuple[str, pathlib.Path]]:
101
+ """Discover plugins within a marketplace directory.
102
+
103
+ Scans immediate subdirectories for .claude-plugin/plugin.json files.
104
+
105
+ Args:
106
+ marketplace_path: Path to the marketplace directory.
107
+
108
+ Returns:
109
+ List of (plugin_name, plugin_path) tuples for each discovered plugin.
110
+
111
+ Raises:
112
+ json.JSONDecodeError: If plugin.json exists but contains invalid JSON.
113
+ KeyError: If plugin.json exists but lacks the 'name' field.
114
+ """
115
+ plugins = []
116
+ for entry in sorted(marketplace_path.iterdir()):
117
+ if not entry.is_dir():
118
+ continue
119
+ plugin_json = entry / ".claude-plugin" / "plugin.json"
120
+ if not plugin_json.is_file():
121
+ continue
122
+ with plugin_json.open() as f:
123
+ data = json.load(f)
124
+ plugins.append((data["name"], entry))
125
+ return plugins
126
+
127
+
128
+ def _get_timeout(env_var: str, default: int = 30) -> int:
129
+ """Read and validate a timeout from an environment variable.
130
+
131
+ Args:
132
+ env_var: Name of the environment variable.
133
+ default: Default timeout in seconds.
134
+
135
+ Returns:
136
+ Timeout value in seconds.
137
+
138
+ Raises:
139
+ SystemExit: If the value is not a valid positive integer.
140
+ """
141
+ timeout_str = os.environ.get(env_var, str(default))
142
+ try:
143
+ value = int(timeout_str)
144
+ except ValueError:
145
+ print(
146
+ f"Error: {env_var} must be a positive integer, got: {timeout_str}",
147
+ file=sys.stderr,
148
+ )
149
+ sys.exit(1)
150
+ if value <= 0:
151
+ print(
152
+ f"Error: {env_var} must be a positive integer, got: {timeout_str}",
153
+ file=sys.stderr,
154
+ )
155
+ sys.exit(1)
156
+ return value
157
+
158
+
159
+ def register_marketplace(claude_bin: str, marketplace_path: pathlib.Path) -> bool:
160
+ """Register a marketplace with Claude Code.
161
+
162
+ Runs: claude plugin marketplace add <absolute-path>
163
+
164
+ Args:
165
+ claude_bin: Path to claude binary.
166
+ marketplace_path: Absolute path to marketplace directory.
167
+
168
+ Returns:
169
+ True if registration succeeded, False otherwise.
170
+ """
171
+ timeout = _get_timeout("CLAUDE_REGISTER_TIMEOUT")
172
+ try:
173
+ result = subprocess.run(
174
+ [claude_bin, "plugin", "marketplace", "add", str(marketplace_path)],
175
+ capture_output=True,
176
+ text=True,
177
+ timeout=timeout,
178
+ )
179
+ except subprocess.TimeoutExpired:
180
+ print(
181
+ f"Error: Timed out after {timeout} seconds registering marketplace {marketplace_path}",
182
+ file=sys.stderr,
183
+ )
184
+ return False
185
+
186
+ if result.returncode != 0:
187
+ print(
188
+ f"Error: Failed to register marketplace {marketplace_path}: {result.stderr}",
189
+ file=sys.stderr,
190
+ )
191
+ return False
192
+ return True
193
+
194
+
195
+ def install_plugin(claude_bin: str, plugin_name: str, marketplace_name: str) -> bool:
196
+ """Install a plugin via Claude Code CLI.
197
+
198
+ Runs: claude plugin install <plugin_name>@<marketplace_name> --scope user
199
+
200
+ Args:
201
+ claude_bin: Path to claude binary.
202
+ plugin_name: Name of the plugin (from plugin.json).
203
+ marketplace_name: Name of the marketplace (from marketplace.json).
204
+
205
+ Returns:
206
+ True if install succeeded, False otherwise.
207
+ """
208
+ timeout = _get_timeout("CLAUDE_INSTALL_TIMEOUT")
209
+ plugin_ref = f"{plugin_name}@{marketplace_name}"
210
+ try:
211
+ result = subprocess.run(
212
+ [claude_bin, "plugin", "install", plugin_ref, "--scope", "user"],
213
+ capture_output=True,
214
+ text=True,
215
+ timeout=timeout,
216
+ )
217
+ except subprocess.TimeoutExpired:
218
+ print(
219
+ f"Error: Timed out after {timeout} seconds installing plugin {plugin_ref}",
220
+ file=sys.stderr,
221
+ )
222
+ return False
223
+
224
+ if result.returncode != 0:
225
+ print(
226
+ f"Error: Failed to install plugin {plugin_ref}: {result.stderr}",
227
+ file=sys.stderr,
228
+ )
229
+ return False
230
+ return True
231
+
232
+
233
+ def uninstall_plugin(claude_bin: str, plugin_name: str, marketplace_name: str) -> bool:
234
+ """Uninstall a plugin via Claude Code CLI.
235
+
236
+ Runs: claude plugin uninstall <plugin_name>@<marketplace_name> --scope user
237
+
238
+ Args:
239
+ claude_bin: Path to claude binary.
240
+ plugin_name: Name of the plugin (from plugin.json).
241
+ marketplace_name: Name of the marketplace (from marketplace.json).
242
+
243
+ Returns:
244
+ True if uninstall succeeded, False otherwise.
245
+ """
246
+ timeout = _get_timeout("CLAUDE_UNINSTALL_TIMEOUT")
247
+ plugin_ref = f"{plugin_name}@{marketplace_name}"
248
+ try:
249
+ result = subprocess.run(
250
+ [claude_bin, "plugin", "uninstall", plugin_ref, "--scope", "user"],
251
+ capture_output=True,
252
+ text=True,
253
+ timeout=timeout,
254
+ )
255
+ except subprocess.TimeoutExpired:
256
+ print(
257
+ f"Error: Timed out after {timeout} seconds uninstalling plugin {plugin_ref}",
258
+ file=sys.stderr,
259
+ )
260
+ return False
261
+
262
+ if result.returncode != 0:
263
+ stderr = result.stderr.strip() if result.stderr else ""
264
+ if "not found" in stderr.lower() or "not installed" in stderr.lower():
265
+ print(f"Plugin already uninstalled (not found): {plugin_ref}")
266
+ return True
267
+ print(
268
+ f"Error: Failed to uninstall plugin {plugin_ref}: {stderr}",
269
+ file=sys.stderr,
270
+ )
271
+ return False
272
+ return True
273
+
274
+
275
+ def remove_marketplace(claude_bin: str, marketplace_name: str) -> bool:
276
+ """Remove a marketplace registration from Claude Code.
277
+
278
+ Runs: claude plugin marketplace remove <marketplace_name>
279
+
280
+ Args:
281
+ claude_bin: Path to claude binary.
282
+ marketplace_name: Marketplace name (from marketplace.json).
283
+
284
+ Returns:
285
+ True if removal succeeded, False otherwise.
286
+ """
287
+ timeout = _get_timeout("CLAUDE_UNINSTALL_TIMEOUT")
288
+ try:
289
+ result = subprocess.run(
290
+ [claude_bin, "plugin", "marketplace", "remove", marketplace_name],
291
+ capture_output=True,
292
+ text=True,
293
+ timeout=timeout,
294
+ )
295
+ except subprocess.TimeoutExpired:
296
+ print(
297
+ f"Error: Timed out after {timeout} seconds removing marketplace {marketplace_name}",
298
+ file=sys.stderr,
299
+ )
300
+ return False
301
+
302
+ if result.returncode != 0:
303
+ stderr = result.stderr.strip() if result.stderr else ""
304
+ if "not found" in stderr.lower():
305
+ print(f"Marketplace already removed (not found): {marketplace_name}")
306
+ return True
307
+ print(
308
+ f"Error: Failed to remove marketplace {marketplace_name}: {stderr}",
309
+ file=sys.stderr,
310
+ )
311
+ return False
312
+ return True
313
+
314
+
315
+ def install_marketplace_plugins(marketplace_dir: pathlib.Path) -> None:
316
+ """Orchestrate marketplace plugin installation.
317
+
318
+ Locates claude binary, discovers marketplace entries, registers each
319
+ marketplace, discovers and installs plugins.
320
+
321
+ Args:
322
+ marketplace_dir: Path to CLAUDE_MARKETPLACES_DIR.
323
+
324
+ Raises:
325
+ SystemExit: If claude binary not found or any operation fails.
326
+ """
327
+ claude_bin = locate_claude_binary()
328
+
329
+ if not marketplace_dir.is_dir():
330
+ print(
331
+ f"Warning: Marketplace directory does not exist: {marketplace_dir}. No marketplaces to register.",
332
+ file=sys.stderr,
333
+ )
334
+ return
335
+
336
+ entries = discover_marketplace_entries(marketplace_dir)
337
+ if not entries:
338
+ print("Warning: No marketplace entries found. Nothing to do.", file=sys.stderr)
339
+ return
340
+
341
+ marketplaces_processed = 0
342
+ marketplaces_registered = 0
343
+ plugins_installed = 0
344
+ any_failures = False
345
+
346
+ for entry in entries:
347
+ marketplaces_processed += 1
348
+ marketplace_name = read_marketplace_name(entry)
349
+
350
+ reg_success = register_marketplace(claude_bin, entry)
351
+ if reg_success:
352
+ marketplaces_registered += 1
353
+ else:
354
+ any_failures = True
355
+
356
+ plugins = discover_plugins(entry)
357
+ for plugin_name, _plugin_path in plugins:
358
+ success = install_plugin(claude_bin, plugin_name, marketplace_name)
359
+ if success:
360
+ plugins_installed += 1
361
+ else:
362
+ any_failures = True
363
+
364
+ print(
365
+ f"Install summary: {marketplaces_processed} marketplaces processed, "
366
+ f"{marketplaces_registered} registered, {plugins_installed} plugins installed"
367
+ )
368
+
369
+ if any_failures:
370
+ print(
371
+ "Error: Some marketplace operations failed (see errors above).",
372
+ file=sys.stderr,
373
+ )
374
+ sys.exit(1)
375
+
376
+
377
+ def uninstall_marketplace_plugins(marketplace_dir: pathlib.Path) -> None:
378
+ """Orchestrate marketplace plugin uninstallation.
379
+
380
+ Locates claude binary, discovers marketplace entries, uninstalls each
381
+ plugin, then removes marketplace registrations.
382
+
383
+ Args:
384
+ marketplace_dir: Path to CLAUDE_MARKETPLACES_DIR.
385
+
386
+ Raises:
387
+ SystemExit: If claude binary not found or any operation fails.
388
+ """
389
+ claude_bin = locate_claude_binary()
390
+
391
+ if not marketplace_dir.is_dir():
392
+ print(
393
+ f"Warning: Marketplace directory does not exist: {marketplace_dir}. No marketplaces to uninstall.",
394
+ file=sys.stderr,
395
+ )
396
+ return
397
+
398
+ entries = discover_marketplace_entries(marketplace_dir)
399
+ if not entries:
400
+ print("Warning: No marketplace entries found. Nothing to uninstall.", file=sys.stderr)
401
+ return
402
+
403
+ marketplaces_processed = 0
404
+ plugins_uninstalled = 0
405
+ any_failures = False
406
+
407
+ for entry in entries:
408
+ marketplaces_processed += 1
409
+ marketplace_name = read_marketplace_name(entry)
410
+
411
+ plugins = discover_plugins(entry)
412
+ for plugin_name, _plugin_path in plugins:
413
+ success = uninstall_plugin(claude_bin, plugin_name, marketplace_name)
414
+ if success:
415
+ plugins_uninstalled += 1
416
+ else:
417
+ any_failures = True
418
+
419
+ remove_success = remove_marketplace(claude_bin, marketplace_name)
420
+ if not remove_success:
421
+ any_failures = True
422
+
423
+ print(
424
+ f"Uninstall summary: {marketplaces_processed} marketplace(s) processed, "
425
+ f"{plugins_uninstalled} plugin(s) uninstalled"
426
+ )
427
+
428
+ if any_failures:
429
+ print(
430
+ "Error: Some marketplace operations failed (see errors above).",
431
+ file=sys.stderr,
432
+ )
433
+ sys.exit(1)
@@ -0,0 +1,237 @@
1
+ """Validate marketplace XML manifest files.
2
+
3
+ Checks:
4
+ - All <linkfile dest> attributes use the ${CLAUDE_MARKETPLACES_DIR}
5
+ variable prefix, rejecting hard-coded or relative paths.
6
+ - All <include> chains are unbroken (every referenced file exists).
7
+ - All flattened project path names are unique across manifests.
8
+ - All <project revision> attributes follow valid formats.
9
+ """
10
+
11
+ import sys
12
+ import xml.etree.ElementTree as ET
13
+ from pathlib import Path
14
+
15
+ from kanon_cli.constants import (
16
+ ALLOWED_BRANCHES,
17
+ CONSTRAINT_RE,
18
+ MARKETPLACE_DIR_PREFIX,
19
+ MARKETPLACE_FILE_GLOB,
20
+ REFS_TAGS_RE,
21
+ )
22
+
23
+
24
+ def validate_linkfile_dest(xml_path: Path) -> list[str]:
25
+ """Validate all linkfile dest attributes in a manifest XML file.
26
+
27
+ Checks that every <linkfile> element's dest attribute starts with
28
+ ${CLAUDE_MARKETPLACES_DIR}/. Returns a list of error messages for
29
+ any violations found. An empty list means validation passed.
30
+
31
+ Args:
32
+ xml_path: Path to the XML manifest file to validate.
33
+
34
+ Returns:
35
+ List of error messages. Empty if all dest attributes are valid.
36
+ Each error identifies the file, project name, and invalid dest.
37
+ """
38
+ errors: list[str] = []
39
+ tree = ET.parse(xml_path)
40
+ root = tree.getroot()
41
+
42
+ for project in root.findall("project"):
43
+ project_name = project.get("name", "<unknown>")
44
+ for linkfile in project.findall("linkfile"):
45
+ dest = linkfile.get("dest", "")
46
+ if not dest.startswith(MARKETPLACE_DIR_PREFIX):
47
+ errors.append(
48
+ f"{xml_path}: project '{project_name}' has "
49
+ f"invalid linkfile dest='{dest}' — "
50
+ f"must start with {MARKETPLACE_DIR_PREFIX}"
51
+ )
52
+
53
+ return errors
54
+
55
+
56
+ def validate_include_chain(
57
+ xml_path: Path,
58
+ repo_root: Path,
59
+ ) -> list[str]:
60
+ """Validate that all includes in a manifest chain resolve to files.
61
+
62
+ Recursively follows <include> elements starting from xml_path,
63
+ checking that each referenced file exists. Returns errors for any
64
+ broken links in the chain.
65
+
66
+ Args:
67
+ xml_path: Path to the XML manifest file to validate.
68
+ repo_root: Repository root for resolving include paths.
69
+
70
+ Returns:
71
+ List of error messages. Empty if the entire chain is valid.
72
+ Each error identifies the source file and missing include.
73
+ """
74
+ errors: list[str] = []
75
+ visited: set[str] = set()
76
+
77
+ def _walk(current_path: Path) -> None:
78
+ resolved = str(current_path.resolve())
79
+ if resolved in visited:
80
+ return
81
+ visited.add(resolved)
82
+
83
+ try:
84
+ tree = ET.parse(current_path)
85
+ except ET.ParseError as exc:
86
+ errors.append(f"{current_path}: XML parse error: {exc}")
87
+ return
88
+ root = tree.getroot()
89
+
90
+ for include in root.findall("include"):
91
+ name = include.get("name")
92
+ if not name:
93
+ errors.append(f'{current_path}: <include> element missing required "name" attribute')
94
+ continue
95
+ include_path = repo_root / name
96
+ if not include_path.exists():
97
+ errors.append(f'{current_path}: <include name="{name}"> references non-existent file: {include_path}')
98
+ else:
99
+ _walk(include_path)
100
+
101
+ _walk(xml_path)
102
+ return errors
103
+
104
+
105
+ def validate_name_uniqueness(xml_files: list[Path]) -> list[str]:
106
+ """Validate that all project path attributes are unique across manifests.
107
+
108
+ Parses each XML file, collects all <project path="..."> values, and
109
+ reports any duplicates along with the files containing them.
110
+
111
+ Args:
112
+ xml_files: List of paths to marketplace XML manifest files.
113
+
114
+ Returns:
115
+ List of error messages. Empty if all paths are unique.
116
+ Each error identifies the duplicate path and conflicting files.
117
+ """
118
+ errors: list[str] = []
119
+ path_to_files: dict[str, list[str]] = {}
120
+
121
+ for xml_file in xml_files:
122
+ tree = ET.parse(xml_file)
123
+ root = tree.getroot()
124
+ for project in root.findall("project"):
125
+ path_attr = project.get("path", "")
126
+ if path_attr:
127
+ if path_attr not in path_to_files:
128
+ path_to_files[path_attr] = []
129
+ path_to_files[path_attr].append(str(xml_file))
130
+
131
+ for path_attr, files in path_to_files.items():
132
+ if len(files) > 1:
133
+ file_list = ", ".join(files)
134
+ errors.append(f"Duplicate project path '{path_attr}' found in: {file_list}")
135
+
136
+ return errors
137
+
138
+
139
+ def _is_valid_revision(revision: str) -> bool:
140
+ """Check if a revision string is a valid format.
141
+
142
+ Valid formats:
143
+ - refs/tags/<path>/<semver> (e.g., refs/tags/example/proj/1.0.0)
144
+ - Single version constraints (~=1.2.0, >=1.0.0, <2.0.0)
145
+ - Compound version constraints (>=1.0.0,<2.0.0)
146
+ - Wildcard (*)
147
+ - Branch names (main)
148
+ """
149
+ if revision in ALLOWED_BRANCHES:
150
+ return True
151
+ if revision == "*":
152
+ return True
153
+ if REFS_TAGS_RE.match(revision):
154
+ return True
155
+ # Support compound constraints separated by commas (e.g., >=1.0.0,<2.0.0)
156
+ parts = revision.split(",")
157
+ if all(CONSTRAINT_RE.match(part) for part in parts):
158
+ return True
159
+ return False
160
+
161
+
162
+ def validate_tag_format(xml_files: list[Path]) -> list[str]:
163
+ """Validate that all project revision attributes follow valid formats.
164
+
165
+ Checks that each <project> element's revision attribute is either a
166
+ refs/tags/<path>/<semver> tag, a version constraint, a wildcard, or
167
+ an allowed branch name. Returns errors for any invalid revisions.
168
+
169
+ Args:
170
+ xml_files: List of paths to marketplace XML manifest files.
171
+
172
+ Returns:
173
+ List of error messages. Empty if all revisions are valid.
174
+ Each error identifies the file, project name, and invalid revision.
175
+ """
176
+ errors: list[str] = []
177
+
178
+ for xml_file in xml_files:
179
+ tree = ET.parse(xml_file)
180
+ root = tree.getroot()
181
+ for project in root.findall("project"):
182
+ revision = project.get("revision", "")
183
+ if revision and not _is_valid_revision(revision):
184
+ project_name = project.get("name", "<unknown>")
185
+ errors.append(
186
+ f"{xml_file}: project '{project_name}' has "
187
+ f"invalid revision='{revision}' — must be "
188
+ f"refs/tags/<path>/<semver>, a version constraint, "
189
+ f"or an allowed branch"
190
+ )
191
+
192
+ return errors
193
+
194
+
195
+ def validate_marketplace(repo_root: Path) -> int:
196
+ """Validate all marketplace XML files found under repo-specs/.
197
+
198
+ Scans for *-marketplace.xml files and validates each one
199
+ for linkfile dest attributes and include chain integrity.
200
+ Exits with non-zero code if any validation errors are found.
201
+
202
+ Args:
203
+ repo_root: Repository root directory.
204
+
205
+ Returns:
206
+ 0 if all files pass validation, 1 otherwise.
207
+ """
208
+ marketplace_files = sorted(repo_root.joinpath("repo-specs").rglob(MARKETPLACE_FILE_GLOB))
209
+
210
+ if not marketplace_files:
211
+ print(
212
+ "Error: No *-marketplace.xml files found under repo-specs/",
213
+ file=sys.stderr,
214
+ )
215
+ return 1
216
+
217
+ all_errors: list[str] = []
218
+ for xml_file in marketplace_files:
219
+ rel_path = xml_file.relative_to(repo_root)
220
+ print(f"Validating {rel_path}...")
221
+ all_errors.extend(validate_linkfile_dest(xml_file))
222
+ all_errors.extend(validate_include_chain(xml_file, repo_root))
223
+
224
+ all_errors.extend(validate_name_uniqueness(marketplace_files))
225
+ all_errors.extend(validate_tag_format(marketplace_files))
226
+
227
+ if all_errors:
228
+ print(
229
+ f"\nFound {len(all_errors)} validation error(s):",
230
+ file=sys.stderr,
231
+ )
232
+ for error in all_errors:
233
+ print(f" {error}", file=sys.stderr)
234
+ return 1
235
+
236
+ print(f"\nAll {len(marketplace_files)} marketplace files passed.")
237
+ return 0