dsh-crate-web 0.1.0
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.
- package/README.md +302 -0
- package/README.zh-CN.md +304 -0
- package/cordis.patch.yml +11 -0
- package/core/dsh_pack/__init__.py +77 -0
- package/core/dsh_pack/__main__.py +4 -0
- package/core/dsh_pack/cli.py +372 -0
- package/core/dsh_pack/errors.py +167 -0
- package/core/dsh_pack/export.py +912 -0
- package/core/dsh_pack/importer.py +1347 -0
- package/core/dsh_pack/network.py +77 -0
- package/core/dsh_pack/pack.py +248 -0
- package/core/dsh_pack/preflight.py +811 -0
- package/core/dsh_pack/user_cli.py +462 -0
- package/core/dsh_pack/validation.py +272 -0
- package/core/dsh_pack/verify.py +1336 -0
- package/lib/client.js +1623 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +1275 -0
- package/package.json +73 -0
- package/restart-helper.mjs +121 -0
- package/runtime/python/LICENSE.txt +676 -0
- package/runtime/python/__install__.json +82 -0
- package/runtime/python/_asyncio.pyd +0 -0
- package/runtime/python/_bz2.pyd +0 -0
- package/runtime/python/_ctypes.pyd +0 -0
- package/runtime/python/_decimal.pyd +0 -0
- package/runtime/python/_elementtree.pyd +0 -0
- package/runtime/python/_hashlib.pyd +0 -0
- package/runtime/python/_lzma.pyd +0 -0
- package/runtime/python/_multiprocessing.pyd +0 -0
- package/runtime/python/_overlapped.pyd +0 -0
- package/runtime/python/_queue.pyd +0 -0
- package/runtime/python/_remote_debugging.pyd +0 -0
- package/runtime/python/_socket.pyd +0 -0
- package/runtime/python/_sqlite3.pyd +0 -0
- package/runtime/python/_ssl.pyd +0 -0
- package/runtime/python/_uuid.pyd +0 -0
- package/runtime/python/_wmi.pyd +0 -0
- package/runtime/python/_zoneinfo.pyd +0 -0
- package/runtime/python/_zstd.pyd +0 -0
- package/runtime/python/libcrypto-3.dll +0 -0
- package/runtime/python/libffi-8.dll +0 -0
- package/runtime/python/libssl-3.dll +0 -0
- package/runtime/python/pyexpat.pyd +0 -0
- package/runtime/python/python.cat +0 -0
- package/runtime/python/python.exe +0 -0
- package/runtime/python/python3.dll +0 -0
- package/runtime/python/python314._pth +6 -0
- package/runtime/python/python314.dll +0 -0
- package/runtime/python/python314.zip +0 -0
- package/runtime/python/pythonw.exe +0 -0
- package/runtime/python/select.pyd +0 -0
- package/runtime/python/sqlite3.dll +0 -0
- package/runtime/python/unicodedata.pyd +0 -0
- package/runtime/python/vcruntime140.dll +0 -0
- package/runtime/python/vcruntime140_1.dll +0 -0
- package/runtime/python/winsound.pyd +0 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""User-facing Phase 6.5 CLI adapter.
|
|
2
|
+
|
|
3
|
+
This module is deliberately a thin boundary around the existing Core CLI.
|
|
4
|
+
It translates stable user arguments, preserves the Core JSON result under
|
|
5
|
+
``result``, and adds operation reports. Pack schema, Preflight, Import, and
|
|
6
|
+
Verify decisions remain owned by :mod:`dsh_pack.cli` and the Core modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import uuid
|
|
16
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from io import StringIO
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Sequence
|
|
21
|
+
|
|
22
|
+
from . import cli as core_cli
|
|
23
|
+
from .errors import PackImportError
|
|
24
|
+
from .importer import ImportOptions, create_import_plan
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_OPERATION_ID = re.compile(r"^op-[0-9a-f]{32}$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _default_dsh_home() -> Path:
|
|
31
|
+
value = os.environ.get("DSH_HOME")
|
|
32
|
+
return (Path(value).expanduser() if value else Path.home() / ".dsh").resolve()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _dsh_home(value: str | None) -> Path:
|
|
36
|
+
return (Path(value).expanduser() if value else _default_dsh_home()).resolve()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _profile_path(value: str, home: Path) -> Path:
|
|
40
|
+
candidate = Path(value).expanduser()
|
|
41
|
+
if (
|
|
42
|
+
candidate.is_absolute()
|
|
43
|
+
or candidate.exists()
|
|
44
|
+
or "/" in value
|
|
45
|
+
or "\\" in value
|
|
46
|
+
or (len(value) > 1 and value[1] == ":")
|
|
47
|
+
):
|
|
48
|
+
return candidate.resolve()
|
|
49
|
+
return (home / "profiles" / value).resolve()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _operation_home(args: argparse.Namespace, *, profile: str | None = None) -> Path:
|
|
53
|
+
explicit = getattr(args, "dsh_home", None)
|
|
54
|
+
if explicit:
|
|
55
|
+
return _dsh_home(explicit)
|
|
56
|
+
if profile:
|
|
57
|
+
candidate = Path(profile).expanduser()
|
|
58
|
+
if candidate.is_absolute() or "/" in profile or "\\" in profile:
|
|
59
|
+
resolved = candidate.resolve()
|
|
60
|
+
if resolved.parent.name == "profiles":
|
|
61
|
+
return resolved.parent.parent
|
|
62
|
+
return _default_dsh_home()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _json_error(message: str) -> dict[str, Any]:
|
|
66
|
+
return {"status": "failed", "error": {"message": message}}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _invoke_core(argv: list[str]) -> tuple[int, dict[str, Any]]:
|
|
70
|
+
"""Invoke the existing Core entry point and parse its JSON contract."""
|
|
71
|
+
|
|
72
|
+
stdout = StringIO()
|
|
73
|
+
stderr = StringIO()
|
|
74
|
+
try:
|
|
75
|
+
with redirect_stdout(stdout), redirect_stderr(stderr):
|
|
76
|
+
exit_code = core_cli.main([*argv, "--json"])
|
|
77
|
+
except SystemExit as error:
|
|
78
|
+
exit_code = int(error.code or 0)
|
|
79
|
+
except Exception as error: # keep the user CLI machine-readable
|
|
80
|
+
return 1, _json_error(str(error))
|
|
81
|
+
|
|
82
|
+
raw = stdout.getvalue().strip()
|
|
83
|
+
if not raw:
|
|
84
|
+
return exit_code, _json_error(stderr.getvalue().strip() or "Core returned no JSON result")
|
|
85
|
+
try:
|
|
86
|
+
result = json.loads(raw)
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
return exit_code, _json_error(raw)
|
|
89
|
+
if not isinstance(result, dict):
|
|
90
|
+
return exit_code, _json_error("Core returned a non-object JSON result")
|
|
91
|
+
return exit_code, result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _user_status(command: str, exit_code: int, result: dict[str, Any]) -> str:
|
|
95
|
+
if command == "verify":
|
|
96
|
+
status = result.get("status")
|
|
97
|
+
return status if isinstance(status, str) else ("PASS" if exit_code == 0 else "FAIL")
|
|
98
|
+
if command == "inspect":
|
|
99
|
+
status = result.get("status")
|
|
100
|
+
return status if isinstance(status, str) else ("READY" if exit_code == 0 else "NOT_READY")
|
|
101
|
+
return "PASS" if exit_code == 0 else "FAIL"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _envelope(
|
|
105
|
+
command: str,
|
|
106
|
+
exit_code: int,
|
|
107
|
+
result: dict[str, Any],
|
|
108
|
+
*,
|
|
109
|
+
operation_id: str | None,
|
|
110
|
+
dry_run: bool = False,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
status = _user_status(command, exit_code, result)
|
|
113
|
+
if dry_run and command == "import":
|
|
114
|
+
preflight = result.get("plan", {}).get("preflight", {}) if isinstance(result.get("plan"), dict) else {}
|
|
115
|
+
if isinstance(preflight, dict) and preflight.get("status") in {"READY", "NOT_READY"}:
|
|
116
|
+
status = preflight["status"]
|
|
117
|
+
return {
|
|
118
|
+
"operationId": operation_id,
|
|
119
|
+
"command": command,
|
|
120
|
+
"status": status,
|
|
121
|
+
"exitCode": exit_code,
|
|
122
|
+
"dryRun": dry_run,
|
|
123
|
+
"result": result,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _new_operation_id() -> str:
|
|
128
|
+
return f"op-{uuid.uuid4().hex}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _operation_path(home: Path, operation_id: str) -> Path:
|
|
132
|
+
if not _OPERATION_ID.fullmatch(operation_id):
|
|
133
|
+
raise ValueError("invalid operation id")
|
|
134
|
+
return home / ".dsh-pack" / "operations" / f"{operation_id}.json"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _record_operation(home: Path, envelope: dict[str, Any]) -> None:
|
|
138
|
+
operation_id = envelope.get("operationId")
|
|
139
|
+
if not isinstance(operation_id, str):
|
|
140
|
+
return
|
|
141
|
+
path = _operation_path(home, operation_id)
|
|
142
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
record = {
|
|
144
|
+
"schemaVersion": 1,
|
|
145
|
+
"createdAt": datetime.now(timezone.utc).isoformat(),
|
|
146
|
+
**envelope,
|
|
147
|
+
}
|
|
148
|
+
temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
149
|
+
try:
|
|
150
|
+
temporary.write_text(json.dumps(record, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
151
|
+
os.replace(temporary, path)
|
|
152
|
+
finally:
|
|
153
|
+
if temporary.exists():
|
|
154
|
+
temporary.unlink()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _load_operation(home: Path, operation_id: str) -> dict[str, Any]:
|
|
158
|
+
path = _operation_path(home, operation_id)
|
|
159
|
+
try:
|
|
160
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
161
|
+
except FileNotFoundError as error:
|
|
162
|
+
raise ValueError(f"operation report not found: {operation_id}") from error
|
|
163
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
164
|
+
raise ValueError(f"operation report is unreadable: {operation_id}") from error
|
|
165
|
+
if not isinstance(value, dict):
|
|
166
|
+
raise ValueError(f"operation report is not a JSON object: {operation_id}")
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _print(value: dict[str, Any], *, as_json: bool) -> None:
|
|
171
|
+
if as_json:
|
|
172
|
+
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
|
|
173
|
+
return
|
|
174
|
+
operation_id = value.get("operationId")
|
|
175
|
+
if operation_id:
|
|
176
|
+
print(f"Operation: {operation_id}")
|
|
177
|
+
elif value.get("dryRun"):
|
|
178
|
+
print("Operation: not persisted (--dry-run)")
|
|
179
|
+
print(f"Status: {value.get('status', 'UNKNOWN')}")
|
|
180
|
+
result = value.get("result", value)
|
|
181
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _append_option(argv: list[str], flag: str, value: str | None) -> None:
|
|
185
|
+
if value:
|
|
186
|
+
argv.extend([flag, value])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _append_repeated(argv: list[str], flag: str, values: list[str]) -> None:
|
|
190
|
+
for value in values:
|
|
191
|
+
argv.extend([flag, value])
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _export_core_args(args: argparse.Namespace, home: Path) -> list[str]:
|
|
195
|
+
argv = ["export", "--profile", str(_profile_path(args.profile, home)), "--output", str(Path(args.output).expanduser()), "--json"]
|
|
196
|
+
_append_option(argv, "--environment-json", args.environment_json)
|
|
197
|
+
for flag, value in (
|
|
198
|
+
("--dsh-version", args.dsh_version),
|
|
199
|
+
("--node-version", args.node_version),
|
|
200
|
+
("--os-name", args.os_name),
|
|
201
|
+
("--os-version", args.os_version),
|
|
202
|
+
("--os-arch", args.os_arch),
|
|
203
|
+
):
|
|
204
|
+
_append_option(argv, flag, value)
|
|
205
|
+
_append_repeated(argv, "--embed", args.embed)
|
|
206
|
+
_append_repeated(argv, "--reference-only", args.reference_only)
|
|
207
|
+
if args.include_installed_bundles:
|
|
208
|
+
argv.append("--include-installed-bundles")
|
|
209
|
+
_append_repeated(argv, "--required-secret", args.required_secret)
|
|
210
|
+
return argv
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _inspect_core_args(args: argparse.Namespace) -> list[str]:
|
|
214
|
+
argv = ["inspect", args.pack, "--json"]
|
|
215
|
+
for flag, value in (
|
|
216
|
+
("--dsh-version", args.dsh_version),
|
|
217
|
+
("--node-version", args.node_version),
|
|
218
|
+
("--os-name", args.os_name),
|
|
219
|
+
("--os-version", args.os_version),
|
|
220
|
+
):
|
|
221
|
+
_append_option(argv, flag, value)
|
|
222
|
+
_append_repeated(argv, "--secret", args.secret)
|
|
223
|
+
_append_repeated(argv, "--plugin", args.plugin)
|
|
224
|
+
if args.check_reference_plugins:
|
|
225
|
+
argv.append("--check-reference-plugins")
|
|
226
|
+
if args.no_process_secrets:
|
|
227
|
+
argv.append("--no-process-secrets")
|
|
228
|
+
return argv
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _import_core_args(args: argparse.Namespace, home: Path) -> list[str]:
|
|
232
|
+
argv = ["import", args.pack, "--dsh-home", str(home), "--json"]
|
|
233
|
+
for flag, value in (
|
|
234
|
+
("--dsh-version", args.dsh_version),
|
|
235
|
+
("--node-version", args.node_version),
|
|
236
|
+
("--os-name", args.os_name),
|
|
237
|
+
("--os-version", args.os_version),
|
|
238
|
+
):
|
|
239
|
+
_append_option(argv, flag, value)
|
|
240
|
+
_append_repeated(argv, "--secret", args.secret)
|
|
241
|
+
_append_repeated(argv, "--reference-plugin", args.reference_plugin)
|
|
242
|
+
_append_option(argv, "--target-profile", args.target_profile)
|
|
243
|
+
if args.overwrite:
|
|
244
|
+
argv.append("--overwrite")
|
|
245
|
+
if args.confirm_overwrite:
|
|
246
|
+
argv.append("--confirm-overwrite")
|
|
247
|
+
if args.offline:
|
|
248
|
+
argv.append("--offline")
|
|
249
|
+
if args.no_process_secrets:
|
|
250
|
+
argv.append("--no-process-secrets")
|
|
251
|
+
return argv
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _verify_core_args(args: argparse.Namespace, home: Path) -> list[str]:
|
|
255
|
+
argv = ["verify", "--dsh-home", str(home), "--profile", args.profile, "--mode", args.mode, "--json"]
|
|
256
|
+
_append_option(argv, "--runner-config", args.runner_config)
|
|
257
|
+
_append_repeated(argv, "--plugin-contract", args.plugin_contract)
|
|
258
|
+
for flag, value in (
|
|
259
|
+
("--dsh-version", args.dsh_version),
|
|
260
|
+
("--node-version", args.node_version),
|
|
261
|
+
("--os-name", args.os_name),
|
|
262
|
+
("--os-version", args.os_version),
|
|
263
|
+
):
|
|
264
|
+
_append_option(argv, flag, value)
|
|
265
|
+
_append_repeated(argv, "--secret", args.secret)
|
|
266
|
+
if args.no_process_secrets:
|
|
267
|
+
argv.append("--no-process-secrets")
|
|
268
|
+
return argv
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _delete_core_args(args: argparse.Namespace, home: Path) -> list[str]:
|
|
272
|
+
argv = ["delete-profile", "--dsh-home", str(home), "--profile", args.profile, "--json"]
|
|
273
|
+
if args.confirm_delete:
|
|
274
|
+
argv.append("--confirm-delete")
|
|
275
|
+
return argv
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _finish_operation(
|
|
279
|
+
command: str,
|
|
280
|
+
args: argparse.Namespace,
|
|
281
|
+
home: Path,
|
|
282
|
+
exit_code: int,
|
|
283
|
+
result: dict[str, Any],
|
|
284
|
+
) -> int:
|
|
285
|
+
envelope = _envelope(command, exit_code, result, operation_id=_new_operation_id())
|
|
286
|
+
try:
|
|
287
|
+
_record_operation(home, envelope)
|
|
288
|
+
except OSError as error:
|
|
289
|
+
# The Core result remains authoritative; report persistence is a
|
|
290
|
+
# secondary diagnostic aid and must not rewrite Core decisions.
|
|
291
|
+
envelope["reportError"] = str(error)
|
|
292
|
+
_print(envelope, as_json=args.json)
|
|
293
|
+
return exit_code
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _run_export(args: argparse.Namespace) -> int:
|
|
297
|
+
home = _operation_home(args, profile=args.profile)
|
|
298
|
+
exit_code, result = _invoke_core(_export_core_args(args, home))
|
|
299
|
+
return _finish_operation("export", args, home, exit_code, result)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _run_inspect(args: argparse.Namespace) -> int:
|
|
303
|
+
home = _operation_home(args)
|
|
304
|
+
exit_code, result = _invoke_core(_inspect_core_args(args))
|
|
305
|
+
return _finish_operation("inspect", args, home, exit_code, result)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _import_options(args: argparse.Namespace) -> ImportOptions:
|
|
309
|
+
environment: dict[str, dict[str, str]] = {}
|
|
310
|
+
for section, version in (("dsh", args.dsh_version), ("node", args.node_version)):
|
|
311
|
+
if version:
|
|
312
|
+
environment[section] = {"version": version}
|
|
313
|
+
if args.os_name or args.os_version:
|
|
314
|
+
environment["os"] = {
|
|
315
|
+
key: value
|
|
316
|
+
for key, value in (("name", args.os_name), ("version", args.os_version))
|
|
317
|
+
if value
|
|
318
|
+
}
|
|
319
|
+
available_secrets = set() if args.no_process_secrets else set(os.environ)
|
|
320
|
+
available_secrets.update(args.secret)
|
|
321
|
+
return ImportOptions(
|
|
322
|
+
current_environment=environment,
|
|
323
|
+
available_secrets=available_secrets,
|
|
324
|
+
reference_plugin_sources=core_cli._reference_sources(args.reference_plugin),
|
|
325
|
+
allow_network_reference_install=not args.offline,
|
|
326
|
+
target_profile_name=args.target_profile,
|
|
327
|
+
overwrite_existing_profile=args.overwrite,
|
|
328
|
+
overwrite_confirmation="OVERWRITE" if args.confirm_overwrite else None,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _run_dry_import(args: argparse.Namespace, home: Path) -> tuple[int, dict[str, Any]]:
|
|
333
|
+
try:
|
|
334
|
+
plan = create_import_plan(args.pack, home, options=_import_options(args))
|
|
335
|
+
except (PackImportError, ValueError, OSError) as error:
|
|
336
|
+
return 1, {"status": "failed", "error": error.as_dict() if isinstance(error, PackImportError) else {"message": str(error)}}
|
|
337
|
+
return 0, {"status": "dry-run", "readOnly": True, "plan": plan.as_dict()}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _run_import(args: argparse.Namespace) -> int:
|
|
341
|
+
home = _dsh_home(args.dsh_home)
|
|
342
|
+
if args.dry_run:
|
|
343
|
+
exit_code, result = _run_dry_import(args, home)
|
|
344
|
+
envelope = _envelope("import", exit_code, result, operation_id=None, dry_run=True)
|
|
345
|
+
_print(envelope, as_json=args.json)
|
|
346
|
+
return exit_code
|
|
347
|
+
exit_code, result = _invoke_core(_import_core_args(args, home))
|
|
348
|
+
return _finish_operation("import", args, home, exit_code, result)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _run_verify(args: argparse.Namespace) -> int:
|
|
352
|
+
home = _dsh_home(args.dsh_home)
|
|
353
|
+
exit_code, result = _invoke_core(_verify_core_args(args, home))
|
|
354
|
+
return _finish_operation("verify", args, home, exit_code, result)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _run_delete_profile(args: argparse.Namespace) -> int:
|
|
358
|
+
home = _dsh_home(args.dsh_home)
|
|
359
|
+
exit_code, result = _invoke_core(_delete_core_args(args, home))
|
|
360
|
+
return _finish_operation("delete-profile", args, home, exit_code, result)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _run_report(args: argparse.Namespace) -> int:
|
|
364
|
+
try:
|
|
365
|
+
report = _load_operation(_dsh_home(args.dsh_home), args.operation_id)
|
|
366
|
+
except ValueError as error:
|
|
367
|
+
value = {"status": "FAIL", "error": {"message": str(error)}}
|
|
368
|
+
_print(value, as_json=args.json)
|
|
369
|
+
return 1
|
|
370
|
+
_print(report, as_json=args.json)
|
|
371
|
+
# Reading a report succeeded even when the recorded operation failed.
|
|
372
|
+
# The original operation exit code remains available in the report.
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _add_runtime_options(parser: argparse.ArgumentParser) -> None:
|
|
377
|
+
parser.add_argument("--dsh-home", help="DSH_HOME used for Profile resolution and operation reports")
|
|
378
|
+
parser.add_argument("--dsh-version", help="current DSH version evidence")
|
|
379
|
+
parser.add_argument("--node-version", help="current Node version evidence")
|
|
380
|
+
parser.add_argument("--os-name", help="current OS name evidence")
|
|
381
|
+
parser.add_argument("--os-version", help="current OS version evidence")
|
|
382
|
+
parser.add_argument("--secret", action="append", default=[], help="declare an available Secret name")
|
|
383
|
+
parser.add_argument("--no-process-secrets", action="store_true", help="do not use current process environment names")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
387
|
+
parser = argparse.ArgumentParser(prog="dsh-crate", description="User-facing DSH Crate commands backed by the existing Core")
|
|
388
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
389
|
+
|
|
390
|
+
export = subparsers.add_parser("export", help="export one Profile to a .dshcrate")
|
|
391
|
+
export.add_argument("--profile", required=True, help="Profile name or Profile directory")
|
|
392
|
+
export.add_argument("-o", "--output", required=True, help="output .dshcrate path")
|
|
393
|
+
export.add_argument("--environment-json", help="JSON file containing Pack environment")
|
|
394
|
+
export.add_argument("--dsh-version", default="unknown", help="DSH version evidence")
|
|
395
|
+
export.add_argument("--node-version", default="unknown", help="Node version evidence")
|
|
396
|
+
export.add_argument("--os-name", default="unknown", help="OS name evidence")
|
|
397
|
+
export.add_argument("--os-version", default="unknown", help="OS version evidence")
|
|
398
|
+
export.add_argument("--os-arch", default="unknown", help="OS architecture evidence")
|
|
399
|
+
export.add_argument("--embed", action="append", default=[], metavar="PLUGIN")
|
|
400
|
+
export.add_argument("--reference-only", action="append", default=[], metavar="PLUGIN")
|
|
401
|
+
export.add_argument("--include-installed-bundles", action="store_true", help="include direct installed Bundle inventory in the exported Profile composition")
|
|
402
|
+
export.add_argument("--required-secret", action="append", default=[], metavar="NAME")
|
|
403
|
+
export.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
404
|
+
export.add_argument("--dsh-home", help="DSH_HOME used for Profile-name resolution and reports")
|
|
405
|
+
export.set_defaults(handler=_run_export)
|
|
406
|
+
|
|
407
|
+
inspect = subparsers.add_parser("inspect", help="read-only Pack preflight")
|
|
408
|
+
inspect.add_argument("pack", help="input .dshcrate")
|
|
409
|
+
_add_runtime_options(inspect)
|
|
410
|
+
inspect.add_argument("--plugin", action="append", default=[], metavar="NAME[@VERSION]")
|
|
411
|
+
inspect.add_argument("--check-reference-plugins", action="store_true")
|
|
412
|
+
inspect.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
413
|
+
inspect.set_defaults(handler=_run_inspect)
|
|
414
|
+
|
|
415
|
+
import_parser = subparsers.add_parser("import", help="import a Pack as a new Profile")
|
|
416
|
+
import_parser.add_argument("pack", help="input .dshcrate")
|
|
417
|
+
_add_runtime_options(import_parser)
|
|
418
|
+
import_parser.add_argument("--dry-run", action="store_true", help="preflight and plan only; never write the target DSH_HOME")
|
|
419
|
+
import_parser.add_argument("--offline", action="store_true", help="disable npm/GitHub fallback for missing reference-only plugins")
|
|
420
|
+
import_parser.add_argument("--target-profile", help="new target Profile name; duplicate names are rejected")
|
|
421
|
+
import_parser.add_argument("--overwrite", action="store_true", help="replace an existing Profile after confirmation")
|
|
422
|
+
import_parser.add_argument("--confirm-overwrite", action="store_true", help="confirm the destructive overwrite operation")
|
|
423
|
+
import_parser.add_argument("--reference-plugin", action="append", default=[], metavar="NAME=PACKAGE_DIR")
|
|
424
|
+
import_parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
425
|
+
import_parser.set_defaults(handler=_run_import)
|
|
426
|
+
|
|
427
|
+
verify = subparsers.add_parser("verify", help="verify a prepared Profile")
|
|
428
|
+
verify.add_argument("--profile", required=True, help="Profile name under DSH_HOME/profiles")
|
|
429
|
+
verify.add_argument("--mode", choices=("web", "headless"), default="web")
|
|
430
|
+
verify.add_argument("--runner-config", help="JSON config for an explicit subprocess VerifyAdapter")
|
|
431
|
+
verify.add_argument("--plugin-contract", action="append", default=[])
|
|
432
|
+
_add_runtime_options(verify)
|
|
433
|
+
verify.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
434
|
+
verify.set_defaults(handler=_run_verify)
|
|
435
|
+
|
|
436
|
+
report = subparsers.add_parser("report", help="read a persisted operation report")
|
|
437
|
+
report.add_argument("operation_id", help="operation id returned by a non-dry-run command")
|
|
438
|
+
report.add_argument("--dsh-home", help="DSH_HOME containing .dsh-pack/operations")
|
|
439
|
+
report.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
440
|
+
report.set_defaults(handler=_run_report)
|
|
441
|
+
|
|
442
|
+
delete = subparsers.add_parser("delete-profile", help="delete one Profile after explicit confirmation")
|
|
443
|
+
delete.add_argument("--profile", required=True, help="Profile name under DSH_HOME/profiles")
|
|
444
|
+
delete.add_argument("--confirm-delete", action="store_true", help="confirm the destructive deletion")
|
|
445
|
+
delete.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
446
|
+
delete.add_argument("--dsh-home", help="DSH_HOME containing the Profile and operation reports")
|
|
447
|
+
delete.set_defaults(handler=_run_delete_profile)
|
|
448
|
+
return parser
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
452
|
+
# The Web bridge may resolve the installed ``dsh-pack`` console script.
|
|
453
|
+
# Keep that existing integration on the raw Core contract; the bridge
|
|
454
|
+
# sets this marker explicitly and never consumes the user envelope.
|
|
455
|
+
if os.environ.get("DSH_PACK_CORE_MODE") == "1":
|
|
456
|
+
return core_cli.main(argv)
|
|
457
|
+
args = _build_parser().parse_args(argv)
|
|
458
|
+
return args.handler(args)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
if __name__ == "__main__": # pragma: no cover - exercised by the console script
|
|
462
|
+
raise SystemExit(main())
|