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,372 @@
|
|
|
1
|
+
"""Command-line entry point for the DSH Pack core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Sequence
|
|
10
|
+
|
|
11
|
+
from .errors import DshPackError, PackImportError
|
|
12
|
+
from .export import EMBEDDED, REFERENCE_ONLY, ExportOptions, PluginExportOptions, export_profile
|
|
13
|
+
from .importer import ImportOptions, create_import_plan, delete_profile, import_pack
|
|
14
|
+
from .preflight import PreflightContext, inspect_pack, render_text
|
|
15
|
+
from .verify import SubprocessVerifyAdapter, VerifyOptions, load_smoke_contracts, verify_profile
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _plugin_inventory(values: list[str]) -> dict[str, str | None]:
|
|
19
|
+
inventory: dict[str, str | None] = {}
|
|
20
|
+
for value in values:
|
|
21
|
+
if "@" in value[1:]:
|
|
22
|
+
name, version = value.rsplit("@", 1)
|
|
23
|
+
inventory[name] = version or None
|
|
24
|
+
else:
|
|
25
|
+
inventory[value] = None
|
|
26
|
+
return inventory
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _context(args: argparse.Namespace) -> PreflightContext:
|
|
30
|
+
environment: dict[str, dict[str, str]] = {}
|
|
31
|
+
for section, version in (("dsh", args.dsh_version), ("node", args.node_version)):
|
|
32
|
+
if version:
|
|
33
|
+
environment[section] = {"version": version}
|
|
34
|
+
if args.os_name or args.os_version:
|
|
35
|
+
environment["os"] = {
|
|
36
|
+
key: value
|
|
37
|
+
for key, value in (("name", args.os_name), ("version", args.os_version))
|
|
38
|
+
if value
|
|
39
|
+
}
|
|
40
|
+
available_plugins = None
|
|
41
|
+
if args.check_reference_plugins or args.plugin:
|
|
42
|
+
available_plugins = _plugin_inventory(args.plugin)
|
|
43
|
+
return PreflightContext(
|
|
44
|
+
current_environment=environment,
|
|
45
|
+
available_secrets=None if not args.no_process_secrets else set(),
|
|
46
|
+
available_plugins=available_plugins,
|
|
47
|
+
allow_network_reference_install=getattr(args, "allow_network_reference_install", False),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _inspect_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
52
|
+
parser = subparsers.add_parser("inspect", help="read-only Pack preflight")
|
|
53
|
+
parser.add_argument("pack", help="input .dshcrate")
|
|
54
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
55
|
+
parser.add_argument("--dsh-version", help="current DSH version")
|
|
56
|
+
parser.add_argument("--node-version", help="current Node version")
|
|
57
|
+
parser.add_argument("--os-name", help="current OS name")
|
|
58
|
+
parser.add_argument("--os-version", help="current OS version")
|
|
59
|
+
parser.add_argument("--secret", action="append", default=[], help="declare an available Secret name")
|
|
60
|
+
parser.add_argument("--plugin", action="append", default=[], metavar="NAME[@VERSION]", help="declare an installed reference-only plugin")
|
|
61
|
+
parser.add_argument("--check-reference-plugins", action="store_true", help="treat --plugin values as the complete plugin inventory")
|
|
62
|
+
parser.add_argument("--allow-network-reference-install", action="store_true", help="report supported missing reference-only sources as npm network installs")
|
|
63
|
+
parser.add_argument("--no-process-secrets", action="store_true", help="do not use current process environment names")
|
|
64
|
+
parser.set_defaults(handler=_run_inspect)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _export_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
68
|
+
parser = subparsers.add_parser("export", help="export an existing Profile into a .dshcrate")
|
|
69
|
+
parser.add_argument("--profile", required=True, help="Profile directory")
|
|
70
|
+
parser.add_argument("--output", required=True, help="output .dshcrate path")
|
|
71
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
72
|
+
parser.add_argument("--environment-json", help="JSON object containing Pack environment")
|
|
73
|
+
parser.add_argument("--dsh-version", default="unknown", help="DSH version evidence")
|
|
74
|
+
parser.add_argument("--node-version", default="unknown", help="Node version evidence")
|
|
75
|
+
parser.add_argument("--os-name", default="unknown", help="OS name evidence")
|
|
76
|
+
parser.add_argument("--os-version", default="unknown", help="OS version evidence")
|
|
77
|
+
parser.add_argument("--os-arch", default="unknown", help="OS architecture evidence")
|
|
78
|
+
parser.add_argument("--embed", action="append", default=[], metavar="PLUGIN", help="embed one plugin artifact")
|
|
79
|
+
parser.add_argument("--reference-only", action="append", default=[], metavar="PLUGIN", help="keep one plugin reference-only")
|
|
80
|
+
parser.add_argument("--include-installed-bundles", action="store_true", help="include direct installed Bundle inventory in the exported Profile composition")
|
|
81
|
+
parser.add_argument("--required-secret", action="append", default=[], metavar="NAME", help="record one required Secret name")
|
|
82
|
+
parser.set_defaults(handler=_run_export)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _reference_sources(values: list[str]) -> dict[str, Path]:
|
|
86
|
+
sources: dict[str, Path] = {}
|
|
87
|
+
for value in values:
|
|
88
|
+
if "=" not in value:
|
|
89
|
+
raise ValueError("--reference-plugin must use NAME=PACKAGE_DIRECTORY")
|
|
90
|
+
name, source = value.split("=", 1)
|
|
91
|
+
if not name or not source:
|
|
92
|
+
raise ValueError("--reference-plugin must use NAME=PACKAGE_DIRECTORY")
|
|
93
|
+
sources[name] = Path(source)
|
|
94
|
+
return sources
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _import_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
98
|
+
parser = subparsers.add_parser("import", help="prepare a Pack as a new Profile")
|
|
99
|
+
parser.add_argument("pack", help="input .dshcrate")
|
|
100
|
+
parser.add_argument("--dsh-home", required=True, help="target isolated DSH_HOME")
|
|
101
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
102
|
+
parser.add_argument("--dsh-version", help="current DSH version")
|
|
103
|
+
parser.add_argument("--node-version", help="current Node version")
|
|
104
|
+
parser.add_argument("--os-name", help="current OS name")
|
|
105
|
+
parser.add_argument("--os-version", help="current OS version")
|
|
106
|
+
parser.add_argument("--secret", action="append", default=[], help="declare an available Secret name")
|
|
107
|
+
parser.add_argument("--no-process-secrets", action="store_true", help="do not use current process environment names")
|
|
108
|
+
parser.add_argument("--offline", action="store_true", help="disable npm/GitHub fallback for missing reference-only plugins")
|
|
109
|
+
parser.add_argument("--target-profile", help="requested target Profile name; new imports refuse duplicate names")
|
|
110
|
+
parser.add_argument("--overwrite", action="store_true", help="replace the selected existing Profile after explicit confirmation")
|
|
111
|
+
parser.add_argument("--confirm-overwrite", action="store_true", help="confirm the destructive overwrite operation")
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--reference-plugin",
|
|
114
|
+
action="append",
|
|
115
|
+
default=[],
|
|
116
|
+
metavar="NAME=PACKAGE_DIR",
|
|
117
|
+
help="install one reference-only plugin from a local package directory",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument("--plan-only", action="store_true", help="preflight and plan only; never write the target DSH_HOME")
|
|
120
|
+
parser.set_defaults(handler=_run_import)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _run_inspect(args: argparse.Namespace) -> int:
|
|
124
|
+
context = _context(args)
|
|
125
|
+
if args.secret:
|
|
126
|
+
available = set() if context.available_secrets is not None else set()
|
|
127
|
+
if context.available_secrets is None:
|
|
128
|
+
import os
|
|
129
|
+
|
|
130
|
+
available.update(os.environ)
|
|
131
|
+
available.update(args.secret)
|
|
132
|
+
context = PreflightContext(
|
|
133
|
+
current_environment=context.current_environment,
|
|
134
|
+
available_secrets=available,
|
|
135
|
+
available_plugins=context.available_plugins,
|
|
136
|
+
allow_network_reference_install=context.allow_network_reference_install,
|
|
137
|
+
)
|
|
138
|
+
result = inspect_pack(args.pack, context=context)
|
|
139
|
+
if args.json:
|
|
140
|
+
print(json.dumps(result.as_dict(), ensure_ascii=False, indent=2, sort_keys=True))
|
|
141
|
+
else:
|
|
142
|
+
print(render_text(result))
|
|
143
|
+
return 1 if result.blockers else 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _export_environment(args: argparse.Namespace) -> dict[str, object]:
|
|
147
|
+
if args.environment_json:
|
|
148
|
+
value = json.loads(Path(args.environment_json).read_text(encoding="utf-8"))
|
|
149
|
+
if not isinstance(value, dict):
|
|
150
|
+
raise ValueError("environment JSON must contain an object")
|
|
151
|
+
return value
|
|
152
|
+
return {
|
|
153
|
+
"schemaVersion": 1,
|
|
154
|
+
"os": {"name": args.os_name, "version": args.os_version, "arch": args.os_arch},
|
|
155
|
+
"node": {"version": args.node_version},
|
|
156
|
+
"dsh": {"version": args.dsh_version},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _export_payload(result: object) -> dict[str, object]:
|
|
161
|
+
# Keep this projection deliberately small and value-safe. PackData contains
|
|
162
|
+
# artifact bytes, which must never be serialized into the UI/CLI response.
|
|
163
|
+
export_result = result
|
|
164
|
+
data = export_result.data # type: ignore[attr-defined]
|
|
165
|
+
return {
|
|
166
|
+
"status": "exported",
|
|
167
|
+
"path": str(export_result.path), # type: ignore[attr-defined]
|
|
168
|
+
"profile": data.manifest.get("profile"),
|
|
169
|
+
"environment": data.manifest.get("environment"),
|
|
170
|
+
"requiredSecrets": data.manifest.get("requiredSecrets", []),
|
|
171
|
+
"plugins": data.plugins_lock.get("plugins", []),
|
|
172
|
+
"artifactPaths": sorted(f"plugins/{name}" for name in data.plugin_artifacts),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _run_export(args: argparse.Namespace) -> int:
|
|
177
|
+
overlap = set(args.embed) & set(args.reference_only)
|
|
178
|
+
if overlap:
|
|
179
|
+
payload = {"status": "failed", "error": {"message": f"plugin selected for both --embed and --reference-only: {sorted(overlap)}"}}
|
|
180
|
+
if args.json:
|
|
181
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
182
|
+
else:
|
|
183
|
+
print(f"EXPORT FAIL: {payload['error']['message']}")
|
|
184
|
+
return 2
|
|
185
|
+
plugin_options = {name: PluginExportOptions(mode=EMBEDDED) for name in args.embed}
|
|
186
|
+
plugin_options.update({name: PluginExportOptions(mode=REFERENCE_ONLY) for name in args.reference_only})
|
|
187
|
+
try:
|
|
188
|
+
result = export_profile(
|
|
189
|
+
args.profile,
|
|
190
|
+
args.output,
|
|
191
|
+
options=ExportOptions(
|
|
192
|
+
environment=_export_environment(args),
|
|
193
|
+
required_secrets=tuple(args.required_secret),
|
|
194
|
+
plugin_options=plugin_options,
|
|
195
|
+
include_installation_bundles=args.include_installed_bundles,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
except (DshPackError, OSError, ValueError, json.JSONDecodeError) as error:
|
|
199
|
+
payload = {"status": "failed", "error": {"message": str(error)}}
|
|
200
|
+
if args.json:
|
|
201
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
202
|
+
else:
|
|
203
|
+
print(f"EXPORT FAIL: {error}")
|
|
204
|
+
return 2
|
|
205
|
+
payload = _export_payload(result)
|
|
206
|
+
if args.json:
|
|
207
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
208
|
+
else:
|
|
209
|
+
print(f"EXPORT PASS: {result.path}")
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _run_import(args: argparse.Namespace) -> int:
|
|
214
|
+
environment: dict[str, dict[str, str]] = {}
|
|
215
|
+
for section, version in (("dsh", args.dsh_version), ("node", args.node_version)):
|
|
216
|
+
if version:
|
|
217
|
+
environment[section] = {"version": version}
|
|
218
|
+
if args.os_name or args.os_version:
|
|
219
|
+
environment["os"] = {
|
|
220
|
+
key: value
|
|
221
|
+
for key, value in (("name", args.os_name), ("version", args.os_version))
|
|
222
|
+
if value
|
|
223
|
+
}
|
|
224
|
+
available_secrets = set() if args.no_process_secrets else set(os.environ)
|
|
225
|
+
available_secrets.update(args.secret)
|
|
226
|
+
try:
|
|
227
|
+
sources = _reference_sources(args.reference_plugin)
|
|
228
|
+
options = ImportOptions(
|
|
229
|
+
current_environment=environment,
|
|
230
|
+
available_secrets=available_secrets,
|
|
231
|
+
reference_plugin_sources=sources,
|
|
232
|
+
allow_network_reference_install=not args.offline,
|
|
233
|
+
target_profile_name=args.target_profile,
|
|
234
|
+
overwrite_existing_profile=args.overwrite,
|
|
235
|
+
overwrite_confirmation="OVERWRITE" if args.confirm_overwrite else None,
|
|
236
|
+
)
|
|
237
|
+
if args.plan_only:
|
|
238
|
+
plan = create_import_plan(args.pack, args.dsh_home, options=options)
|
|
239
|
+
payload = {"status": "plan", "readOnly": True, "plan": plan.as_dict()}
|
|
240
|
+
if args.json:
|
|
241
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
242
|
+
else:
|
|
243
|
+
print(f"Plan: {plan.profile_name}")
|
|
244
|
+
return 0
|
|
245
|
+
result = import_pack(args.pack, args.dsh_home, options=options)
|
|
246
|
+
except (PackImportError, ValueError, OSError) as error:
|
|
247
|
+
payload = {"status": "failed", "error": error.as_dict() if isinstance(error, PackImportError) else {"message": str(error)}}
|
|
248
|
+
if args.json:
|
|
249
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
250
|
+
else:
|
|
251
|
+
print(f"Import failed: {payload['error'].get('message', str(error))}")
|
|
252
|
+
return 1
|
|
253
|
+
if args.json:
|
|
254
|
+
print(json.dumps(result.as_dict(), ensure_ascii=False, indent=2, sort_keys=True))
|
|
255
|
+
else:
|
|
256
|
+
print(f"Status: {result.status}")
|
|
257
|
+
print(f"Profile: {result.profile_path}")
|
|
258
|
+
print(f"Metadata: {result.metadata_path}")
|
|
259
|
+
print(f"Installed plugins: {len(result.installed_plugins)}")
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _delete_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
264
|
+
parser = subparsers.add_parser("delete-profile", help="delete one Profile after explicit confirmation")
|
|
265
|
+
parser.add_argument("--dsh-home", required=True, help="DSH_HOME containing the Profile")
|
|
266
|
+
parser.add_argument("--profile", required=True, help="Profile name under DSH_HOME/profiles")
|
|
267
|
+
parser.add_argument("--confirm-delete", action="store_true", help="confirm the destructive deletion")
|
|
268
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
269
|
+
parser.set_defaults(handler=_run_delete_profile)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _run_delete_profile(args: argparse.Namespace) -> int:
|
|
273
|
+
try:
|
|
274
|
+
result = delete_profile(
|
|
275
|
+
args.dsh_home,
|
|
276
|
+
args.profile,
|
|
277
|
+
confirmation="DELETE" if args.confirm_delete else None,
|
|
278
|
+
)
|
|
279
|
+
except PackImportError as error:
|
|
280
|
+
payload = {"status": "failed", "error": error.as_dict()}
|
|
281
|
+
if args.json:
|
|
282
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
283
|
+
else:
|
|
284
|
+
print(f"Delete failed: {error}")
|
|
285
|
+
return 1
|
|
286
|
+
payload = result.as_dict()
|
|
287
|
+
if args.json:
|
|
288
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
289
|
+
else:
|
|
290
|
+
print(f"Deleted Profile: {result.profile_name}")
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _verify_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
295
|
+
parser = subparsers.add_parser("verify", help="verify a prepared Profile without changing it")
|
|
296
|
+
parser.add_argument("--dsh-home", required=True, help="isolated DSH_HOME containing the prepared Profile")
|
|
297
|
+
parser.add_argument("--profile", required=True, help="Profile name under DSH_HOME/profiles")
|
|
298
|
+
parser.add_argument("--mode", choices=("web", "headless"), default="web", help="runtime surface to verify")
|
|
299
|
+
parser.add_argument("--runner-config", help="JSON config for an explicit subprocess VerifyAdapter")
|
|
300
|
+
parser.add_argument("--plugin-contract", action="append", default=[], help="JSON file containing one plugin Smoke Test contract")
|
|
301
|
+
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
302
|
+
parser.add_argument("--dsh-version", help="current DSH version evidence")
|
|
303
|
+
parser.add_argument("--node-version", help="current Node version evidence")
|
|
304
|
+
parser.add_argument("--os-name", help="current OS name evidence")
|
|
305
|
+
parser.add_argument("--os-version", help="current OS version evidence")
|
|
306
|
+
parser.add_argument("--secret", action="append", default=[], help="declare an available Secret name")
|
|
307
|
+
parser.add_argument("--no-process-secrets", action="store_true", help="do not use current process environment names")
|
|
308
|
+
parser.set_defaults(handler=_run_verify)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _run_verify(args: argparse.Namespace) -> int:
|
|
312
|
+
environment: dict[str, dict[str, str]] = {}
|
|
313
|
+
for section, version in (("dsh", args.dsh_version), ("node", args.node_version)):
|
|
314
|
+
if version:
|
|
315
|
+
environment[section] = {"version": version}
|
|
316
|
+
if args.os_name or args.os_version:
|
|
317
|
+
environment["os"] = {
|
|
318
|
+
key: value
|
|
319
|
+
for key, value in (("name", args.os_name), ("version", args.os_version))
|
|
320
|
+
if value
|
|
321
|
+
}
|
|
322
|
+
available_secrets = set() if args.no_process_secrets else set(os.environ)
|
|
323
|
+
available_secrets.update(args.secret)
|
|
324
|
+
try:
|
|
325
|
+
contracts = tuple(
|
|
326
|
+
contract
|
|
327
|
+
for path in args.plugin_contract
|
|
328
|
+
for contract in load_smoke_contracts(path)
|
|
329
|
+
)
|
|
330
|
+
adapter = None
|
|
331
|
+
if args.runner_config:
|
|
332
|
+
config = json.loads(Path(args.runner_config).read_text(encoding="utf-8"))
|
|
333
|
+
if not isinstance(config, dict):
|
|
334
|
+
raise ValueError("runner config JSON must contain an object")
|
|
335
|
+
adapter = SubprocessVerifyAdapter(config)
|
|
336
|
+
result = verify_profile(
|
|
337
|
+
VerifyOptions(
|
|
338
|
+
dsh_home=Path(args.dsh_home).resolve(),
|
|
339
|
+
profile_name=args.profile,
|
|
340
|
+
mode=args.mode,
|
|
341
|
+
current_environment=environment,
|
|
342
|
+
available_secret_names=frozenset(available_secrets),
|
|
343
|
+
adapter=adapter,
|
|
344
|
+
plugin_contracts=contracts,
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
except (OSError, ValueError, json.JSONDecodeError) as error:
|
|
348
|
+
payload = {"status": "FAIL", "error": {"message": str(error)}}
|
|
349
|
+
if args.json:
|
|
350
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
351
|
+
else:
|
|
352
|
+
print(f"Verify failed: {error}")
|
|
353
|
+
return 1
|
|
354
|
+
if args.json:
|
|
355
|
+
print(json.dumps(result.as_dict(), ensure_ascii=False, indent=2, sort_keys=True))
|
|
356
|
+
else:
|
|
357
|
+
print(f"Status: {result.status}")
|
|
358
|
+
for step in result.steps:
|
|
359
|
+
print(f"{step.status:9} {step.name}: {step.message}")
|
|
360
|
+
return 0 if result.status == "PASS" else 1
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
364
|
+
parser = argparse.ArgumentParser(prog="dsh-crate", description="Portable DSH environment Crate tools")
|
|
365
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
366
|
+
_inspect_parser(subparsers)
|
|
367
|
+
_export_parser(subparsers)
|
|
368
|
+
_import_parser(subparsers)
|
|
369
|
+
_delete_parser(subparsers)
|
|
370
|
+
_verify_parser(subparsers)
|
|
371
|
+
args = parser.parse_args(argv)
|
|
372
|
+
return args.handler(args)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Typed errors for the Phase 1 Pack reader and writer."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DshPackError(Exception):
|
|
5
|
+
"""Base class for expected DSH Pack failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SchemaValidationError(DshPackError):
|
|
9
|
+
"""Pack JSON does not satisfy the supported schema."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PackFormatError(DshPackError):
|
|
13
|
+
"""The ZIP container or required Pack members are invalid."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PathSafetyError(DshPackError):
|
|
17
|
+
"""A Pack member escapes or violates the allowed archive path model."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DuplicateEntryError(PackFormatError):
|
|
21
|
+
"""A ZIP contains the same logical member more than once."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class IntegrityError(DshPackError):
|
|
25
|
+
"""A declared integrity record does not match the Pack contents."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExportError(DshPackError):
|
|
29
|
+
"""A Profile cannot be exported into a valid DSH Pack."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PackImportError(DshPackError):
|
|
33
|
+
"""A Pack cannot be prepared as a new Profile."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, message: str, *, stage: str, code: str, details: dict | None = None) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.stage = stage
|
|
38
|
+
self.code = code
|
|
39
|
+
self.details = details or {}
|
|
40
|
+
|
|
41
|
+
def as_dict(self) -> dict:
|
|
42
|
+
details = dict(self.details)
|
|
43
|
+
default_expected = {
|
|
44
|
+
"planning": {"operation": "target Profile is available and safe to create or overwrite"},
|
|
45
|
+
"configuration": {"operation": "write Pack profile configuration"},
|
|
46
|
+
"embedded-install": {"operation": "install a valid embedded artifact"},
|
|
47
|
+
"reference-install": {"operation": "install the reference-only package"},
|
|
48
|
+
"network-install": {"operation": "download and install the recorded network package"},
|
|
49
|
+
"delete": {"operation": "delete the selected Profile"},
|
|
50
|
+
"version-confirmation": {"operation": "installed package matches Pack identity"},
|
|
51
|
+
"commit": {"operation": "commit the prepared Profile and metadata"},
|
|
52
|
+
}.get(self.stage, {"operation": "complete successfully"})
|
|
53
|
+
default_evidence = {
|
|
54
|
+
"stage": self.stage,
|
|
55
|
+
"code": self.code,
|
|
56
|
+
"error": details.get("error", str(self)),
|
|
57
|
+
}
|
|
58
|
+
default_impact = {
|
|
59
|
+
"planning": "Import did not start and no target Profile was changed",
|
|
60
|
+
"configuration": "the new Profile could not be fully configured",
|
|
61
|
+
"embedded-install": "an embedded package was not installed",
|
|
62
|
+
"reference-install": "a reference-only package was not installed",
|
|
63
|
+
"network-install": "a network package was not installed into the temporary Profile",
|
|
64
|
+
"delete": "the selected Profile was not deleted",
|
|
65
|
+
"version-confirmation": "the installed package identity does not satisfy the Pack",
|
|
66
|
+
"commit": "the prepared Profile was not committed as a successful Import",
|
|
67
|
+
}.get(self.stage, f"Import stopped during {self.stage}")
|
|
68
|
+
default_next_step = {
|
|
69
|
+
"planning": "Choose a new target Profile and retry Import",
|
|
70
|
+
"configuration": "check the Profile configuration target and retry Import",
|
|
71
|
+
"embedded-install": "check the embedded artifact and retry Import",
|
|
72
|
+
"reference-install": "provide the exact reference source or installer result and retry Import",
|
|
73
|
+
"network-install": "check npm/network access or provide an embedded artifact and retry Import",
|
|
74
|
+
"delete": "check the Profile path and retry the confirmed deletion",
|
|
75
|
+
"version-confirmation": "provide an artifact with the Pack resolution version and retry Import",
|
|
76
|
+
"commit": "inspect the commit evidence and retry Import",
|
|
77
|
+
}.get(self.stage, "Review the diagnostic evidence, correct the reported item, and retry Import.")
|
|
78
|
+
item = (
|
|
79
|
+
details.get("item")
|
|
80
|
+
or details.get("plugin")
|
|
81
|
+
or details.get("path")
|
|
82
|
+
or details.get("profilePath")
|
|
83
|
+
or self.code
|
|
84
|
+
)
|
|
85
|
+
observed = details.get("observed") or {"message": str(self), "code": self.code}
|
|
86
|
+
evidence = details.get("evidence") or {"stage": self.stage, "code": self.code}
|
|
87
|
+
impact = details.get("impact") or f"Import stopped during {self.stage}"
|
|
88
|
+
original_profile_status = details.get("originalProfileStatus", "unchanged")
|
|
89
|
+
failed_profile_status = details.get(
|
|
90
|
+
"failedProfileStatus",
|
|
91
|
+
details.get("temporaryProfileStatus", "cleaned"),
|
|
92
|
+
)
|
|
93
|
+
temporary_profile_status = details.get("temporaryProfileStatus", failed_profile_status)
|
|
94
|
+
return {
|
|
95
|
+
"code": self.code,
|
|
96
|
+
"stage": self.stage,
|
|
97
|
+
"item": item,
|
|
98
|
+
"expected": details.get("expected") or default_expected,
|
|
99
|
+
"observed": observed,
|
|
100
|
+
"command": details.get("command"),
|
|
101
|
+
"exitCode": details.get("exitCode"),
|
|
102
|
+
"stdout": details.get("stdout"),
|
|
103
|
+
"stderr": details.get("stderr"),
|
|
104
|
+
"evidence": evidence or default_evidence,
|
|
105
|
+
"impact": impact or default_impact,
|
|
106
|
+
"originalProfileStatus": original_profile_status,
|
|
107
|
+
"failedProfileStatus": failed_profile_status,
|
|
108
|
+
"temporaryProfileStatus": temporary_profile_status,
|
|
109
|
+
"canRetry": details.get("canRetry", True),
|
|
110
|
+
"suggestedNextStep": details.get(
|
|
111
|
+
"suggestedNextStep",
|
|
112
|
+
default_next_step,
|
|
113
|
+
),
|
|
114
|
+
"severity": details.get("severity", "BLOCKER"),
|
|
115
|
+
"canContinue": details.get("canContinue", False),
|
|
116
|
+
"suggestedChecks": details.get("suggestedChecks") or _default_checks(self.stage, self.code),
|
|
117
|
+
"message": str(self),
|
|
118
|
+
"details": details,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _default_checks(stage: str, code: str) -> list[str]:
|
|
123
|
+
"""Return a small stage-aware list of checks for a failure diagnostic."""
|
|
124
|
+
|
|
125
|
+
generic = [
|
|
126
|
+
f"confirm the exact {code} evidence above",
|
|
127
|
+
"verify the target DSH_HOME and Profile path are correct",
|
|
128
|
+
"retry after correcting the reported item, or switch to a non-destructive alternative",
|
|
129
|
+
]
|
|
130
|
+
stage_checks = {
|
|
131
|
+
"planning": [
|
|
132
|
+
"verify the target Profile name is available and safe",
|
|
133
|
+
"confirm the Pack schema and SHA-256 integrity are valid",
|
|
134
|
+
"review environment compatibility findings before Import",
|
|
135
|
+
],
|
|
136
|
+
"configuration": [
|
|
137
|
+
"inspect the Profile configuration files written by the Pack",
|
|
138
|
+
"confirm the target Profile directory is writable",
|
|
139
|
+
],
|
|
140
|
+
"embedded-install": [
|
|
141
|
+
"verify the embedded artifact SHA-256 against the Pack lock",
|
|
142
|
+
"confirm the artifact is a valid npm tarball",
|
|
143
|
+
"check for unsupported hardlink or symlink members",
|
|
144
|
+
],
|
|
145
|
+
"reference-install": [
|
|
146
|
+
"provide the exact reference package source or installer result",
|
|
147
|
+
"verify the installed reference package identity matches the Pack",
|
|
148
|
+
],
|
|
149
|
+
"network-install": [
|
|
150
|
+
"check npm registry and network reachability",
|
|
151
|
+
"verify the recorded package version exists on the registry",
|
|
152
|
+
"retry with a longer timeout, or provide an embedded artifact",
|
|
153
|
+
],
|
|
154
|
+
"delete": [
|
|
155
|
+
"confirm the exact Profile path and name",
|
|
156
|
+
"verify the Profile is not the active runtime Profile",
|
|
157
|
+
],
|
|
158
|
+
"version-confirmation": [
|
|
159
|
+
"verify the installed package version matches the Pack resolution",
|
|
160
|
+
"provide an artifact or source with the Pack resolution version",
|
|
161
|
+
],
|
|
162
|
+
"commit": [
|
|
163
|
+
"inspect the commit evidence for the Profile and metadata",
|
|
164
|
+
"verify no half-written Profile or metadata remains",
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
return stage_checks.get(stage, generic)
|