dsh-crate-web 0.1.0 → 0.1.2-preview.2
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/LICENSE +21 -0
- package/README.md +11 -290
- package/README.zh-CN.md +2 -303
- package/THIRD-PARTY-LICENSE.txt +21 -0
- package/cordis.patch.yml +1 -6
- package/core/dsh_pack/_version.py +1 -0
- package/core/dsh_pack/bundle.py +74 -0
- package/core/dsh_pack/cli.py +81 -18
- package/core/dsh_pack/desktop_verify.py +304 -0
- package/core/dsh_pack/diagnostics.py +1170 -0
- package/core/dsh_pack/errors.py +16 -1
- package/core/dsh_pack/export.py +86 -103
- package/core/dsh_pack/export_plan.py +96 -0
- package/core/dsh_pack/identity.py +25 -0
- package/core/dsh_pack/importer.py +1598 -1347
- package/core/dsh_pack/network.py +5 -4
- package/core/dsh_pack/pack.py +14 -2
- package/core/dsh_pack/portability.py +392 -0
- package/core/dsh_pack/preflight.py +63 -16
- package/core/dsh_pack/troubleshooting.py +94 -0
- package/core/dsh_pack/user_cli.py +16 -0
- package/core/dsh_pack/validation.py +64 -4
- package/core/dsh_pack/verify.py +1485 -1336
- package/core/dsh_pack/windows_verify.py +177 -0
- package/desktop-patch-helper.mjs +140 -0
- package/lib/client.js +292 -102
- package/lib/index.d.ts +7 -1
- package/lib/index.js +1121 -229
- package/package.json +13 -5
- package/restart-helper.mjs +121 -121
- package/runtime/python/LICENSE.txt +676 -676
- package/runtime/python/__install__.json +81 -81
- package/runtime/python/python314._pth +6 -6
- package/skills/dsh-crate-troubleshooting/SKILL.md +172 -0
- package/skills/dsh-crate-troubleshooting/compatibility.json +11 -0
- package/skills/dsh-crate-troubleshooting/reference/diagnostics.generated.md +96 -0
- package/skills/dsh-crate-troubleshooting/reference/repair-boundaries.md +60 -0
- package/skills/dsh-crate-troubleshooting/reference/stages.generated.md +22 -0
- package/verify-desktop.mjs +314 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""独立桌面验证器的Win32资源、进程身份和被动TCP表;不依赖测试目录。"""
|
|
2
|
+
import ctypes as c
|
|
3
|
+
import os,socket
|
|
4
|
+
|
|
5
|
+
class ProcessEntry(c.Structure):
|
|
6
|
+
_fields_ = [('size', c.c_ulong), ('usage', c.c_ulong), ('pid', c.c_ulong),
|
|
7
|
+
('heap', c.c_size_t), ('module', c.c_ulong), ('threads', c.c_ulong),
|
|
8
|
+
('parent', c.c_ulong), ('priority', c.c_long), ('flags', c.c_ulong), ('image', c.c_wchar * 260)]
|
|
9
|
+
|
|
10
|
+
def process_tree(root_pid):
|
|
11
|
+
kernel = c.WinDLL('kernel32', use_last_error=True)
|
|
12
|
+
kernel.CreateToolhelp32Snapshot.argtypes = [c.c_ulong, c.c_ulong]
|
|
13
|
+
kernel.CreateToolhelp32Snapshot.restype = c.c_void_p
|
|
14
|
+
kernel.Process32FirstW.argtypes = kernel.Process32NextW.argtypes = [c.c_void_p, c.POINTER(ProcessEntry)]
|
|
15
|
+
kernel.CloseHandle.argtypes = [c.c_void_p]
|
|
16
|
+
handle = kernel.CreateToolhelp32Snapshot(2, 0)
|
|
17
|
+
if handle == c.c_void_p(-1).value:
|
|
18
|
+
raise OSError(c.get_last_error(), 'process snapshot')
|
|
19
|
+
rows = []
|
|
20
|
+
try:
|
|
21
|
+
row = ProcessEntry()
|
|
22
|
+
row.size = c.sizeof(row)
|
|
23
|
+
valid = kernel.Process32FirstW(handle, c.byref(row))
|
|
24
|
+
while valid:
|
|
25
|
+
rows.append((row.pid, row.parent))
|
|
26
|
+
valid = kernel.Process32NextW(handle, c.byref(row))
|
|
27
|
+
error = c.get_last_error()
|
|
28
|
+
if error not in (0, 18):
|
|
29
|
+
raise OSError(error, 'process enumeration')
|
|
30
|
+
finally:
|
|
31
|
+
kernel.CloseHandle(handle)
|
|
32
|
+
owned = {root_pid}
|
|
33
|
+
for _ in range(len(rows) + 1):
|
|
34
|
+
added = {pid for pid, parent in rows if parent in owned} - owned
|
|
35
|
+
if not added:
|
|
36
|
+
return owned
|
|
37
|
+
owned.update(added)
|
|
38
|
+
raise RuntimeError('process ancestry did not converge')
|
|
39
|
+
|
|
40
|
+
class BasicJobLimits(c.Structure):
|
|
41
|
+
_fields_ = [('processTime', c.c_int64), ('jobTime', c.c_int64), ('flags', c.c_ulong),
|
|
42
|
+
('minimumWorkingSet', c.c_size_t), ('maximumWorkingSet', c.c_size_t),
|
|
43
|
+
('activeProcesses', c.c_ulong), ('affinity', c.c_size_t), ('priority', c.c_ulong), ('scheduling', c.c_ulong)]
|
|
44
|
+
|
|
45
|
+
class JobIo(c.Structure):
|
|
46
|
+
_fields_ = [(name, c.c_uint64) for name in ['readOperations', 'writeOperations', 'otherOperations', 'readBytes', 'writeBytes', 'otherBytes']]
|
|
47
|
+
|
|
48
|
+
class ExtendedJobLimits(c.Structure):
|
|
49
|
+
_fields_ = [('basic', BasicJobLimits), ('io', JobIo), ('processMemory', c.c_size_t),
|
|
50
|
+
('jobMemory', c.c_size_t), ('peakProcessMemory', c.c_size_t), ('peakJobMemory', c.c_size_t)]
|
|
51
|
+
|
|
52
|
+
class OwnedJob:
|
|
53
|
+
"""Only the created controller joins; its future children cannot outlive Core."""
|
|
54
|
+
def __init__(self):
|
|
55
|
+
self.kernel = c.WinDLL('kernel32', use_last_error=True)
|
|
56
|
+
self.kernel.CreateJobObjectW.argtypes = [c.c_void_p, c.c_wchar_p]
|
|
57
|
+
self.kernel.CreateJobObjectW.restype = c.c_void_p
|
|
58
|
+
self.kernel.SetInformationJobObject.argtypes = [c.c_void_p, c.c_int, c.c_void_p, c.c_ulong]
|
|
59
|
+
self.kernel.AssignProcessToJobObject.argtypes = [c.c_void_p, c.c_void_p]
|
|
60
|
+
self.kernel.CloseHandle.argtypes = [c.c_void_p]
|
|
61
|
+
self.handle = self.kernel.CreateJobObjectW(None, None)
|
|
62
|
+
if not self.handle:
|
|
63
|
+
raise OSError(c.get_last_error(), 'CreateJobObjectW')
|
|
64
|
+
limits = ExtendedJobLimits()
|
|
65
|
+
limits.basic.flags = 0x2000 # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, no breakaway.
|
|
66
|
+
if not self.kernel.SetInformationJobObject(self.handle, 9, c.byref(limits), c.sizeof(limits)):
|
|
67
|
+
error = c.get_last_error()
|
|
68
|
+
self.close()
|
|
69
|
+
raise OSError(error, 'SetInformationJobObject')
|
|
70
|
+
|
|
71
|
+
def assign(self, process):
|
|
72
|
+
if not self.kernel.AssignProcessToJobObject(self.handle, int(process._handle)):
|
|
73
|
+
raise OSError(c.get_last_error(), 'AssignProcessToJobObject')
|
|
74
|
+
|
|
75
|
+
def active_processes(self):
|
|
76
|
+
class Accounting(c.Structure):
|
|
77
|
+
_fields_ = [(name, c.c_int64) for name in ['user', 'kernel', 'periodUser', 'periodKernel']] + [
|
|
78
|
+
(name, c.c_ulong) for name in ['pageFaults', 'totalProcesses', 'activeProcesses', 'terminatedProcesses']]
|
|
79
|
+
value = Accounting()
|
|
80
|
+
self.kernel.QueryInformationJobObject.argtypes = [c.c_void_p, c.c_int, c.c_void_p, c.c_ulong, c.c_void_p]
|
|
81
|
+
if not self.kernel.QueryInformationJobObject(self.handle, 1, c.byref(value), c.sizeof(value), None):
|
|
82
|
+
raise OSError(c.get_last_error(), 'QueryInformationJobObject')
|
|
83
|
+
return fields(value)
|
|
84
|
+
|
|
85
|
+
def close(self):
|
|
86
|
+
if self.handle:
|
|
87
|
+
self.kernel.CloseHandle(self.handle)
|
|
88
|
+
self.handle = None
|
|
89
|
+
class Memory(c.Structure):
|
|
90
|
+
_fields_=[('length',c.c_ulong),('load',c.c_ulong)]+[(n,c.c_uint64) for n in ['totalPhysical','availablePhysical','totalPageFile','availablePageFile','totalVirtual','availableVirtual','availableExtendedVirtual']]
|
|
91
|
+
class ProcessMemory(c.Structure):
|
|
92
|
+
_fields_=[('cb',c.c_ulong),('pageFaults',c.c_ulong)]+[(n,c.c_size_t) for n in ['peakWorkingSet','workingSet','peakPagedPool','pagedPool','peakNonpagedPool','nonpagedPool','pageFile','peakPageFile','privateBytes']]
|
|
93
|
+
class Io(c.Structure):
|
|
94
|
+
_fields_=[(n,c.c_uint64) for n in ['readOperations','writeOperations','otherOperations','readBytes','writeBytes','otherBytes']]
|
|
95
|
+
def fields(obj):return {name:getattr(obj,name) for name,_ in obj._fields_}
|
|
96
|
+
|
|
97
|
+
def summarize_resources(rows):
|
|
98
|
+
"""区间差分CPU及离散采样峰值;不把Host资源归因于单个Worker。"""
|
|
99
|
+
valid=[r for r in rows if 'cpu100ns' in r.get('system',{})];cpu=[];by={}
|
|
100
|
+
for a,b in zip(valid,valid[1:]):
|
|
101
|
+
previous=a['system']['cpu100ns'];current=b['system']['cpu100ns']
|
|
102
|
+
total=sum(current[k]-previous[k] for k in ['kernelIncludingIdle','user']);idle=current['idle']-previous['idle']
|
|
103
|
+
if total>0:cpu.append(100*(total-idle)/total)
|
|
104
|
+
for row in valid:
|
|
105
|
+
for process in row.get('processes',[]):by.setdefault(process['pid'],[]).append((row,process))
|
|
106
|
+
processes=[]
|
|
107
|
+
for pid,pairs in by.items():
|
|
108
|
+
rates=[];memory=[p['memory'] for r,p in pairs if p.get('memory')]
|
|
109
|
+
for (a,p),(b,q) in zip(pairs,pairs[1:]):
|
|
110
|
+
elapsed=(b['monotonicNs']-a['monotonicNs'])/1e9;delta=sum(q['cpu100ns'].values())-sum(p['cpu100ns'].values())
|
|
111
|
+
if elapsed>0:rates.append(delta/1e7/elapsed/(b['system']['logicalCpus'] or 1)*100)
|
|
112
|
+
last=pairs[-1][1]
|
|
113
|
+
processes.append({'pid':pid,'role':last['role'],'peakSampledWorkingSetBytes':max((m['workingSet'] for m in memory),default=None),
|
|
114
|
+
'peakSampledPrivateBytes':max((m['privateBytes'] for m in memory),default=None),'cpuPercentOfMachinePeak':max(rates,default=None),
|
|
115
|
+
'finalCumulativeCpuSeconds':sum(last['cpu100ns'].values())/1e7,'finalCumulativeIo':last.get('io')})
|
|
116
|
+
loads=[r['system']['memory']['load'] for r in valid]
|
|
117
|
+
return {'systemCpuPercentPeak':max(cpu,default=None),'systemMemoryLoadPercentRange':[min(loads),max(loads)] if loads else None,
|
|
118
|
+
'processes':processes,'interpretation':'离散采样;Host含Worker和其他业务。I/O为Windows进程累计计数,不能等同物理磁盘吞吐;缺样本为null。'}
|
|
119
|
+
|
|
120
|
+
class WindowsCounters:
|
|
121
|
+
def __init__(self):
|
|
122
|
+
self.k=c.WinDLL('kernel32',use_last_error=True);self.ps=c.WinDLL('psapi',use_last_error=True)
|
|
123
|
+
k=self.k
|
|
124
|
+
k.OpenProcess.argtypes=[c.c_ulong,c.c_int,c.c_ulong];k.OpenProcess.restype=c.c_void_p
|
|
125
|
+
k.CloseHandle.argtypes=[c.c_void_p];k.CloseHandle.restype=c.c_int
|
|
126
|
+
k.GetProcessTimes.argtypes=[c.c_void_p]+[c.POINTER(c.c_uint64)]*4;k.GetProcessTimes.restype=c.c_int
|
|
127
|
+
k.GetSystemTimes.argtypes=[c.POINTER(c.c_uint64)]*3;k.GetSystemTimes.restype=c.c_int
|
|
128
|
+
k.GlobalMemoryStatusEx.argtypes=[c.POINTER(Memory)];k.GlobalMemoryStatusEx.restype=c.c_int
|
|
129
|
+
k.GetProcessIoCounters.argtypes=[c.c_void_p,c.POINTER(Io)];k.GetProcessIoCounters.restype=c.c_int
|
|
130
|
+
k.GetExitCodeProcess.argtypes=[c.c_void_p,c.POINTER(c.c_ulong)];k.GetExitCodeProcess.restype=c.c_int
|
|
131
|
+
k.QueryFullProcessImageNameW.argtypes=[c.c_void_p,c.c_ulong,c.c_wchar_p,c.POINTER(c.c_ulong)];k.QueryFullProcessImageNameW.restype=c.c_int
|
|
132
|
+
self.ps.GetProcessMemoryInfo.argtypes=[c.c_void_p,c.POINTER(ProcessMemory),c.c_ulong];self.ps.GetProcessMemoryInfo.restype=c.c_int
|
|
133
|
+
def system(self):
|
|
134
|
+
idle,kernel,user=(c.c_uint64() for _ in range(3));memory=Memory();memory.length=c.sizeof(memory)
|
|
135
|
+
if not self.k.GetSystemTimes(c.byref(idle),c.byref(kernel),c.byref(user)) or not self.k.GlobalMemoryStatusEx(c.byref(memory)):raise OSError(c.get_last_error(),'system counters')
|
|
136
|
+
return {'cpu100ns':{'idle':idle.value,'kernelIncludingIdle':kernel.value,'user':user.value},'memory':fields(memory),'logicalCpus':os.cpu_count()}
|
|
137
|
+
def open(self,pid):
|
|
138
|
+
handle=self.k.OpenProcess(0x1010,False,pid)
|
|
139
|
+
if not handle:raise OSError(c.get_last_error(),'OpenProcess for observation')
|
|
140
|
+
image=c.create_unicode_buffer(32768);length=c.c_ulong(len(image))
|
|
141
|
+
if not self.k.QueryFullProcessImageNameW(handle,0,image,c.byref(length)):
|
|
142
|
+
self.k.CloseHandle(handle);raise OSError(c.get_last_error(),'process image')
|
|
143
|
+
return handle,image.value
|
|
144
|
+
def process(self,pid,handle,role,image):
|
|
145
|
+
creation,end,kernel,user=(c.c_uint64() for _ in range(4));memory=ProcessMemory();memory.cb=c.sizeof(memory);io=Io();code=c.c_ulong()
|
|
146
|
+
if not self.k.GetProcessTimes(handle,*[c.byref(x) for x in [creation,end,kernel,user]]):raise OSError(c.get_last_error(),'process times')
|
|
147
|
+
mem_ok=bool(self.ps.GetProcessMemoryInfo(handle,c.byref(memory),memory.cb));io_ok=bool(self.k.GetProcessIoCounters(handle,c.byref(io)))
|
|
148
|
+
exited=bool(end.value);self.k.GetExitCodeProcess(handle,c.byref(code))
|
|
149
|
+
return {'pid':pid,'role':role,'image':image,'creationFiletime':str(creation.value),'cpu100ns':{'kernel':kernel.value,'user':user.value},
|
|
150
|
+
'memory':fields(memory) if mem_ok else None,'io':fields(io) if io_ok else None,'exited':exited,'exitCode':code.value if exited else None}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Row4(c.Structure):
|
|
154
|
+
_fields_=[('state',c.c_ulong),('localAddress',c.c_ubyte*4),('localPort',c.c_ulong),('remoteAddress',c.c_ubyte*4),('remotePort',c.c_ulong),('pid',c.c_ulong)]
|
|
155
|
+
class Row6(c.Structure):
|
|
156
|
+
_fields_=[('localAddress',c.c_ubyte*16),('localScope',c.c_ulong),('localPort',c.c_ulong),('remoteAddress',c.c_ubyte*16),('remoteScope',c.c_ulong),('remotePort',c.c_ulong),('state',c.c_ulong),('pid',c.c_ulong)]
|
|
157
|
+
STATES={1:'CLOSED',2:'LISTEN',3:'SYN_SENT',4:'SYN_RCVD',5:'ESTABLISHED',6:'FIN_WAIT1',7:'FIN_WAIT2',8:'CLOSE_WAIT',9:'CLOSING',10:'LAST_ACK',11:'TIME_WAIT',12:'DELETE_TCB'}
|
|
158
|
+
|
|
159
|
+
def tcp_rows():
|
|
160
|
+
dll=c.WinDLL('iphlpapi',use_last_error=True)
|
|
161
|
+
dll.GetExtendedTcpTable.argtypes=[c.c_void_p,c.POINTER(c.c_ulong),c.c_int,c.c_ulong,c.c_ulong,c.c_ulong];dll.GetExtendedTcpTable.restype=c.c_ulong
|
|
162
|
+
rows=[]
|
|
163
|
+
for family,kind in [(socket.AF_INET,Row4),(socket.AF_INET6,Row6)]:
|
|
164
|
+
size=c.c_ulong();status=dll.GetExtendedTcpTable(None,c.byref(size),False,family,5,0)
|
|
165
|
+
if status not in (0,122):raise OSError(status,'TCP表大小查询')
|
|
166
|
+
buf=c.create_string_buffer(size.value+65536);size.value=len(buf)
|
|
167
|
+
status=dll.GetExtendedTcpTable(buf,c.byref(size),False,family,5,0)
|
|
168
|
+
if status:raise OSError(status,'TCP表读取')
|
|
169
|
+
count=c.c_ulong.from_buffer(buf).value
|
|
170
|
+
if 4+count*c.sizeof(kind)>len(buf):raise ValueError('TCP表结构超出缓冲区')
|
|
171
|
+
for i in range(count):
|
|
172
|
+
item=kind.from_buffer_copy(buf,4+i*c.sizeof(kind))
|
|
173
|
+
rows.append({'family':'IPv4' if family==socket.AF_INET else 'IPv6','localAddress':socket.inet_ntop(family,bytes(item.localAddress)),
|
|
174
|
+
'localPort':socket.ntohs(item.localPort&65535),'remoteAddress':socket.inet_ntop(family,bytes(item.remoteAddress)),
|
|
175
|
+
'remotePort':socket.ntohs(item.remotePort&65535),'state':STATES.get(item.state,str(item.state)),'pid':item.pid})
|
|
176
|
+
return rows
|
|
177
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Read-only adapter: use the installed official parser/composer, never activate entries. */
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { dirname, join, resolve } from 'node:path'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
8
|
+
|
|
9
|
+
const hash = value => createHash('sha256').update(value).digest('hex')
|
|
10
|
+
const runtime = resolve(process.argv[2])
|
|
11
|
+
const profile = resolve(process.argv[3])
|
|
12
|
+
const anchor = join(runtime, 'package.json')
|
|
13
|
+
const require = createRequire(anchor)
|
|
14
|
+
const { parseDocument, isMap, isScalar, isSeq } = require('yaml')
|
|
15
|
+
const boot = await import(pathToFileURL(join(runtime, 'node_modules/@deepseek-ai/dsh-app-boot/lib/index.js')).href)
|
|
16
|
+
|
|
17
|
+
function rowsById(rows, result = new Map(), depth = 0) {
|
|
18
|
+
if (depth > 64) throw new Error('patch nesting exceeds adapter limit')
|
|
19
|
+
for (const row of rows ?? []) {
|
|
20
|
+
if (!row || typeof row !== 'object') continue
|
|
21
|
+
if (typeof row.id === 'string') result.set(row.id, [...(result.get(row.id) ?? []), row])
|
|
22
|
+
if (Array.isArray(row.config)) rowsById(row.config, result, depth + 1)
|
|
23
|
+
if (Array.isArray(row.insert)) rowsById(row.insert, result, depth + 1)
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasFilesystemInsert(rows, depth = 0) {
|
|
29
|
+
if (depth > 64) throw new Error('patch nesting exceeds adapter limit')
|
|
30
|
+
return (rows ?? []).some(row => row && typeof row === 'object' &&
|
|
31
|
+
(typeof row.name === 'string' && row.name.startsWith('file:') ||
|
|
32
|
+
row.group && Array.isArray(row.config) && hasFilesystemInsert(row.config, depth + 1)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function main() {
|
|
36
|
+
const path = join(profile, 'cordis.patch.yml')
|
|
37
|
+
if (!existsSync(path)) return { changed: false, reason: 'no-profile-patch' }
|
|
38
|
+
const original = readFileSync(path)
|
|
39
|
+
const source = boot.loadOptionalPatches('dsh-crate', path) ?? []
|
|
40
|
+
// A custom root is deliberately left to its owner; this adapter targets the
|
|
41
|
+
// normal empty-root + ordered Bundle + id-targeted override contract only.
|
|
42
|
+
if ((boot.loadOptionalPatches('dsh-crate', join(profile, 'cordis.yml')) ?? []).length) {
|
|
43
|
+
return { changed: false, reason: 'custom-root-entries' }
|
|
44
|
+
}
|
|
45
|
+
const manifest = boot.readProfileManifest('dsh-crate', profile)
|
|
46
|
+
// Retired official Bundles are not in the runtime's current catalog. Avoid
|
|
47
|
+
// loadProfileDirectory here: it can rewrite a manifest to remove them.
|
|
48
|
+
const catalog = new Set([...boot.DEFAULT_PROFILE_BUNDLES, ...boot.OPTIONAL_BUNDLES,
|
|
49
|
+
...Object.values(boot.PROFILE_TEMPLATES).flatMap(template => template.bundles)])
|
|
50
|
+
const exemptions = boot.readProfileVersionExemptions(profile)
|
|
51
|
+
const layers = []
|
|
52
|
+
for (const name of manifest.dsh?.profile?.bundles ?? []) {
|
|
53
|
+
if (name.startsWith('@deepseek-ai/') && !catalog.has(name)) return { changed: false, reason: 'uncatalogued-official-bundle' }
|
|
54
|
+
const root = boot.resolveBundleDir('dsh-crate', name, anchor, profile)
|
|
55
|
+
const bundleManifest = boot.readProfileManifest('dsh-crate', root)
|
|
56
|
+
const issue = boot.evaluatePluginCompatibility(bundleManifest, exemptions)
|
|
57
|
+
if (issue && !issue.exempted) return { changed: false, reason: 'incompatible-bundle' }
|
|
58
|
+
const bundle = bundleManifest.dsh?.bundle
|
|
59
|
+
if (!bundle) throw new Error('declared Bundle has no patch')
|
|
60
|
+
layers.push(...boot.bundlePatchPaths(root, bundle).map(file => boot.loadOverlayPatches('dsh-crate', file)))
|
|
61
|
+
}
|
|
62
|
+
const baseWarnings = []
|
|
63
|
+
const base = boot.composeEntries(layers, message => baseWarnings.push(message))
|
|
64
|
+
if (baseWarnings.length) return { changed: false, reason: 'ambiguous-bundle-composition' }
|
|
65
|
+
const declarations = new Map()
|
|
66
|
+
for (const patches of layers) {
|
|
67
|
+
for (const patch of patches) {
|
|
68
|
+
for (const [id, rows] of rowsById(patch.insert ?? [])) {
|
|
69
|
+
declarations.set(id, (declarations.get(id) ?? 0) + rows.length)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const slots = rowsById(base)
|
|
74
|
+
const occurrences = rowsById(source)
|
|
75
|
+
// The official parser anchors filesystem insert names beside the original
|
|
76
|
+
// file. Preserve these profiles unchanged rather than compare two dialects.
|
|
77
|
+
if (source.some(patch => hasFilesystemInsert(patch.insert))) {
|
|
78
|
+
return { changed: false, reason: 'filesystem-insert-requires-official-parser' }
|
|
79
|
+
}
|
|
80
|
+
const homePatch = boot.loadOptionalPatches('dsh-crate', join(dirname(dirname(profile)), 'cordis.patch.yml')) ?? []
|
|
81
|
+
const homeSlots = rowsById(homePatch)
|
|
82
|
+
const document = parseDocument(original.toString('utf8'), {
|
|
83
|
+
customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: value => value }],
|
|
84
|
+
uniqueKeys: true,
|
|
85
|
+
})
|
|
86
|
+
if (document.errors.length || !isSeq(document.contents)) throw new Error('profile patch cannot be safely represented')
|
|
87
|
+
const changes = []
|
|
88
|
+
for (const item of document.contents.items) {
|
|
89
|
+
if (!isMap(item)) continue
|
|
90
|
+
const idNode = item.get('id', true), nameNode = item.get('name', true)
|
|
91
|
+
if (!isScalar(idNode) || !isScalar(nameNode)) continue
|
|
92
|
+
const id = idNode.value, name = nameNode.value
|
|
93
|
+
if (typeof id !== 'string' || typeof name !== 'string' || !/^@deepseek-ai\/[A-Za-z0-9._-]+$/.test(name)) continue
|
|
94
|
+
const slot = slots.get(id)
|
|
95
|
+
if (slot?.length !== 1 || slot[0].name !== name || declarations.get(id) !== 1 || occurrences.get(id)?.length !== 1 || homeSlots.has(id)) continue
|
|
96
|
+
// Preserve explicit package overrides and any hoisted dependency that can
|
|
97
|
+
// select a different implementation than the installation anchor.
|
|
98
|
+
if (name in (manifest.dependencies ?? {}) || name in (manifest.optionalDependencies ?? {})) continue
|
|
99
|
+
if (existsSync(join(profile, 'node_modules', name, 'package.json')) ||
|
|
100
|
+
existsSync(join(dirname(profile), 'node_modules', name, 'package.json'))) continue
|
|
101
|
+
let official
|
|
102
|
+
try { official = require.resolve(name + '/package.json') } catch { continue }
|
|
103
|
+
if (!realpathSync(official).replaceAll('\\', '/').startsWith(realpathSync(join(runtime, 'node_modules')).replaceAll('\\', '/') + '/')) continue
|
|
104
|
+
try {
|
|
105
|
+
const local = createRequire(join(profile, 'package.json')).resolve(name + '/package.json')
|
|
106
|
+
if (realpathSync(local) !== realpathSync(official)) continue
|
|
107
|
+
} catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') continue }
|
|
108
|
+
item.delete('name')
|
|
109
|
+
changes.push({ id, name })
|
|
110
|
+
}
|
|
111
|
+
if (!changes.length) return { changed: false, reason: 'no-equivalent-official-overrides' }
|
|
112
|
+
const content = document.toString()
|
|
113
|
+
// Official loadOptionalPatches treats !!js as inert expression nodes. It
|
|
114
|
+
// does not evaluate them, and this helper never calls Loader/boot/apply.
|
|
115
|
+
const yaml = require('js-yaml')
|
|
116
|
+
const expression = new yaml.Type('tag:yaml.org,2002:js', {
|
|
117
|
+
kind: 'scalar', resolve: value => typeof value === 'string',
|
|
118
|
+
construct: value => ({ __jsExpr: value }),
|
|
119
|
+
})
|
|
120
|
+
const next = yaml.load(content, { schema: yaml.JSON_SCHEMA.extend(expression) })
|
|
121
|
+
const beforeWarnings = [], afterWarnings = []
|
|
122
|
+
const before = boot.composeEntries([...layers, source, homePatch], message => beforeWarnings.push(message))
|
|
123
|
+
const after = boot.composeEntries([...layers, next, homePatch], message => afterWarnings.push(message))
|
|
124
|
+
if (!isDeepStrictEqual(before, after) || !isDeepStrictEqual(beforeWarnings, afterWarnings)) {
|
|
125
|
+
throw new Error('the official Loader composition changed; refusing adaptation')
|
|
126
|
+
}
|
|
127
|
+
return { changed: true, originalSha256: hash(original), runtimeSha256: hash(content),
|
|
128
|
+
loaderTreeSha256: hash(JSON.stringify(before)), removedNames: changes,
|
|
129
|
+
contentBase64: Buffer.from(content).toString('base64') }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try { process.stdout.write(JSON.stringify(main()) + '\n') }
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Core redacts the full process diagnostic before exposing it. Do not echo
|
|
135
|
+
// parser snippets, which may contain values from the configuration file.
|
|
136
|
+
const detail = error instanceof Error ? { name: error.name,
|
|
137
|
+
message: error.message.split(/\n|\r/)[0], code: error.code ?? null } : { name: 'Error', message: String(error).split(/\n|\r/)[0] }
|
|
138
|
+
process.stdout.write(JSON.stringify({ error: 'official patch equivalence could not be verified', detail }) + '\n')
|
|
139
|
+
process.exitCode = 1
|
|
140
|
+
}
|