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.
Files changed (57) hide show
  1. package/README.md +302 -0
  2. package/README.zh-CN.md +304 -0
  3. package/cordis.patch.yml +11 -0
  4. package/core/dsh_pack/__init__.py +77 -0
  5. package/core/dsh_pack/__main__.py +4 -0
  6. package/core/dsh_pack/cli.py +372 -0
  7. package/core/dsh_pack/errors.py +167 -0
  8. package/core/dsh_pack/export.py +912 -0
  9. package/core/dsh_pack/importer.py +1347 -0
  10. package/core/dsh_pack/network.py +77 -0
  11. package/core/dsh_pack/pack.py +248 -0
  12. package/core/dsh_pack/preflight.py +811 -0
  13. package/core/dsh_pack/user_cli.py +462 -0
  14. package/core/dsh_pack/validation.py +272 -0
  15. package/core/dsh_pack/verify.py +1336 -0
  16. package/lib/client.js +1623 -0
  17. package/lib/index.d.ts +30 -0
  18. package/lib/index.js +1275 -0
  19. package/package.json +73 -0
  20. package/restart-helper.mjs +121 -0
  21. package/runtime/python/LICENSE.txt +676 -0
  22. package/runtime/python/__install__.json +82 -0
  23. package/runtime/python/_asyncio.pyd +0 -0
  24. package/runtime/python/_bz2.pyd +0 -0
  25. package/runtime/python/_ctypes.pyd +0 -0
  26. package/runtime/python/_decimal.pyd +0 -0
  27. package/runtime/python/_elementtree.pyd +0 -0
  28. package/runtime/python/_hashlib.pyd +0 -0
  29. package/runtime/python/_lzma.pyd +0 -0
  30. package/runtime/python/_multiprocessing.pyd +0 -0
  31. package/runtime/python/_overlapped.pyd +0 -0
  32. package/runtime/python/_queue.pyd +0 -0
  33. package/runtime/python/_remote_debugging.pyd +0 -0
  34. package/runtime/python/_socket.pyd +0 -0
  35. package/runtime/python/_sqlite3.pyd +0 -0
  36. package/runtime/python/_ssl.pyd +0 -0
  37. package/runtime/python/_uuid.pyd +0 -0
  38. package/runtime/python/_wmi.pyd +0 -0
  39. package/runtime/python/_zoneinfo.pyd +0 -0
  40. package/runtime/python/_zstd.pyd +0 -0
  41. package/runtime/python/libcrypto-3.dll +0 -0
  42. package/runtime/python/libffi-8.dll +0 -0
  43. package/runtime/python/libssl-3.dll +0 -0
  44. package/runtime/python/pyexpat.pyd +0 -0
  45. package/runtime/python/python.cat +0 -0
  46. package/runtime/python/python.exe +0 -0
  47. package/runtime/python/python3.dll +0 -0
  48. package/runtime/python/python314._pth +6 -0
  49. package/runtime/python/python314.dll +0 -0
  50. package/runtime/python/python314.zip +0 -0
  51. package/runtime/python/pythonw.exe +0 -0
  52. package/runtime/python/select.pyd +0 -0
  53. package/runtime/python/sqlite3.dll +0 -0
  54. package/runtime/python/unicodedata.pyd +0 -0
  55. package/runtime/python/vcruntime140.dll +0 -0
  56. package/runtime/python/vcruntime140_1.dll +0 -0
  57. 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)