@softspark/ai-toolkit 4.32.4 → 4.33.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/CHANGELOG.md +25 -0
- package/README.md +13 -16
- package/app/.claude-plugin/plugin.json +1 -1
- package/kb/procedures/sop-release-verification.md +6 -4
- package/kb/reference/dsh-compatibility.md +85 -13
- package/llms-full.txt +91 -17
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/ecosystem_tools.json +5 -5
- package/scripts/install_steps/dsh.py +506 -53
|
@@ -14,8 +14,8 @@ import json
|
|
|
14
14
|
import os
|
|
15
15
|
import re
|
|
16
16
|
import secrets
|
|
17
|
-
import signal
|
|
18
17
|
import shutil
|
|
18
|
+
import signal
|
|
19
19
|
import stat
|
|
20
20
|
import subprocess
|
|
21
21
|
import sys
|
|
@@ -44,16 +44,26 @@ from install_steps.install_state import (
|
|
|
44
44
|
secure_dsh_state_mutation_supported,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
SUPPORTED_DSH_VERSION = "0.1.
|
|
47
|
+
SUPPORTED_DSH_VERSION = "0.1.2-rc.1"
|
|
48
48
|
MINIMUM_PNPM_VERSION = (11, 7, 0)
|
|
49
49
|
MAXIMUM_PNPM_VERSION_EXCLUSIVE = (12, 0, 0)
|
|
50
50
|
SUPPORTED_PNPM_RANGE = ">=11.7.0,<12.0.0"
|
|
51
51
|
DEFAULT_PROFILE = "web"
|
|
52
52
|
PRESET_NAME = "softspark-orchestrator"
|
|
53
53
|
PACKAGES = {
|
|
54
|
-
"@softspark/dsh-codex": "1.
|
|
55
|
-
"@softspark/dsh-orchestrator": "
|
|
54
|
+
"@softspark/dsh-codex": "1.5.0",
|
|
55
|
+
"@softspark/dsh-orchestrator": "2.0.0",
|
|
56
56
|
}
|
|
57
|
+
CLAUDE_SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk"
|
|
58
|
+
CLAUDE_SDK_VERSION = "0.3.263"
|
|
59
|
+
CLAUDE_PROVIDER = "@deepseek-ai/dsh-subagent-claude-code"
|
|
60
|
+
CLAUDE_SDK_OVERRIDE = f"{CLAUDE_PROVIDER}@0.1.2-rc.1>{CLAUDE_SDK_PACKAGE}"
|
|
61
|
+
PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml"
|
|
62
|
+
DEFAULT_PROFILE_PATCH = (
|
|
63
|
+
b"# Your patch layer for this dsh profile, applied after every bundle layer:\n"
|
|
64
|
+
b"# a top-level YAML array of loader patch entries (id-targeted config\n"
|
|
65
|
+
b"# overrides, disables, and insert lists; `!!js` expressions allowed).\n[]\n"
|
|
66
|
+
)
|
|
57
67
|
MANAGED_PACKAGE_NAMES = tuple(PACKAGES)
|
|
58
68
|
PROBE_TIMEOUT_SECONDS = 5
|
|
59
69
|
PACKAGE_MUTATION_TIMEOUT_SECONDS = 300
|
|
@@ -378,9 +388,7 @@ def _acquire_dsh_home_directory_lock(home: _PinnedDshHome) -> None:
|
|
|
378
388
|
try:
|
|
379
389
|
fcntl.flock(home.root_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
380
390
|
except BlockingIOError as error:
|
|
381
|
-
raise DshLifecycleError(
|
|
382
|
-
"DSH home directory lifecycle lock is busy"
|
|
383
|
-
) from error
|
|
391
|
+
raise DshLifecycleError("DSH home directory lifecycle lock is busy") from error
|
|
384
392
|
except OSError as error:
|
|
385
393
|
raise DshLifecycleError(
|
|
386
394
|
"unable to acquire DSH home directory lifecycle lock"
|
|
@@ -414,10 +422,14 @@ def _cleanup_failed_lifecycle_lock(
|
|
|
414
422
|
dir_fd=home.root_descriptor,
|
|
415
423
|
follow_symlinks=False,
|
|
416
424
|
)
|
|
417
|
-
if
|
|
418
|
-
named.
|
|
419
|
-
|
|
420
|
-
|
|
425
|
+
if (
|
|
426
|
+
not stat.S_ISREG(named.st_mode)
|
|
427
|
+
or (
|
|
428
|
+
named.st_dev,
|
|
429
|
+
named.st_ino,
|
|
430
|
+
)
|
|
431
|
+
!= identity
|
|
432
|
+
):
|
|
421
433
|
return path
|
|
422
434
|
_secure_rename_noreplace_at(
|
|
423
435
|
home.root_descriptor,
|
|
@@ -429,10 +441,14 @@ def _cleanup_failed_lifecycle_lock(
|
|
|
429
441
|
dir_fd=home.root_descriptor,
|
|
430
442
|
follow_symlinks=False,
|
|
431
443
|
)
|
|
432
|
-
if
|
|
433
|
-
moved.
|
|
434
|
-
|
|
435
|
-
|
|
444
|
+
if (
|
|
445
|
+
not stat.S_ISREG(moved.st_mode)
|
|
446
|
+
or (
|
|
447
|
+
moved.st_dev,
|
|
448
|
+
moved.st_ino,
|
|
449
|
+
)
|
|
450
|
+
!= identity
|
|
451
|
+
):
|
|
436
452
|
return recovery
|
|
437
453
|
os.unlink(recovery.name, dir_fd=home.root_descriptor)
|
|
438
454
|
return None
|
|
@@ -459,11 +475,7 @@ def _lifecycle_lock_recovery_artifacts(dsh_home: Path) -> tuple[Path, ...]:
|
|
|
459
475
|
pass
|
|
460
476
|
return tuple(
|
|
461
477
|
sorted(
|
|
462
|
-
(
|
|
463
|
-
path
|
|
464
|
-
for path in candidates
|
|
465
|
-
if path.exists() or path.is_symlink()
|
|
466
|
-
),
|
|
478
|
+
(path for path in candidates if path.exists() or path.is_symlink()),
|
|
467
479
|
key=str,
|
|
468
480
|
)
|
|
469
481
|
)
|
|
@@ -522,10 +534,14 @@ def _read_lifecycle_lock_fields_at(
|
|
|
522
534
|
descriptor = os.open(path.name, flags, dir_fd=home.root_descriptor)
|
|
523
535
|
try:
|
|
524
536
|
metadata = os.fstat(descriptor)
|
|
525
|
-
if
|
|
526
|
-
metadata.
|
|
527
|
-
|
|
528
|
-
|
|
537
|
+
if (
|
|
538
|
+
not stat.S_ISREG(metadata.st_mode)
|
|
539
|
+
or (
|
|
540
|
+
metadata.st_dev,
|
|
541
|
+
metadata.st_ino,
|
|
542
|
+
)
|
|
543
|
+
!= identity
|
|
544
|
+
):
|
|
529
545
|
raise DshLifecycleError(
|
|
530
546
|
f"DSH lifecycle lock identity changed while reading: {path}"
|
|
531
547
|
)
|
|
@@ -1025,12 +1041,21 @@ class _ProfileTransaction:
|
|
|
1025
1041
|
latest_manifest: _OwnedEntry | None = None
|
|
1026
1042
|
latest_package_identity: _PackageMutationIdentity | None = None
|
|
1027
1043
|
rollback_blocked: bool = False
|
|
1044
|
+
workspace: _PathPrestate | None = None
|
|
1045
|
+
latest_workspace: _OwnedEntry | None = None
|
|
1046
|
+
profile_patch: _PathPrestate | None = None
|
|
1047
|
+
latest_profile_patch: _OwnedEntry | None = None
|
|
1048
|
+
pnpm_settings: dict[str, object] | None = None
|
|
1049
|
+
initialized_manifest: _PathPrestate | None = None
|
|
1028
1050
|
|
|
1029
1051
|
|
|
1030
1052
|
@dataclass(frozen=True)
|
|
1031
1053
|
class _PackageMutationIdentity:
|
|
1032
1054
|
manifest: _PathPrestate | None
|
|
1033
1055
|
package_trees: dict[Path, dict[Path, _PathPrestate] | None]
|
|
1056
|
+
workspace: _PathPrestate | None = None
|
|
1057
|
+
workspace_entry: _OwnedEntry | None = None
|
|
1058
|
+
manifest_entry: _OwnedEntry | None = None
|
|
1034
1059
|
|
|
1035
1060
|
|
|
1036
1061
|
@dataclass(frozen=True)
|
|
@@ -1101,14 +1126,32 @@ def _capture_package_mutation_identity(
|
|
|
1101
1126
|
)
|
|
1102
1127
|
for package in PACKAGES
|
|
1103
1128
|
}
|
|
1104
|
-
|
|
1129
|
+
workspace_path = profile_root / PNPM_WORKSPACE_FILENAME
|
|
1130
|
+
workspace = _optional_profile_file(workspace_path)
|
|
1131
|
+
workspace_entry = _capture_entry(workspace_path) if workspace is not None else None
|
|
1132
|
+
return _PackageMutationIdentity(
|
|
1133
|
+
manifest,
|
|
1134
|
+
package_trees,
|
|
1135
|
+
workspace,
|
|
1136
|
+
workspace_entry,
|
|
1137
|
+
_capture_entry(manifest_path) if manifest is not None else None,
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _optional_profile_file(path: Path) -> _PathPrestate | None:
|
|
1142
|
+
if not path.exists() and not path.is_symlink():
|
|
1143
|
+
return None
|
|
1144
|
+
snapshot = _path_prestate(path)
|
|
1145
|
+
if snapshot.kind != "file":
|
|
1146
|
+
raise DshLifecycleError(f"unsafe DSH profile configuration: {path}")
|
|
1147
|
+
return snapshot
|
|
1105
1148
|
|
|
1106
1149
|
|
|
1107
1150
|
def _capture_profile_transaction(dsh_home: Path, profile: str) -> _ProfileTransaction:
|
|
1108
1151
|
profile_root = dsh_home / "profiles" / profile
|
|
1109
1152
|
node_modules = profile_root / "node_modules"
|
|
1110
1153
|
namespace = node_modules / "@softspark"
|
|
1111
|
-
base_paths = (profile_root, node_modules, namespace)
|
|
1154
|
+
base_paths = (profile_root.parent, profile_root, node_modules, namespace)
|
|
1112
1155
|
base_existed = {
|
|
1113
1156
|
path: path.exists() and path.is_dir() and not path.is_symlink()
|
|
1114
1157
|
for path in base_paths
|
|
@@ -1133,6 +1176,13 @@ def _capture_profile_transaction(dsh_home: Path, profile: str) -> _ProfileTransa
|
|
|
1133
1176
|
manifest,
|
|
1134
1177
|
package_trees,
|
|
1135
1178
|
{},
|
|
1179
|
+
workspace=_optional_profile_file(profile_root / PNPM_WORKSPACE_FILENAME),
|
|
1180
|
+
profile_patch=(
|
|
1181
|
+
_path_prestate(profile_root / "cordis.patch.yml")
|
|
1182
|
+
if (profile_root / "cordis.patch.yml").exists()
|
|
1183
|
+
or (profile_root / "cordis.patch.yml").is_symlink()
|
|
1184
|
+
else None
|
|
1185
|
+
),
|
|
1136
1186
|
)
|
|
1137
1187
|
transaction.latest_package_identity = _capture_package_mutation_identity(
|
|
1138
1188
|
profile_root
|
|
@@ -1158,6 +1208,24 @@ def _observe_profile_transaction(transaction: _ProfileTransaction) -> None:
|
|
|
1158
1208
|
transaction.latest_manifest = None
|
|
1159
1209
|
else:
|
|
1160
1210
|
transaction.latest_manifest = None
|
|
1211
|
+
workspace_path = transaction.profile_root / PNPM_WORKSPACE_FILENAME
|
|
1212
|
+
transaction.latest_workspace = (
|
|
1213
|
+
_capture_entry(workspace_path)
|
|
1214
|
+
if workspace_path.exists() or workspace_path.is_symlink()
|
|
1215
|
+
else None
|
|
1216
|
+
)
|
|
1217
|
+
patch_path = transaction.profile_root / "cordis.patch.yml"
|
|
1218
|
+
if (
|
|
1219
|
+
transaction.profile_patch is None
|
|
1220
|
+
and patch_path.exists()
|
|
1221
|
+
and transaction.latest_profile_patch is None
|
|
1222
|
+
):
|
|
1223
|
+
if _read_regular_bytes(patch_path) not in {DEFAULT_PROFILE_PATCH, b"[]\n"}:
|
|
1224
|
+
transaction.rollback_blocked = True
|
|
1225
|
+
raise DshLifecycleError(
|
|
1226
|
+
"unexpected profile patch appeared during initialization; preserved"
|
|
1227
|
+
)
|
|
1228
|
+
transaction.latest_profile_patch = _capture_entry(patch_path)
|
|
1161
1229
|
|
|
1162
1230
|
|
|
1163
1231
|
def _run_profile_command(
|
|
@@ -1232,6 +1300,8 @@ def _run_profile_command(
|
|
|
1232
1300
|
"package recovery target drifted during successful external mutation"
|
|
1233
1301
|
)
|
|
1234
1302
|
transaction.latest_package_identity = after
|
|
1303
|
+
if transaction.pnpm_settings is not None and argv[4] in {"add", "remove"}:
|
|
1304
|
+
_check_pnpm_settings_after_mutation(argv[0], dsh_home, argv[3], transaction)
|
|
1235
1305
|
return result
|
|
1236
1306
|
|
|
1237
1307
|
|
|
@@ -1409,7 +1479,11 @@ def _created_file_matches(
|
|
|
1409
1479
|
)
|
|
1410
1480
|
|
|
1411
1481
|
|
|
1412
|
-
def _create_snapshot_file(
|
|
1482
|
+
def _create_snapshot_file(
|
|
1483
|
+
snapshot: _PathPrestate,
|
|
1484
|
+
parent: int,
|
|
1485
|
+
created_entries: list[_OwnedEntry] | None = None,
|
|
1486
|
+
) -> bool:
|
|
1413
1487
|
_assert_active_dsh_home_binding()
|
|
1414
1488
|
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
|
|
1415
1489
|
descriptor = os.open(snapshot.path.name, flags, snapshot.mode, dir_fd=parent)
|
|
@@ -1428,6 +1502,19 @@ def _create_snapshot_file(snapshot: _PathPrestate, parent: int) -> bool:
|
|
|
1428
1502
|
created=created,
|
|
1429
1503
|
)
|
|
1430
1504
|
_assert_active_dsh_home_binding()
|
|
1505
|
+
if created_matches and created_entries is not None:
|
|
1506
|
+
parent_metadata = os.fstat(parent)
|
|
1507
|
+
created_entries.append(
|
|
1508
|
+
_OwnedEntry(
|
|
1509
|
+
snapshot.path,
|
|
1510
|
+
created.st_dev,
|
|
1511
|
+
created.st_ino,
|
|
1512
|
+
"file",
|
|
1513
|
+
hashlib.sha256(snapshot.content or b"").hexdigest(),
|
|
1514
|
+
parent_device=parent_metadata.st_dev,
|
|
1515
|
+
parent_inode=parent_metadata.st_ino,
|
|
1516
|
+
)
|
|
1517
|
+
)
|
|
1431
1518
|
return created_matches
|
|
1432
1519
|
finally:
|
|
1433
1520
|
os.close(descriptor)
|
|
@@ -1697,16 +1784,30 @@ def _manifest_has_only_empty_managed_dependencies(path: Path) -> bool:
|
|
|
1697
1784
|
|
|
1698
1785
|
def _restore_manifest_prestate(transaction: _ProfileTransaction) -> list[Path]:
|
|
1699
1786
|
path = transaction.profile_root / "package.json"
|
|
1700
|
-
|
|
1701
|
-
|
|
1787
|
+
removable = _manifest_has_only_empty_managed_dependencies(path)
|
|
1788
|
+
if not removable and transaction.initialized_manifest is not None:
|
|
1789
|
+
try:
|
|
1790
|
+
removable = json.loads(_read_regular_bytes(path)) == json.loads(
|
|
1791
|
+
transaction.initialized_manifest.content or b"{}"
|
|
1792
|
+
)
|
|
1793
|
+
except (DshLifecycleError, ValueError):
|
|
1794
|
+
removable = False
|
|
1795
|
+
return _restore_profile_file_prestate(
|
|
1796
|
+
path, transaction.manifest, transaction.latest_manifest, removable=removable
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
def _restore_profile_file_prestate(
|
|
1801
|
+
path: Path,
|
|
1802
|
+
expected: _PathPrestate | None,
|
|
1803
|
+
current_identity: _OwnedEntry | None,
|
|
1804
|
+
*,
|
|
1805
|
+
removable: bool = False,
|
|
1806
|
+
) -> list[Path]:
|
|
1702
1807
|
if expected is None:
|
|
1703
1808
|
if not path.exists() and not path.is_symlink():
|
|
1704
1809
|
return []
|
|
1705
|
-
if
|
|
1706
|
-
current_identity is None
|
|
1707
|
-
or current_identity.kind != "file"
|
|
1708
|
-
or not _manifest_has_only_empty_managed_dependencies(path)
|
|
1709
|
-
):
|
|
1810
|
+
if current_identity is None or current_identity.kind != "file" or not removable:
|
|
1710
1811
|
return [path]
|
|
1711
1812
|
try:
|
|
1712
1813
|
_assert_entry_unchanged(current_identity, "profile package manifest")
|
|
@@ -1827,6 +1928,26 @@ def _restore_profile_prestate(transaction: _ProfileTransaction) -> list[Path]:
|
|
|
1827
1928
|
for root, snapshots in transaction.package_trees.items():
|
|
1828
1929
|
residuals.extend(_restore_tree_prestate(root, snapshots))
|
|
1829
1930
|
residuals.extend(_restore_manifest_prestate(transaction))
|
|
1931
|
+
residuals.extend(
|
|
1932
|
+
_restore_profile_file_prestate(
|
|
1933
|
+
transaction.profile_root / PNPM_WORKSPACE_FILENAME,
|
|
1934
|
+
transaction.workspace,
|
|
1935
|
+
transaction.latest_workspace,
|
|
1936
|
+
removable=True,
|
|
1937
|
+
)
|
|
1938
|
+
)
|
|
1939
|
+
if (
|
|
1940
|
+
transaction.profile_patch is None
|
|
1941
|
+
and transaction.latest_profile_patch is not None
|
|
1942
|
+
):
|
|
1943
|
+
residuals.extend(
|
|
1944
|
+
_restore_profile_file_prestate(
|
|
1945
|
+
transaction.profile_root / "cordis.patch.yml",
|
|
1946
|
+
None,
|
|
1947
|
+
transaction.latest_profile_patch,
|
|
1948
|
+
removable=True,
|
|
1949
|
+
)
|
|
1950
|
+
)
|
|
1830
1951
|
for path in reversed(tuple(transaction.base_existed)):
|
|
1831
1952
|
if transaction.base_existed[path]:
|
|
1832
1953
|
continue
|
|
@@ -2442,7 +2563,9 @@ def _copy_tree_exclusive(
|
|
|
2442
2563
|
raise DshLifecycleError(
|
|
2443
2564
|
f"managed preset contains an unsupported entry: {relative}"
|
|
2444
2565
|
)
|
|
2445
|
-
source_content, source_metadata = _stable_regular_file_bytes(
|
|
2566
|
+
source_content, source_metadata = _stable_regular_file_bytes(
|
|
2567
|
+
source_path
|
|
2568
|
+
)
|
|
2446
2569
|
expected_digest = hashlib.sha256(source_content).hexdigest()
|
|
2447
2570
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW
|
|
2448
2571
|
try:
|
|
@@ -2710,7 +2833,7 @@ def _remove_exact_managed_tree(
|
|
|
2710
2833
|
|
|
2711
2834
|
|
|
2712
2835
|
def _validate_profile(profile: str) -> str:
|
|
2713
|
-
if profile in {".", ".."} or not PROFILE_PATTERN.fullmatch(profile):
|
|
2836
|
+
if profile in {".", "..", "node_modules"} or not PROFILE_PATTERN.fullmatch(profile):
|
|
2714
2837
|
raise DshLifecycleError(
|
|
2715
2838
|
"invalid DSH profile id; use 1-64 lowercase letters, digits, '.', '_' or '-'"
|
|
2716
2839
|
)
|
|
@@ -2786,11 +2909,12 @@ def _mutation_environment(
|
|
|
2786
2909
|
) -> dict[str, str]:
|
|
2787
2910
|
environment = _minimal_environment(dsh_home)
|
|
2788
2911
|
verified_directory = str(prerequisites.pnpm.command_path.parent)
|
|
2789
|
-
environment["PATH"] =
|
|
2790
|
-
verified_directory + os.pathsep + prerequisites.execution_path
|
|
2791
|
-
)
|
|
2912
|
+
environment["PATH"] = verified_directory + os.pathsep + prerequisites.execution_path
|
|
2792
2913
|
selected = shutil.which("pnpm", path=environment["PATH"])
|
|
2793
|
-
if
|
|
2914
|
+
if (
|
|
2915
|
+
selected is None
|
|
2916
|
+
or _absolute_command_path(selected) != prerequisites.pnpm.command_path
|
|
2917
|
+
):
|
|
2794
2918
|
raise DshLifecycleError("pnpm prerequisite PATH binding changed")
|
|
2795
2919
|
return environment
|
|
2796
2920
|
|
|
@@ -2991,9 +3115,7 @@ def _run(argv: list[str], *, dsh_home: Path) -> subprocess.CompletedProcess[str]
|
|
|
2991
3115
|
timeout=PACKAGE_MUTATION_TIMEOUT_SECONDS
|
|
2992
3116
|
)
|
|
2993
3117
|
if _process_group_exists(process.pid):
|
|
2994
|
-
raise DshLifecycleError(
|
|
2995
|
-
"DSH command left a descendant process running"
|
|
2996
|
-
)
|
|
3118
|
+
raise DshLifecycleError("DSH command left a descendant process running")
|
|
2997
3119
|
_assert_active_dsh_home_binding()
|
|
2998
3120
|
_active_prerequisites()
|
|
2999
3121
|
if process.returncode in {-2, 130}:
|
|
@@ -3041,7 +3163,9 @@ def _capture_executable_prerequisite(
|
|
|
3041
3163
|
try:
|
|
3042
3164
|
command_metadata = command_path.lstat()
|
|
3043
3165
|
command_link_target = (
|
|
3044
|
-
os.readlink(command_path)
|
|
3166
|
+
os.readlink(command_path)
|
|
3167
|
+
if stat.S_ISLNK(command_metadata.st_mode)
|
|
3168
|
+
else None
|
|
3045
3169
|
)
|
|
3046
3170
|
resolved_path = command_path.resolve(strict=True)
|
|
3047
3171
|
resolved_metadata = resolved_path.stat(follow_symlinks=False)
|
|
@@ -3308,9 +3432,9 @@ def _tree_inventory(root: Path) -> dict[str, object]:
|
|
|
3308
3432
|
try:
|
|
3309
3433
|
children = sorted(
|
|
3310
3434
|
directory.iterdir(),
|
|
3311
|
-
key=lambda item:
|
|
3312
|
-
|
|
3313
|
-
|
|
3435
|
+
key=lambda item: (
|
|
3436
|
+
item.relative_to(root).as_posix().encode("utf-8", errors="strict")
|
|
3437
|
+
),
|
|
3314
3438
|
reverse=True,
|
|
3315
3439
|
)
|
|
3316
3440
|
except (OSError, UnicodeError) as error:
|
|
@@ -3507,6 +3631,309 @@ def _profile_package_versions(dsh_home: Path, profile: str) -> dict[str, str]:
|
|
|
3507
3631
|
return versions
|
|
3508
3632
|
|
|
3509
3633
|
|
|
3634
|
+
def _parse_pnpm_settings(result: subprocess.CompletedProcess[str]) -> dict[str, object]:
|
|
3635
|
+
try:
|
|
3636
|
+
if len(result.stdout.encode("utf-8")) > 256 * 1024:
|
|
3637
|
+
raise ValueError("oversized configuration")
|
|
3638
|
+
value = json.loads(result.stdout)
|
|
3639
|
+
except (ValueError, UnicodeError) as error:
|
|
3640
|
+
raise DshLifecycleError(
|
|
3641
|
+
"pnpm returned invalid profile configuration"
|
|
3642
|
+
) from error
|
|
3643
|
+
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
|
3644
|
+
raise DshLifecycleError("pnpm returned invalid profile configuration")
|
|
3645
|
+
return value
|
|
3646
|
+
|
|
3647
|
+
|
|
3648
|
+
def _read_pnpm_settings(
|
|
3649
|
+
executable: str, dsh_home: Path, profile: str, transaction: _ProfileTransaction
|
|
3650
|
+
) -> dict[str, object]:
|
|
3651
|
+
before = transaction.latest_package_identity
|
|
3652
|
+
result = _run_profile_command(
|
|
3653
|
+
[
|
|
3654
|
+
executable,
|
|
3655
|
+
"plugin",
|
|
3656
|
+
"--profile",
|
|
3657
|
+
profile,
|
|
3658
|
+
"config",
|
|
3659
|
+
"list",
|
|
3660
|
+
"--json",
|
|
3661
|
+
"--location",
|
|
3662
|
+
"project",
|
|
3663
|
+
],
|
|
3664
|
+
dsh_home=dsh_home,
|
|
3665
|
+
transaction=transaction,
|
|
3666
|
+
)
|
|
3667
|
+
if before != transaction.latest_package_identity:
|
|
3668
|
+
transaction.rollback_blocked = True
|
|
3669
|
+
raise DshLifecycleError(
|
|
3670
|
+
"profile identity changed during configuration inspection; preserved"
|
|
3671
|
+
)
|
|
3672
|
+
return _parse_pnpm_settings(result)
|
|
3673
|
+
|
|
3674
|
+
|
|
3675
|
+
def _initial_profile_manifest(profile: str) -> bytes:
|
|
3676
|
+
app = {
|
|
3677
|
+
"web": "dsh-web-app",
|
|
3678
|
+
"acp": "dsh-acp-app",
|
|
3679
|
+
"headless": "dsh-headless",
|
|
3680
|
+
"sdk": "dsh-sdk-app",
|
|
3681
|
+
}.get(profile)
|
|
3682
|
+
bundles = ["@deepseek-ai/dsh-base"]
|
|
3683
|
+
if app is not None:
|
|
3684
|
+
bundles.append(f"@deepseek-ai/{app}")
|
|
3685
|
+
if profile == "sdk-minimal":
|
|
3686
|
+
bundles = ["@deepseek-ai/dsh-sdk-minimal"]
|
|
3687
|
+
reload = (
|
|
3688
|
+
"startup" if profile in {"acp", "headless", "sdk", "sdk-minimal"} else "live"
|
|
3689
|
+
)
|
|
3690
|
+
return (
|
|
3691
|
+
json.dumps(
|
|
3692
|
+
{
|
|
3693
|
+
"name": f"dsh-profile-{profile}",
|
|
3694
|
+
"private": True,
|
|
3695
|
+
"dependencies": {},
|
|
3696
|
+
"dsh": {"profile": {"bundles": bundles, "patchReload": reload}},
|
|
3697
|
+
},
|
|
3698
|
+
indent=2,
|
|
3699
|
+
)
|
|
3700
|
+
+ "\n"
|
|
3701
|
+
).encode()
|
|
3702
|
+
|
|
3703
|
+
|
|
3704
|
+
def _preclaim_profile_configuration(
|
|
3705
|
+
transaction: _ProfileTransaction, profile: str
|
|
3706
|
+
) -> None:
|
|
3707
|
+
"""Own initialization inodes before invoking a command that may initialize a profile."""
|
|
3708
|
+
existing = [
|
|
3709
|
+
entry
|
|
3710
|
+
for entry in (transaction.latest_manifest, transaction.latest_workspace)
|
|
3711
|
+
if entry is not None
|
|
3712
|
+
]
|
|
3713
|
+
try:
|
|
3714
|
+
for path in (transaction.profile_root.parent, transaction.profile_root):
|
|
3715
|
+
if not transaction.base_existed[path]:
|
|
3716
|
+
owned: list[_OwnedEntry] = []
|
|
3717
|
+
_claim_directory(path, owned)
|
|
3718
|
+
transaction.created_base[path] = owned[0]
|
|
3719
|
+
files = (
|
|
3720
|
+
("package.json", transaction.manifest, _initial_profile_manifest(profile)),
|
|
3721
|
+
(
|
|
3722
|
+
PNPM_WORKSPACE_FILENAME,
|
|
3723
|
+
transaction.workspace,
|
|
3724
|
+
b'{"packages":["."],"nodeLinker":"hoisted","autoInstallPeers":false}\n',
|
|
3725
|
+
),
|
|
3726
|
+
("cordis.patch.yml", transaction.profile_patch, DEFAULT_PROFILE_PATCH),
|
|
3727
|
+
)
|
|
3728
|
+
parent = _open_active_dsh_directory(transaction.profile_root)
|
|
3729
|
+
try:
|
|
3730
|
+
for name, prestate, content in files:
|
|
3731
|
+
if prestate is not None:
|
|
3732
|
+
continue
|
|
3733
|
+
snapshot = _PathPrestate(
|
|
3734
|
+
transaction.profile_root / name, "file", 0o600, content
|
|
3735
|
+
)
|
|
3736
|
+
claimed: list[_OwnedEntry] = []
|
|
3737
|
+
if not _create_snapshot_file(snapshot, parent, claimed):
|
|
3738
|
+
raise DshLifecycleError(
|
|
3739
|
+
"profile initialization file identity changed"
|
|
3740
|
+
)
|
|
3741
|
+
existing.append(claimed[0])
|
|
3742
|
+
if name == "package.json":
|
|
3743
|
+
transaction.initialized_manifest = snapshot
|
|
3744
|
+
elif name == "cordis.patch.yml":
|
|
3745
|
+
transaction.latest_profile_patch = claimed[0]
|
|
3746
|
+
finally:
|
|
3747
|
+
os.close(parent)
|
|
3748
|
+
for entry in existing:
|
|
3749
|
+
_assert_entry_unchanged(entry, "profile initialization")
|
|
3750
|
+
transaction.latest_package_identity = _capture_package_mutation_identity(
|
|
3751
|
+
transaction.profile_root
|
|
3752
|
+
)
|
|
3753
|
+
_observe_profile_transaction(transaction)
|
|
3754
|
+
except (DshLifecycleError, OSError, KeyboardInterrupt):
|
|
3755
|
+
transaction.rollback_blocked = True
|
|
3756
|
+
raise DshLifecycleError(
|
|
3757
|
+
"profile initialization ownership changed; preserved for recovery"
|
|
3758
|
+
) from None
|
|
3759
|
+
|
|
3760
|
+
|
|
3761
|
+
def _read_only_pnpm_settings(dsh_home: Path, profile: str) -> dict[str, object]:
|
|
3762
|
+
root = dsh_home / "profiles" / profile
|
|
3763
|
+
if not root.exists():
|
|
3764
|
+
return {}
|
|
3765
|
+
_optional_profile_file(root / PNPM_WORKSPACE_FILENAME)
|
|
3766
|
+
executable, _version = _find_supported_pnpm(dsh_home)
|
|
3767
|
+
result = _run_version_probe(
|
|
3768
|
+
[
|
|
3769
|
+
executable,
|
|
3770
|
+
"--dir",
|
|
3771
|
+
str(root),
|
|
3772
|
+
"config",
|
|
3773
|
+
"list",
|
|
3774
|
+
"--json",
|
|
3775
|
+
"--location",
|
|
3776
|
+
"project",
|
|
3777
|
+
],
|
|
3778
|
+
dsh_home=dsh_home,
|
|
3779
|
+
label="pnpm configuration",
|
|
3780
|
+
remediation=f"inspect profile configuration at {str(root)!r}",
|
|
3781
|
+
)
|
|
3782
|
+
return _parse_pnpm_settings(result)
|
|
3783
|
+
|
|
3784
|
+
|
|
3785
|
+
def _compatible_sdk_overrides(settings: dict[str, object]) -> dict[str, str]:
|
|
3786
|
+
linker = settings.get("nodeLinker")
|
|
3787
|
+
if linker is not None and linker != "hoisted":
|
|
3788
|
+
raise DshLifecycleError(
|
|
3789
|
+
"DSH profile requires nodeLinker: hoisted; existing setting was preserved"
|
|
3790
|
+
)
|
|
3791
|
+
overrides = settings.get("overrides", {})
|
|
3792
|
+
if not isinstance(overrides, dict) or not all(
|
|
3793
|
+
isinstance(key, str) and isinstance(value, str)
|
|
3794
|
+
for key, value in overrides.items()
|
|
3795
|
+
):
|
|
3796
|
+
raise DshLifecycleError(
|
|
3797
|
+
"DSH profile overrides must be an object of package selectors and versions"
|
|
3798
|
+
)
|
|
3799
|
+
for key, value in overrides.items():
|
|
3800
|
+
if CLAUDE_SDK_PACKAGE in key and value != CLAUDE_SDK_VERSION:
|
|
3801
|
+
raise DshLifecycleError(
|
|
3802
|
+
"conflicting Claude SDK override; existing profile settings were preserved"
|
|
3803
|
+
)
|
|
3804
|
+
return {**overrides, CLAUDE_SDK_OVERRIDE: CLAUDE_SDK_VERSION}
|
|
3805
|
+
|
|
3806
|
+
|
|
3807
|
+
def _configure_claude_sdk(
|
|
3808
|
+
executable: str, dsh_home: Path, profile: str, transaction: _ProfileTransaction
|
|
3809
|
+
) -> None:
|
|
3810
|
+
if transaction.base_existed[transaction.profile_root]:
|
|
3811
|
+
_compatible_sdk_overrides(_read_only_pnpm_settings(dsh_home, profile))
|
|
3812
|
+
_preclaim_profile_configuration(transaction, profile)
|
|
3813
|
+
settings = _read_pnpm_settings(executable, dsh_home, profile, transaction)
|
|
3814
|
+
overrides = _compatible_sdk_overrides(settings)
|
|
3815
|
+
changes: dict[str, object] = {"overrides": overrides}
|
|
3816
|
+
if settings.get("nodeLinker") is None:
|
|
3817
|
+
changes["nodeLinker"] = "hoisted"
|
|
3818
|
+
for key, value in changes.items():
|
|
3819
|
+
if settings.get(key) == value:
|
|
3820
|
+
continue
|
|
3821
|
+
_run_profile_command(
|
|
3822
|
+
[
|
|
3823
|
+
executable,
|
|
3824
|
+
"plugin",
|
|
3825
|
+
"--profile",
|
|
3826
|
+
profile,
|
|
3827
|
+
"config",
|
|
3828
|
+
"set",
|
|
3829
|
+
"--json",
|
|
3830
|
+
"--location",
|
|
3831
|
+
"project",
|
|
3832
|
+
key,
|
|
3833
|
+
json.dumps(value),
|
|
3834
|
+
],
|
|
3835
|
+
dsh_home=dsh_home,
|
|
3836
|
+
transaction=transaction,
|
|
3837
|
+
)
|
|
3838
|
+
expected = {**settings, key: value}
|
|
3839
|
+
settings = _read_pnpm_settings(executable, dsh_home, profile, transaction)
|
|
3840
|
+
if settings != expected:
|
|
3841
|
+
transaction.rollback_blocked = True
|
|
3842
|
+
raise DshLifecycleError(
|
|
3843
|
+
"profile settings changed during configuration; refusing to overwrite concurrent edits"
|
|
3844
|
+
)
|
|
3845
|
+
transaction.pnpm_settings = settings
|
|
3846
|
+
|
|
3847
|
+
|
|
3848
|
+
def _check_pnpm_settings_after_mutation(
|
|
3849
|
+
executable: str, dsh_home: Path, profile: str, transaction: _ProfileTransaction
|
|
3850
|
+
) -> None:
|
|
3851
|
+
expected = dict(transaction.pnpm_settings or {})
|
|
3852
|
+
found = _read_pnpm_settings(executable, dsh_home, profile, transaction)
|
|
3853
|
+
# pnpm may record release-age approval for the exact reviewed SDK artifacts.
|
|
3854
|
+
previous = expected.pop("minimumReleaseAgeExclude", [])
|
|
3855
|
+
current = found.get("minimumReleaseAgeExclude", [])
|
|
3856
|
+
remaining = {
|
|
3857
|
+
key: value for key, value in found.items() if key != "minimumReleaseAgeExclude"
|
|
3858
|
+
}
|
|
3859
|
+
approved = {f"{CLAUDE_SDK_PACKAGE}@{CLAUDE_SDK_VERSION}"}
|
|
3860
|
+
approved.update(f"{package}@{version}" for package, version in PACKAGES.items())
|
|
3861
|
+
approved.update(
|
|
3862
|
+
f"{CLAUDE_SDK_PACKAGE}-{platform}@{CLAUDE_SDK_VERSION}"
|
|
3863
|
+
for platform in (
|
|
3864
|
+
"darwin-arm64",
|
|
3865
|
+
"darwin-x64",
|
|
3866
|
+
"linux-arm64",
|
|
3867
|
+
"linux-arm64-musl",
|
|
3868
|
+
"linux-x64",
|
|
3869
|
+
"linux-x64-musl",
|
|
3870
|
+
"win32-arm64",
|
|
3871
|
+
"win32-x64",
|
|
3872
|
+
)
|
|
3873
|
+
)
|
|
3874
|
+
if (
|
|
3875
|
+
remaining != expected
|
|
3876
|
+
or not isinstance(previous, list)
|
|
3877
|
+
or not isinstance(current, list)
|
|
3878
|
+
or any(not isinstance(item, str) for item in [*previous, *current])
|
|
3879
|
+
or not set(previous).issubset(current)
|
|
3880
|
+
or not set(current).difference(previous).issubset(approved)
|
|
3881
|
+
):
|
|
3882
|
+
transaction.rollback_blocked = True
|
|
3883
|
+
raise DshLifecycleError(
|
|
3884
|
+
"profile settings changed during package mutation; refusing to overwrite concurrent edits"
|
|
3885
|
+
)
|
|
3886
|
+
transaction.pnpm_settings = found
|
|
3887
|
+
|
|
3888
|
+
|
|
3889
|
+
def _assert_installed_claude_sdk(dsh_home: Path, profile: str) -> None:
|
|
3890
|
+
modules = dsh_home / "profiles" / profile / "node_modules"
|
|
3891
|
+
provider = modules / Path(CLAUDE_PROVIDER)
|
|
3892
|
+
_assert_no_symlink_ancestry(provider, "Claude provider")
|
|
3893
|
+
try:
|
|
3894
|
+
provider_manifest = json.loads(_read_regular_bytes(provider / "package.json"))
|
|
3895
|
+
except (DshLifecycleError, ValueError) as error:
|
|
3896
|
+
raise DshLifecycleError(
|
|
3897
|
+
"installed Claude provider metadata is missing or unsafe"
|
|
3898
|
+
) from error
|
|
3899
|
+
if (
|
|
3900
|
+
not isinstance(provider_manifest, dict)
|
|
3901
|
+
or provider_manifest.get("name") != CLAUDE_PROVIDER
|
|
3902
|
+
or provider_manifest.get("version") != SUPPORTED_DSH_VERSION
|
|
3903
|
+
):
|
|
3904
|
+
raise DshLifecycleError(
|
|
3905
|
+
"installed Claude provider version does not match the qualified DSH host"
|
|
3906
|
+
)
|
|
3907
|
+
# Match the actual bare import's nearest node_modules candidates, including shadows.
|
|
3908
|
+
candidates = (
|
|
3909
|
+
provider / "lib" / "node_modules",
|
|
3910
|
+
provider / "node_modules",
|
|
3911
|
+
provider.parent / "node_modules",
|
|
3912
|
+
modules,
|
|
3913
|
+
)
|
|
3914
|
+
for base in candidates:
|
|
3915
|
+
root = base / Path(CLAUDE_SDK_PACKAGE)
|
|
3916
|
+
if not root.exists() and not root.is_symlink():
|
|
3917
|
+
continue
|
|
3918
|
+
_assert_no_symlink_ancestry(root, "Claude SDK")
|
|
3919
|
+
try:
|
|
3920
|
+
manifest = json.loads(_read_regular_bytes(root / "package.json"))
|
|
3921
|
+
except (DshLifecycleError, ValueError) as error:
|
|
3922
|
+
raise DshLifecycleError(
|
|
3923
|
+
"installed Claude SDK metadata is unreadable"
|
|
3924
|
+
) from error
|
|
3925
|
+
if (
|
|
3926
|
+
isinstance(manifest, dict)
|
|
3927
|
+
and manifest.get("name") == CLAUDE_SDK_PACKAGE
|
|
3928
|
+
and manifest.get("version") == CLAUDE_SDK_VERSION
|
|
3929
|
+
):
|
|
3930
|
+
return
|
|
3931
|
+
break
|
|
3932
|
+
raise DshLifecycleError(
|
|
3933
|
+
f"installed Claude SDK does not match {CLAUDE_SDK_VERSION}; run 'ai-toolkit dsh update'"
|
|
3934
|
+
)
|
|
3935
|
+
|
|
3936
|
+
|
|
3510
3937
|
def _profile_unmanaged_dependencies(
|
|
3511
3938
|
dsh_home: Path, profile: str
|
|
3512
3939
|
) -> dict[str, dict[str, object]]:
|
|
@@ -4034,6 +4461,12 @@ def _install(
|
|
|
4034
4461
|
if source_hash != current_hash:
|
|
4035
4462
|
raise DshLifecycleError("installed DSH package preset source has drifted")
|
|
4036
4463
|
_current_dsh_executable(dsh_home)
|
|
4464
|
+
settings = _read_only_pnpm_settings(dsh_home, profile)
|
|
4465
|
+
if settings.get("overrides", {}) != _compatible_sdk_overrides(settings):
|
|
4466
|
+
raise DshLifecycleError(
|
|
4467
|
+
"Claude SDK compatibility configuration is missing; run 'ai-toolkit dsh update'"
|
|
4468
|
+
)
|
|
4469
|
+
_assert_installed_claude_sdk(dsh_home, profile)
|
|
4037
4470
|
return f"DSH profile integration already installed and unchanged: {profile}"
|
|
4038
4471
|
if preset_destination.exists() or preset_destination.is_symlink():
|
|
4039
4472
|
raise DshLifecycleError(
|
|
@@ -4059,6 +4492,10 @@ def _install(
|
|
|
4059
4492
|
for package, version in PACKAGES.items()
|
|
4060
4493
|
]
|
|
4061
4494
|
if dry_run:
|
|
4495
|
+
_compatible_sdk_overrides(_read_only_pnpm_settings(dsh_home, profile))
|
|
4496
|
+
print(
|
|
4497
|
+
f"PLAN Claude SDK profile override: {CLAUDE_SDK_OVERRIDE} -> {CLAUDE_SDK_VERSION}"
|
|
4498
|
+
)
|
|
4062
4499
|
for command in commands:
|
|
4063
4500
|
print("PLAN argv:", repr(command))
|
|
4064
4501
|
print(
|
|
@@ -4082,6 +4519,7 @@ def _install(
|
|
|
4082
4519
|
expected_transaction=profile_transaction,
|
|
4083
4520
|
)
|
|
4084
4521
|
try:
|
|
4522
|
+
_configure_claude_sdk(executable, dsh_home, profile, profile_transaction)
|
|
4085
4523
|
expected_managed = dict(present_packages)
|
|
4086
4524
|
observed_trees: dict[str, dict[str, object]] = {}
|
|
4087
4525
|
for (package, version), command in zip(PACKAGES.items(), commands, strict=True):
|
|
@@ -4115,6 +4553,7 @@ def _install(
|
|
|
4115
4553
|
expected_managed=PACKAGES,
|
|
4116
4554
|
expected_unmanaged=unmanaged_dependencies,
|
|
4117
4555
|
)
|
|
4556
|
+
_assert_installed_claude_sdk(dsh_home, profile)
|
|
4118
4557
|
package_trees = _package_tree_inventories(dsh_home, profile)
|
|
4119
4558
|
source = _package_source(dsh_home, profile)
|
|
4120
4559
|
preset_hash = _tree_hash(source)
|
|
@@ -4347,6 +4786,10 @@ def _update(
|
|
|
4347
4786
|
for package, version in PACKAGES.items()
|
|
4348
4787
|
]
|
|
4349
4788
|
if dry_run:
|
|
4789
|
+
_compatible_sdk_overrides(_read_only_pnpm_settings(dsh_home, profile))
|
|
4790
|
+
print(
|
|
4791
|
+
f"PLAN Claude SDK profile override: {CLAUDE_SDK_OVERRIDE} -> {CLAUDE_SDK_VERSION}"
|
|
4792
|
+
)
|
|
4350
4793
|
for command in commands:
|
|
4351
4794
|
print("PLAN argv:", repr(command))
|
|
4352
4795
|
print(f"PLAN preset: {_package_source(dsh_home, profile)} -> {destination}")
|
|
@@ -4366,11 +4809,10 @@ def _update(
|
|
|
4366
4809
|
expected_transaction=profile_transaction,
|
|
4367
4810
|
)
|
|
4368
4811
|
try:
|
|
4812
|
+
_configure_claude_sdk(executable, dsh_home, profile, profile_transaction)
|
|
4369
4813
|
expected_managed = dict(recorded_packages)
|
|
4370
4814
|
expected_trees = dict(record["package_trees"])
|
|
4371
|
-
for (package, version), command in zip(
|
|
4372
|
-
PACKAGES.items(), commands, strict=True
|
|
4373
|
-
):
|
|
4815
|
+
for (package, version), command in zip(PACKAGES.items(), commands, strict=True):
|
|
4374
4816
|
_run_profile_command(
|
|
4375
4817
|
command, dsh_home=dsh_home, transaction=profile_transaction
|
|
4376
4818
|
)
|
|
@@ -4398,6 +4840,7 @@ def _update(
|
|
|
4398
4840
|
expected_managed=PACKAGES,
|
|
4399
4841
|
expected_unmanaged=unmanaged_dependencies,
|
|
4400
4842
|
)
|
|
4843
|
+
_assert_installed_claude_sdk(dsh_home, profile)
|
|
4401
4844
|
package_trees = _package_tree_inventories(dsh_home, profile)
|
|
4402
4845
|
preset_hash, backup = _replace_owned_preset(
|
|
4403
4846
|
_package_source(dsh_home, profile),
|
|
@@ -4645,6 +5088,15 @@ def _doctor(*, profile: str) -> int:
|
|
|
4645
5088
|
print(f"{package}: {found} (expected {expected})")
|
|
4646
5089
|
if found != expected:
|
|
4647
5090
|
healthy = False
|
|
5091
|
+
try:
|
|
5092
|
+
settings = _read_only_pnpm_settings(dsh_home, profile)
|
|
5093
|
+
if settings.get("overrides", {}) != _compatible_sdk_overrides(settings):
|
|
5094
|
+
raise DshLifecycleError("profile SDK override is missing")
|
|
5095
|
+
_assert_installed_claude_sdk(dsh_home, profile)
|
|
5096
|
+
print(f"Claude SDK: compatible ({CLAUDE_SDK_VERSION})")
|
|
5097
|
+
except DshLifecycleError as error:
|
|
5098
|
+
print(f"Claude SDK: incompatible ({error})")
|
|
5099
|
+
healthy = False
|
|
4648
5100
|
try:
|
|
4649
5101
|
record = get_dsh_profile(profile)
|
|
4650
5102
|
except ValueError as error:
|
|
@@ -4778,6 +5230,9 @@ def _uninstall(
|
|
|
4778
5230
|
)
|
|
4779
5231
|
package_mutation_started = False
|
|
4780
5232
|
try:
|
|
5233
|
+
profile_transaction.pnpm_settings = _read_pnpm_settings(
|
|
5234
|
+
executable, dsh_home, profile, profile_transaction
|
|
5235
|
+
)
|
|
4781
5236
|
_assert_entry_unchanged(destination_identity, "managed preset")
|
|
4782
5237
|
backup = _relocate_owned_preset(
|
|
4783
5238
|
destination_identity,
|
|
@@ -4793,9 +5248,7 @@ def _uninstall(
|
|
|
4793
5248
|
)
|
|
4794
5249
|
expected_managed = dict(packages)
|
|
4795
5250
|
expected_trees = dict(record["package_trees"])
|
|
4796
|
-
for package, command in zip(
|
|
4797
|
-
reversed(recorded_packages), commands, strict=True
|
|
4798
|
-
):
|
|
5251
|
+
for package, command in zip(reversed(recorded_packages), commands, strict=True):
|
|
4799
5252
|
package_mutation_started = True
|
|
4800
5253
|
_run_profile_command(
|
|
4801
5254
|
command, dsh_home=dsh_home, transaction=profile_transaction
|