@yugoo-design/companion 0.1.0-beta.3 → 0.1.0-beta.4
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 +34 -7
- package/bin/installer.mjs +29 -30
- package/bin/npm-agent-installation.mjs +1 -1
- package/bin/npm-installer-claude.mjs +28 -7
- package/bin/npm-installer-dsh.mjs +192 -9
- package/package.json +1 -1
- package/payload/overlays/claude-code/.claude-plugin/plugin.json +1 -1
- package/payload/overlays/codex/.codex-plugin/plugin.json +1 -1
- package/payload/overlays/qoder/.qoder-plugin/plugin.json +1 -1
- package/payload/shared/authoring/panel-components.json +40 -40
- package/payload/shared/web/client.js +4 -12
- package/release-hosts.json +1 -1
- package/release-manifest.json +19 -15
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Yugoo Design Companion — beta
|
|
2
2
|
|
|
3
|
-
Package: `@yugoo-design/companion@0.1.0-beta.
|
|
3
|
+
Package: `@yugoo-design/companion@0.1.0-beta.4`.
|
|
4
4
|
|
|
5
5
|
Yugoo Design (语构设计系统) is an interactive design plugin for native Agent Hosts.
|
|
6
6
|
It combines domain methods, Design Projects, versioned Design Files and shared
|
|
@@ -11,19 +11,46 @@ This beta is experimental. Native installation, tools, Panel saving, KAG and GRA
|
|
|
11
11
|
must be verified for the actual host version and platform. A successful package
|
|
12
12
|
installation or runtime-load check does not establish all of those capabilities.
|
|
13
13
|
|
|
14
|
-
Use Node.js `>=22.19.0 <23` and a durable npm installation directory.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Use Node.js `>=22.19.0 <23` and a durable npm installation directory. The current
|
|
15
|
+
release uses the packaged native Git library for normal Design versioning without
|
|
16
|
+
system Git. It bundles Windows x64 MSVC and Linux x64 GNU runtimes; other platforms
|
|
17
|
+
are unsupported. System Git is separate and needed only for the explicit CLI
|
|
18
|
+
diagnostic backend and development fixtures. Missing native support is reported
|
|
19
|
+
rather than silently spawning system Git. Resolve the
|
|
19
20
|
selected beta to the exact version and integrity before acquisition; keep the
|
|
20
21
|
previous installed version available during updates. Do not substitute `latest`.
|
|
21
22
|
|
|
22
23
|
```sh
|
|
23
|
-
npm install --ignore-scripts --save-exact @yugoo-design/companion@0.1.0-beta.
|
|
24
|
+
ONNXRUNTIME_NODE_INSTALL_CUDA=skip npm install --ignore-scripts=false --save-exact @yugoo-design/companion@0.1.0-beta.4 --registry=https://registry.npmjs.org
|
|
24
25
|
npx --no-install design-system --help
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
First-release local embedding runs on CPU. Set the supported
|
|
29
|
+
`ONNXRUNTIME_NODE_INSTALL_CUDA=skip` environment before acquisition to skip ONNX
|
|
30
|
+
Runtime's extra CUDA download while retaining normal dependency lifecycle
|
|
31
|
+
scripts. Preserve the upstream packages and other environment values. The
|
|
32
|
+
installed package cannot configure its acquiring parent npm/npx process; a bare
|
|
33
|
+
npm/npx acquisition does not apply this policy automatically. Keep the same
|
|
34
|
+
policy on later host-installation children that acquire dependencies. The
|
|
35
|
+
maintained DSH adapter passes it to the official CLI in Web and Desktop; older
|
|
36
|
+
installed adapters may need the caller to supply it. This does not establish
|
|
37
|
+
installation success or remove the separate embedding-model download/cache step.
|
|
38
|
+
The default product does not provision GPU runtimes; optional GPU support and
|
|
39
|
+
configurable or user-local model selection are future work.
|
|
40
|
+
|
|
41
|
+
In Windows PowerShell, scope the setting to acquisition and restore its previous
|
|
42
|
+
process value:
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
$previousOrtCuda = [Environment]::GetEnvironmentVariable('ONNXRUNTIME_NODE_INSTALL_CUDA', 'Process')
|
|
46
|
+
try {
|
|
47
|
+
$env:ONNXRUNTIME_NODE_INSTALL_CUDA = 'skip'
|
|
48
|
+
npm install --ignore-scripts=false --save-exact @yugoo-design/companion@0.1.0-beta.4 --registry=https://registry.npmjs.org
|
|
49
|
+
} finally {
|
|
50
|
+
[Environment]::SetEnvironmentVariable('ONNXRUNTIME_NODE_INSTALL_CUDA', $previousOrtCuda, 'Process')
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
27
54
|
Ask the Agent in your current host to install or update Yugoo Design as one
|
|
28
55
|
product. It should identify the host from its actual execution context or your
|
|
29
56
|
explicit choice, not from which CLIs happen to be installed. If identity is
|
package/bin/installer.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { checkRuntime, inspectRuntime, assertGitRuntimeReceipt } from './npm-run
|
|
|
15
15
|
import { runDesktopDshInstaller } from './npm-installer-dsh-desktop.mjs'
|
|
16
16
|
import { desktopPreparationMain, desktopPreparationReuseMain } from './npm-desktop-preparation.mjs'
|
|
17
17
|
import { agentInstallation, AGENT_INSTALL_HOSTS, DESKTOP_INSTALL_OPTIONS } from './npm-agent-installation.mjs'
|
|
18
|
-
import { createDshInstallerAdapter, planDshInstallation, applyDshInstallation } from './npm-installer-dsh.mjs'
|
|
18
|
+
import { createDshInstallerAdapter, planDshInstallation, applyDshInstallation, validateReleaseHosts } from './npm-installer-dsh.mjs'
|
|
19
19
|
|
|
20
20
|
export const INSTALL_HOSTS = ['dsh','codex','claude-code','qoder','codex-desktop','qwenwork']
|
|
21
21
|
const PLUGIN = 'yugoo-design'
|
|
@@ -623,25 +623,7 @@ async function recoverOwnedInstallation(receipt,{adapter,ownRecovery,uninstall=f
|
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
const DSH_FLAGS = ['dsh-config','profile','aggregate-selector','registry']
|
|
626
|
-
|
|
627
|
-
// explicit selector/registry route; preparation values never imply a release.
|
|
628
|
-
export function validateReleaseHosts(config) {
|
|
629
|
-
assert.ok(config && typeof config==='object' && !Array.isArray(config),'Release hosts object required')
|
|
630
|
-
assert.deepEqual(Object.keys(config).sort(),['hosts','schemaVersion'],'Unknown release hosts fields')
|
|
631
|
-
assert.equal(config.schemaVersion,1,'Unsupported release hosts schema')
|
|
632
|
-
assert.ok(config.hosts && typeof config.hosts==='object' && !Array.isArray(config.hosts),'Release hosts map required')
|
|
633
|
-
assert.deepEqual(Object.keys(config.hosts),['dsh'],'Release mapping currently supports DSH only')
|
|
634
|
-
const dsh=config.hosts.dsh
|
|
635
|
-
assert.ok(dsh && typeof dsh==='object' && !Array.isArray(dsh),'DSH release mapping required')
|
|
636
|
-
assert.deepEqual(Object.keys(dsh).sort(),['aggregatePackage','aggregateVersion','registry'],'Unknown DSH release fields')
|
|
637
|
-
assert.match(dsh.aggregatePackage ?? '',/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u,'Exact aggregate package name required')
|
|
638
|
-
if(dsh.aggregateVersion===null && dsh.registry===null)return null
|
|
639
|
-
assert.match(dsh.aggregateVersion ?? '',/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,'Exact aggregate version required')
|
|
640
|
-
assert.equal(typeof dsh.registry,'string','Bound HTTPS registry required')
|
|
641
|
-
const url=new URL(dsh.registry)
|
|
642
|
-
assert.ok(url.protocol==='https:' && !url.username && !url.password && !url.search && !url.hash,'Credential-free HTTPS registry required')
|
|
643
|
-
return {aggregateSelector:`${dsh.aggregatePackage}@${dsh.aggregateVersion}`,registry:url.href}
|
|
644
|
-
}
|
|
626
|
+
export { validateReleaseHosts }
|
|
645
627
|
async function dshReleaseSelection(values,npmRoot) {
|
|
646
628
|
const bytes=await optional(join(npmRoot,'release-hosts.json'))
|
|
647
629
|
const mapped=bytes===null?null:validateReleaseHosts(JSON.parse(bytes))
|
|
@@ -667,7 +649,7 @@ async function dshConfiguration(path) {
|
|
|
667
649
|
}
|
|
668
650
|
return config
|
|
669
651
|
}
|
|
670
|
-
async function dshOutputDirectory(path,{npmRoot,config,configPath,planPath}) {
|
|
652
|
+
async function dshOutputDirectory(path,{npmRoot,config,configPath,planPath,previousProductRoot}) {
|
|
671
653
|
assert.ok(typeof path==='string'&&isAbsolute(path),'Explicit absolute external receipt directory required')
|
|
672
654
|
const output=resolve(path)
|
|
673
655
|
await safePath(output)
|
|
@@ -678,7 +660,7 @@ async function dshOutputDirectory(path,{npmRoot,config,configPath,planPath}) {
|
|
|
678
660
|
try { assert.equal(await realpath(ancestor),ancestor,'Output ancestor realpath mismatch'); break }
|
|
679
661
|
catch(error){if(error.code!=='ENOENT')throw error;ancestor=dirname(ancestor)}
|
|
680
662
|
}
|
|
681
|
-
const roots=[resolve(npmRoot),resolve(config.dshHome)]
|
|
663
|
+
const roots=[resolve(npmRoot),resolve(config.dshHome),...previousProductRoot ? [resolve(previousProductRoot)] : []]
|
|
682
664
|
for(const key of ['node','cli','hostManifest','loader','loaderManifest', ...config.desktopHostManifest ? ['desktopHostManifest'] : []]) {
|
|
683
665
|
roots.push(dirname(resolve(config[key].path)),dirname(await realpath(config[key].path)))
|
|
684
666
|
}
|
|
@@ -718,9 +700,12 @@ async function dshInstallerEntry(values,{npmRoot,dshAdapterFactory,args,signal,d
|
|
|
718
700
|
const name=selection.aggregateSelector.slice(0,selection.aggregateSelector.lastIndexOf('@'))
|
|
719
701
|
aggregateDirectory=dirname(await realpath(createRequire(join(npmRoot,'package.json')).resolve(name+'/package.json')))
|
|
720
702
|
} catch(error) { if(error.code!=='MODULE_NOT_FOUND')throw error }
|
|
721
|
-
const options={adapter,profile:values.profile,...selection
|
|
703
|
+
const options={adapter,profile:values.profile,...selection,productRoot:await realpath(resolve(npmRoot)),
|
|
704
|
+
...values['previous-receipt'] ? {previousReceipt:values['previous-receipt']} : {},
|
|
705
|
+
...aggregateDirectory ? {aggregateDirectory} : {}}
|
|
722
706
|
if(values['dry-run']) {
|
|
723
707
|
const plan=await planDshInstallation(options)
|
|
708
|
+
if(plan.previous)await dshOutputDirectory(output,{npmRoot,config,configPath,previousProductRoot:plan.previous.product.npmRoot})
|
|
724
709
|
const planFile=await writeDshOutput(output,plan.planSha256+'.dsh-plan.json',plan)
|
|
725
710
|
return {...plan,planFile}
|
|
726
711
|
}
|
|
@@ -731,14 +716,27 @@ async function dshInstallerEntry(values,{npmRoot,dshAdapterFactory,args,signal,d
|
|
|
731
716
|
assert.equal(plan.profile,options.profile,'Saved DSH profile mismatch')
|
|
732
717
|
assert.equal(plan.aggregateSelector,options.aggregateSelector,'Saved aggregate selector mismatch')
|
|
733
718
|
assert.equal(plan.registry,new URL(options.registry).href,'Saved registry mismatch')
|
|
719
|
+
assert.equal(plan.previousReceipt,options.previousReceipt,'Saved previous ownership reference mismatch')
|
|
734
720
|
assert.equal(plan.protocol.scope,adapter.scope,'Saved adapter scope mismatch')
|
|
735
721
|
const fresh=await planDshInstallation(options)
|
|
722
|
+
if(fresh.previous)await dshOutputDirectory(output,{npmRoot,config,configPath,previousProductRoot:fresh.previous.product.npmRoot})
|
|
736
723
|
assert.deepEqual(plan.protocol,fresh.protocol,'Saved official pins/home/version mismatch')
|
|
737
724
|
assert.equal(plan.planSha256,fresh.planSha256,'Saved DSH plan or profile drifted')
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
725
|
+
const id=randomUUID(),receiptPath=join(output,id+'.dsh-result.json')
|
|
726
|
+
const preparedRecordFile=await writeDshOutput(output,id+'.dsh-prepared.json',{
|
|
727
|
+
schemaVersion:2,kind:'dsh-installation-prepared',id,planSha256:plan.planSha256,planSnapshot:plan,
|
|
728
|
+
receiptPath,ownershipEstablished:false,phase:'prepared',retrySafe:false})
|
|
729
|
+
try {
|
|
730
|
+
const result=await applyDshInstallation(plan,{adapter,signal,timeoutMs:dshInstallTimeoutMs,logDirectory:output})
|
|
731
|
+
const body={...result,id,receiptPath,preparedRecordFile}
|
|
732
|
+
const receipt={...body,receiptSha256:digest(body)}
|
|
733
|
+
const receiptFile=await writeDshOutput(output,id+'.dsh-result.json',receipt)
|
|
734
|
+
return {...receipt,receiptFile}
|
|
735
|
+
} catch(error) {
|
|
736
|
+
// The native command may already have changed the profile. Keep the exact
|
|
737
|
+
// prepared record discoverable without fabricating a successful terminal receipt.
|
|
738
|
+
throw Object.assign(error,{receiptPath:preparedRecordFile,preparedRecordFile,planFile:values.plan})
|
|
739
|
+
}
|
|
742
740
|
}
|
|
743
741
|
|
|
744
742
|
/** Reuse native session plugin loading. No registration, copying, or host launch. */
|
|
@@ -838,7 +836,7 @@ export async function installerMain(args,{npmRoot,userRoot=homedir(),adapter=cod
|
|
|
838
836
|
'--node-executable <path>':'QwenWork/WorkBuddy: absolute Windows Node path used by the imported suite.',
|
|
839
837
|
'--qwenwork-runtime <file>':'QwenWork: verified native executable/worker/Node paths and byte pins. Agent obtains these through supported native inspection; no credentials.',
|
|
840
838
|
'--separate-mcp':'QwenWork: emit one external connector instead of bundling it when organization policy requires this.',
|
|
841
|
-
'--previous-receipt <file>':'
|
|
839
|
+
'--previous-receipt <file>':'DSH: successful source-bound installation receipt for an owned update or repeat; sparse/manual receipts cannot be adopted. Desktop: reuse an owned preparation or prepare a newer native suite/npm version.',
|
|
842
840
|
'--npm-root-windows, --destination-windows':'Desktop: explicit Windows path mappings required when preparing from another platform; native Windows paths must match actual roots.',
|
|
843
841
|
},
|
|
844
842
|
limitations:['plugin-directory is an optional session-only diagnostic, not installation. Codex requires an admitted adapter; DSH requires pinned public CLI inputs. Desktop preparation needs native import and activation. Exit 2 means host selection or inputs are required; exit 0 does not establish a usable native session.',
|
|
@@ -853,7 +851,8 @@ export async function installerMain(args,{npmRoot,userRoot=homedir(),adapter=cod
|
|
|
853
851
|
desktopPrepare:nextArgs=>desktopPreparationMain(nextArgs,{npmRoot}),
|
|
854
852
|
desktopReuse:nextArgs=>desktopPreparationReuseMain(nextArgs,{npmRoot})})
|
|
855
853
|
}
|
|
856
|
-
for(const key of DESKTOP_INSTALL_OPTIONS)
|
|
854
|
+
for(const key of DESKTOP_INSTALL_OPTIONS)if(!(key==='previous-receipt'&&values.host==='dsh'&&positionals[0]==='install'))
|
|
855
|
+
assert.ok(!Object.hasOwn(values,key),`--${key} is only valid for direct desktop install`)
|
|
857
856
|
if(positionals[0]==='plugin-directory'&&['claude-code','qoder'].includes(values.host)) {
|
|
858
857
|
assert.deepEqual(Object.keys(values),['host'],'Native directory entry accepts only --host; transaction/runtime options must not be silently ignored')
|
|
859
858
|
return nativePluginDirectory({npmRoot,host:values.host})
|
|
@@ -884,7 +883,7 @@ export async function installerMain(args,{npmRoot,userRoot=homedir(),adapter=cod
|
|
|
884
883
|
if(values.apply)assert.ok(values.plan&&values['runtime-receipt']&&!values['receipt-dir']&&!values['disable-embedding'],'Claude apply requires saved plan and runtime receipt only')
|
|
885
884
|
else assert.ok(values['receipt-dir']&&!values.plan&&!values['runtime-receipt'],'Claude dry-run requires external receipt directory only')
|
|
886
885
|
} else assert.ok(values.receipt&&!values.apply&&!values['dry-run']&&!values.plan&&!values['receipt-dir']&&!values['runtime-receipt']&&!values['disable-embedding'],'Claude recovery/uninstall requires only persisted receipt')
|
|
887
|
-
const native=await claudeAdapterFactory({executable:values['claude-executable'],userRoot:profile,scope:values['dry-run']?'plan-only':'production'})
|
|
886
|
+
const native=await claudeAdapterFactory({executable:values['claude-executable'],userRoot:profile,nativeCwd:resolve(npmRoot),scope:values['dry-run']?'plan-only':'production'})
|
|
888
887
|
if(command==='recover'||command==='uninstall') {
|
|
889
888
|
const receipt=JSON.parse(await optional(resolve(values.receipt)))
|
|
890
889
|
assert.equal(receipt.npmRoot,resolve(npmRoot),'Claude receipt npm root mismatch');assert.equal(receipt.userRoot,profile,'Claude receipt profile mismatch')
|
|
@@ -8,7 +8,7 @@ export const AGENT_INSTALL_HOSTS = ['dsh', 'codex', 'claude-code', 'qoder', 'qwe
|
|
|
8
8
|
export const DESKTOP_INSTALL_OPTIONS = ['destination', 'node-executable', 'suite-version', 'npm-root-windows',
|
|
9
9
|
'destination-windows', 'qwenwork-runtime', 'separate-mcp', 'previous-receipt']
|
|
10
10
|
const nativeOptions = {
|
|
11
|
-
dsh: ['host', 'receipt-dir', 'dsh-config', 'profile', 'aggregate-selector', 'registry'],
|
|
11
|
+
dsh: ['host', 'receipt-dir', 'dsh-config', 'profile', 'aggregate-selector', 'registry', 'previous-receipt'],
|
|
12
12
|
codex: ['host', 'receipt-dir', 'codex-executable', 'user-root', 'disable-embedding'],
|
|
13
13
|
'claude-code': ['host', 'receipt-dir', 'claude-executable', 'user-root', 'disable-embedding'],
|
|
14
14
|
qoder: ['host', 'receipt-dir', 'qoder-executable', 'qoder-config-dir'],
|
|
@@ -57,34 +57,54 @@ async function executableFence(s) {
|
|
|
57
57
|
assert.ok((await lstat(exe)).isFile(),'Claude executable must remain regular')
|
|
58
58
|
assert.equal(sha(await readFile(exe)),s.identity.sha256,'Claude executable changed')
|
|
59
59
|
}
|
|
60
|
+
async function nativeCwdFence(s) {
|
|
61
|
+
const cwd=s.identity.nativeCwd
|
|
62
|
+
await assertRuntimePath(cwd)
|
|
63
|
+
assert.equal(await realpath(cwd),cwd,'Claude canonical native working directory changed')
|
|
64
|
+
const info=await lstat(cwd)
|
|
65
|
+
assert.ok(info.isDirectory(),'Claude native working directory must be a directory')
|
|
66
|
+
assert.deepEqual({dev:info.dev,ino:info.ino},s.nativeCwdIdentity,'Claude native working directory replaced')
|
|
67
|
+
assert.notEqual(cwd,s.userRoot,'Claude native working directory must differ from HOME')
|
|
68
|
+
for(const name of ['settings.json','settings.local.json']) {
|
|
69
|
+
const file=join(cwd,'.claude',name)
|
|
70
|
+
await assertRuntimePath(file)
|
|
71
|
+
try { await lstat(file);assert.fail('Claude native working directory contains direct .claude configuration') }
|
|
72
|
+
catch(error) { if(error.code!=='ENOENT')throw error }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
function protocolIdentity(s) { const {scope,...identity}=s.identity;return identity }
|
|
61
76
|
async function state(adapter) {
|
|
62
77
|
const s=adapters.get(adapter);assert.ok(s,'Unrecognized Claude adapter')
|
|
63
|
-
await executableFence(s);return s
|
|
78
|
+
await executableFence(s);await nativeCwdFence(s);return s
|
|
64
79
|
}
|
|
65
80
|
async function call(s,args) {
|
|
66
|
-
await executableFence(s)
|
|
81
|
+
await executableFence(s);await nativeCwdFence(s)
|
|
67
82
|
let result
|
|
68
|
-
try { result=await s.run(s.identity.executable,args,{cwd:s.
|
|
69
|
-
finally { await executableFence(s) }
|
|
83
|
+
try { result=await s.run(s.identity.executable,args,{cwd:s.identity.nativeCwd,env:s.env,timeout:30000,maxBuffer:1048576}) }
|
|
84
|
+
finally { await executableFence(s);await nativeCwdFence(s) }
|
|
70
85
|
assert.ok(result.code===undefined||result.code===0,'Claude command failed')
|
|
71
86
|
assert.ok(!result.signal,'Claude command signalled');return result.stdout??''
|
|
72
87
|
}
|
|
73
88
|
/** Read-only factory. Injected runners are confined to explicitly isolated profiles. */
|
|
74
|
-
export async function createClaudeNativeAdapter({executable,userRoot,scope='plan-only',run=execute}) {
|
|
89
|
+
export async function createClaudeNativeAdapter({executable,userRoot,nativeCwd,scope='plan-only',run=execute}) {
|
|
75
90
|
assert.ok(['plan-only','isolated-acceptance','isolated-test-contract','production'].includes(scope),'Unknown Claude scope')
|
|
76
91
|
assert.ok(isAbsolute(executable??'')&&isAbsolute(userRoot??''),'Explicit Claude executable/profile required')
|
|
92
|
+
assert.ok(isAbsolute(nativeCwd??''),'Explicit Claude native working directory required')
|
|
93
|
+
assert.equal(resolve(nativeCwd),nativeCwd,'Explicit canonical Claude native working directory required')
|
|
77
94
|
const exe=await realpath(executable),home=resolve(userRoot)
|
|
78
95
|
await assertRuntimePath(exe);await assertRuntimePath(home)
|
|
79
96
|
assert.equal(await realpath(home),home);assert.ok((await lstat(home)).isDirectory())
|
|
97
|
+
await assertRuntimePath(nativeCwd);assert.equal(await realpath(nativeCwd),nativeCwd,'Claude canonical native working directory changed')
|
|
98
|
+
const cwdInfo=await lstat(nativeCwd)
|
|
99
|
+
assert.ok(cwdInfo.isDirectory(),'Claude native working directory must be a directory')
|
|
80
100
|
if(scope.startsWith('isolated-'))assert.notEqual(home,resolve(homedir()),'Isolated Claude cannot use actual profile')
|
|
81
101
|
else { assert.equal(run,execute,'Real profile cannot use injected transport');assert.equal(home,resolve(homedir()),'Real profile identity required') }
|
|
82
|
-
const identity={executable:exe,sha256:sha(await readFile(exe)),userRoot:home,configDir:locations(home).config,platform:process.platform,arch:process.arch,scope}
|
|
102
|
+
const identity={executable:exe,sha256:sha(await readFile(exe)),userRoot:home,configDir:locations(home).config,nativeCwd,platform:process.platform,arch:process.arch,scope}
|
|
83
103
|
const config=identity.configDir
|
|
84
104
|
const env={HOME:home,CLAUDE_CONFIG_DIR:config,PATH:dirname(process.execPath)+':/usr/bin:/bin',
|
|
85
105
|
XDG_CONFIG_HOME:join(home,'.config'),XDG_CACHE_HOME:join(home,'.cache'),XDG_DATA_HOME:join(home,'.local/share'),
|
|
86
106
|
TMPDIR:join(config,'tmp'),LANG:'C.UTF-8',LC_ALL:'C.UTF-8',DISABLE_AUTOUPDATER:'1',NODE_OPTIONS:'',NODE_PATH:''}
|
|
87
|
-
const s={identity,userRoot:home,run,env}
|
|
107
|
+
const s={identity,userRoot:home,run,env,nativeCwdIdentity:{dev:cwdInfo.dev,ino:cwdInfo.ino}}
|
|
88
108
|
const version=(await call(s,['--version'])).trim().match(/^(\d+\.\d+\.\d+(?:[-+][\w.-]+)?) \(Claude Code\)$/)
|
|
89
109
|
assert.ok(version,'Expected Claude Code native executable');identity.version=version[1]
|
|
90
110
|
// Check the native interface we actually use, not one developer's binary hash.
|
|
@@ -169,6 +189,7 @@ async function lock(directory,id,fn,recovery=false) {
|
|
|
169
189
|
}
|
|
170
190
|
function validatePlan(plan) {
|
|
171
191
|
const {planSha256,...body}=plan;assert.equal(digest(body),planSha256,'Claude plan body digest mismatch')
|
|
192
|
+
assert.ok(typeof plan.protocol?.nativeCwd==='string','Claude protocol missing nativeCwd; use the retained installed implementation for its existing receipts')
|
|
172
193
|
assert.equal(plan.host,'claude-code');assert.equal(plan.scope,'user');pluginName(plan.selector)
|
|
173
194
|
for(const key of ['npmRoot','userRoot','receiptDirectory'])assert.ok(isAbsolute(plan[key])&&resolve(plan[key])===plan[key],'Noncanonical Claude plan path')
|
|
174
195
|
assert.ok(!beneath(plan.npmRoot,plan.receiptDirectory)&&!beneath(plan.paths.target,plan.receiptDirectory),'Receipt overlaps owned inputs')
|
|
@@ -98,6 +98,165 @@ async function checkPin(pin) {
|
|
|
98
98
|
assert.equal(sha(await regular(await realpath(pin.path))), pin.sha256, 'Official file pin changed')
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Only DSH consumes this local product mapping. Older candidates retain their
|
|
102
|
+
// explicit selector/registry route; preparation values never imply a release.
|
|
103
|
+
export function validateReleaseHosts(config) {
|
|
104
|
+
assert.ok(config && typeof config==='object' && !Array.isArray(config),'Release hosts object required')
|
|
105
|
+
assert.deepEqual(Object.keys(config).sort(),['hosts','schemaVersion'],'Unknown release hosts fields')
|
|
106
|
+
assert.equal(config.schemaVersion,1,'Unsupported release hosts schema')
|
|
107
|
+
assert.ok(config.hosts && typeof config.hosts==='object' && !Array.isArray(config.hosts),'Release hosts map required')
|
|
108
|
+
assert.deepEqual(Object.keys(config.hosts),['dsh'],'Release mapping currently supports DSH only')
|
|
109
|
+
const dsh=config.hosts.dsh
|
|
110
|
+
assert.ok(dsh && typeof dsh==='object' && !Array.isArray(dsh),'DSH release mapping required')
|
|
111
|
+
assert.deepEqual(Object.keys(dsh).sort(),['aggregatePackage','aggregateVersion','registry'],'Unknown DSH release fields')
|
|
112
|
+
assert.match(dsh.aggregatePackage ?? '',/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u,'Exact aggregate package name required')
|
|
113
|
+
if(dsh.aggregateVersion===null && dsh.registry===null)return null
|
|
114
|
+
assert.match(dsh.aggregateVersion ?? '',/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,'Exact aggregate version required')
|
|
115
|
+
assert.equal(typeof dsh.registry,'string','Bound HTTPS registry required')
|
|
116
|
+
const url=new URL(dsh.registry)
|
|
117
|
+
assert.ok(url.protocol==='https:' && !url.username && !url.password && !url.search && !url.hash,'Credential-free HTTPS registry required')
|
|
118
|
+
return {aggregateSelector:`${dsh.aggregatePackage}@${dsh.aggregateVersion}`,registry:url.href}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function boundedFile(path) {
|
|
122
|
+
assert.ok(isAbsolute(path), 'Explicit absolute ownership file required')
|
|
123
|
+
await safeDirectory(dirname(path))
|
|
124
|
+
const info = await lstat(path)
|
|
125
|
+
assert.ok(info.isFile() && info.size <= 1024 * 1024, 'Bounded regular ownership file required')
|
|
126
|
+
return regular(path)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Pin the acquired product, not an aggregate that a registry operation has yet to acquire. */
|
|
130
|
+
async function productIdentity(root) {
|
|
131
|
+
if (root === undefined) return null
|
|
132
|
+
assert.ok(isAbsolute(root), 'Explicit absolute acquired product required')
|
|
133
|
+
await safeDirectory(root)
|
|
134
|
+
const manifestBytes = await boundedFile(join(root, 'package.json')), manifest = JSON.parse(manifestBytes)
|
|
135
|
+
assert.equal(typeof manifest.name, 'string'); assert.equal(typeof manifest.version, 'string')
|
|
136
|
+
let releaseMap = null
|
|
137
|
+
try { releaseMap = await boundedFile(join(root, 'release-hosts.json')) }
|
|
138
|
+
catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
139
|
+
const entries = {}
|
|
140
|
+
for (const file of ['design-system.mjs', 'installer.mjs', 'npm-installer-dsh.mjs', 'npm-agent-installation.mjs'])
|
|
141
|
+
entries[file] = sha(await boundedFile(join(root, 'bin', file)))
|
|
142
|
+
return { npmRoot: root, name: manifest.name, version: manifest.version, manifestSha256: sha(manifestBytes),
|
|
143
|
+
releaseMapSha256: releaseMap === null ? null : sha(releaseMap),
|
|
144
|
+
releaseSelection: releaseMap === null ? null : validateReleaseHosts(JSON.parse(releaseMap)), entries }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function expectedSource(input) {
|
|
148
|
+
return { kind: input.aggregateDirectory ? 'retained-link' : 'registry-selector',
|
|
149
|
+
aggregateSelector: input.aggregateSelector, registry: input.registry,
|
|
150
|
+
...(input.aggregateDirectory ? { aggregateDirectory: input.aggregateDirectory,
|
|
151
|
+
aggregateManifestSha256: input.aggregateManifestSha256, aggregatePatchSha256: input.aggregatePatchSha256 } : {}) }
|
|
152
|
+
}
|
|
153
|
+
function ownedState(current) {
|
|
154
|
+
return { dependency: current.dependency, installed: current.installed, registered: current.registered }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ownerReference(record) {
|
|
158
|
+
return { receiptFile: record.path, receiptSha256: sha(record.bytes), planSha256: record.plan.planSha256,
|
|
159
|
+
product: record.plan.product, owned: record.receipt.source.observed }
|
|
160
|
+
}
|
|
161
|
+
function assertOwnerReference(reference) {
|
|
162
|
+
assert.ok(reference && typeof reference === 'object', 'Actual native owner reference required')
|
|
163
|
+
assert.ok(typeof reference.receiptFile === 'string' && isAbsolute(reference.receiptFile), 'Actual native owner file required')
|
|
164
|
+
for (const key of ['receiptSha256', 'planSha256']) assert.match(reference[key] ?? '', /^[a-f0-9]{64}$/u, 'Native owner hash required')
|
|
165
|
+
assert.ok(reference.product && reference.owned?.installed && reference.owned.registered, 'Native owner identity required')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// This validates a saved fact without reading historical product directories or
|
|
169
|
+
// following its earlier owner. Only the selected current product is reread below.
|
|
170
|
+
async function readOwnedReceipt(path, state, profile, currentProduct) {
|
|
171
|
+
assert.ok(typeof path === 'string' && isAbsolute(path), 'Explicit absolute previous receipt required')
|
|
172
|
+
for (const root of [state.dshHome, currentProduct.npmRoot, ...Object.values(state.pins).map(pin => dirname(pin.path))])
|
|
173
|
+
assert.ok(!beneath(root, path), 'Previous receipt overlaps host or product inputs')
|
|
174
|
+
if (process.platform === 'win32') await assertWindowsPrivate(dirname(path), { directory: true })
|
|
175
|
+
const bytes = await boundedFile(path), receipt = JSON.parse(bytes)
|
|
176
|
+
const { receiptSha256, ...body } = receipt
|
|
177
|
+
assert.equal(receipt.schemaVersion, 2, 'Ownership proof required; sparse or manual receipts cannot be adopted')
|
|
178
|
+
assert.equal(digest(body), receiptSha256, 'Previous receipt digest mismatch')
|
|
179
|
+
assert.equal(receipt.kind, 'dsh-installation'); assert.equal(receipt.host, 'dsh')
|
|
180
|
+
assert.equal(receipt.route, 'official-dsh-bundle'); assert.equal(receipt.scope, state.identity.scope)
|
|
181
|
+
assert.match(receipt.id ?? '', /^[0-9a-f-]{36}$/u)
|
|
182
|
+
assert.equal(receipt.receiptPath, path)
|
|
183
|
+
assert.equal(join(dirname(path), receipt.id + '.dsh-result.json'), path)
|
|
184
|
+
assert.equal(receipt.status, 'registered-config-parsed', 'Successful owned installation required')
|
|
185
|
+
assert.equal(receipt.ownershipEstablished, true, 'Previous installation ownership was not established')
|
|
186
|
+
assert.equal(receipt.exitCode, 0); assert.equal(receipt.partialStateRetained, false)
|
|
187
|
+
assert.equal(receipt.userConfigurationPreserved, true)
|
|
188
|
+
const old = receipt.planSnapshot, { planSha256, ...unsigned } = old ?? {}
|
|
189
|
+
assert.equal(planSha256, receipt.planSha256); assert.equal(digest(unsigned), planSha256, 'Previous plan digest mismatch')
|
|
190
|
+
assert.deepEqual(old.protocol, state.identity, 'Previous official profile or protocol differs')
|
|
191
|
+
assert.equal(old.profile, profile, 'Previous profile differs')
|
|
192
|
+
assert.deepEqual(receipt.product, old.product, 'Previous product identity mismatch')
|
|
193
|
+
assert.ok(old.product?.releaseSelection, 'Previous product was not release-bound')
|
|
194
|
+
assert.ok(!beneath(old.product.npmRoot, path), 'Previous receipt overlaps its acquired product')
|
|
195
|
+
const selected = await selectorInput({ profile: old.profile, aggregateSelector: old.aggregateSelector, registry: old.registry })
|
|
196
|
+
assert.deepEqual(old.package, selected.package, 'Previous selected package differs')
|
|
197
|
+
assert.deepEqual(old.product.releaseSelection, { aggregateSelector: selected.aggregateSelector, registry: selected.registry },
|
|
198
|
+
'Previous product selection mismatch')
|
|
199
|
+
assert.deepEqual(receipt.source?.expected, expectedSource(old), 'Previous selected source differs')
|
|
200
|
+
assert.deepEqual(receipt.source?.observed, ownedState(receipt.readback), 'Previous installed source differs')
|
|
201
|
+
assert.equal(receipt.readback?.configurationParsed, true)
|
|
202
|
+
assert.equal(receipt.readback?.packageInstalled, true); assert.equal(receipt.readback?.bundleRegistered, true)
|
|
203
|
+
assert.equal(receipt.readback.installed?.name, selected.package.name)
|
|
204
|
+
assert.equal(receipt.readback.installed?.version, selected.package.version)
|
|
205
|
+
assert.equal(receipt.readback.registered, true)
|
|
206
|
+
if (old.aggregateDirectory) {
|
|
207
|
+
assert.equal(receipt.readback.installed.realPackageDir, old.aggregateDirectory)
|
|
208
|
+
assert.ok(receipt.readback.dependency.startsWith('link:'))
|
|
209
|
+
} else assert.equal(receipt.readback.dependency, selected.package.version)
|
|
210
|
+
return { path, bytes, receipt, plan: old }
|
|
211
|
+
}
|
|
212
|
+
function assertNativeOwner(record) {
|
|
213
|
+
const { receipt, plan } = record
|
|
214
|
+
assert.equal(receipt.execution?.state, 'completed', 'Owner must be an actual completed native operation')
|
|
215
|
+
assert.equal(receipt.execution.exitCode, 0, 'Native owner must have exited successfully')
|
|
216
|
+
if (plan.action === 'install') {
|
|
217
|
+
assert.ok(!plan.before.installed && !plan.before.registered && plan.before.dependency === null,
|
|
218
|
+
'Native install owner must start with an absent aggregate')
|
|
219
|
+
} else {
|
|
220
|
+
assert.equal(plan.action, 'update', 'A no-command observation is not a native owner')
|
|
221
|
+
assertOwnerReference(plan.previous)
|
|
222
|
+
assert.deepEqual(ownedState(plan.before), plan.previous.owned, 'Native update before-state differs from its owner')
|
|
223
|
+
assert.notEqual(plan.before.installed.version, receipt.readback.installed.version)
|
|
224
|
+
assert.notEqual(plan.previous.product.version, plan.product.version)
|
|
225
|
+
assert.equal(plan.previous.product.name, plan.product.name)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function previousOwnership(path, state, input, product) {
|
|
230
|
+
assert.ok(product?.releaseSelection, 'Owned update requires a release-bound acquired product')
|
|
231
|
+
const selected = await readOwnedReceipt(path, state, input.profile, product)
|
|
232
|
+
let anchor = selected
|
|
233
|
+
if (selected.plan.action !== 'install') {
|
|
234
|
+
const reference = selected.plan.previous
|
|
235
|
+
assertOwnerReference(reference)
|
|
236
|
+
// Read exactly one native anchor. Never follow its own earlier reference.
|
|
237
|
+
const native = await readOwnedReceipt(reference.receiptFile, state, input.profile, product)
|
|
238
|
+
assert.deepEqual(ownerReference(native), reference, 'Native owner reference changed')
|
|
239
|
+
assertNativeOwner(native)
|
|
240
|
+
assert.deepEqual(ownedState(selected.plan.before), reference.owned, 'Previous before-state differs from its native owner')
|
|
241
|
+
if (selected.plan.action === 'already-registered') {
|
|
242
|
+
assert.equal(selected.receipt.execution?.state, 'not-needed')
|
|
243
|
+
assert.deepEqual(selected.plan.product, native.plan.product, 'Observation product differs from its native owner')
|
|
244
|
+
assert.deepEqual(selected.receipt.source, native.receipt.source, 'Observation source differs from its native owner')
|
|
245
|
+
anchor = native
|
|
246
|
+
} else assertNativeOwner(selected)
|
|
247
|
+
} else assertNativeOwner(selected)
|
|
248
|
+
assert.deepEqual(await productIdentity(selected.plan.product.npmRoot), selected.plan.product, 'Previous acquired product changed')
|
|
249
|
+
assert.equal(selected.plan.product.name, product.name, 'Previous product name differs')
|
|
250
|
+
const oldInput = await selectorInput(selected.plan)
|
|
251
|
+
assert.equal(oldInput.package.name, input.package.name, 'Previous aggregate name differs')
|
|
252
|
+
assert.equal(oldInput.registry, input.registry, 'Registry migration requires a separate route')
|
|
253
|
+
assert.equal(Boolean(oldInput.aggregateDirectory), Boolean(input.aggregateDirectory), 'Source-kind migration requires a separate route')
|
|
254
|
+
if (oldInput.package.version === input.package.version)
|
|
255
|
+
assert.deepEqual(product, selected.plan.product, 'Same-version relocation is not an owned update')
|
|
256
|
+
else assert.notEqual(product.version, selected.plan.product.version, 'An update requires a new exact product version')
|
|
257
|
+
return { input: oldInput, inputSha256: sha(selected.bytes), reference: ownerReference(anchor) }
|
|
258
|
+
}
|
|
259
|
+
|
|
101
260
|
/** Pins are caller-selected release inputs, not host discovery or publication proof. */
|
|
102
261
|
export async function createDshInstallerAdapter({ scope = 'official-pinned', dshHome,
|
|
103
262
|
node, cli, hostManifest, loader, loaderManifest, desktopHostManifest, hostVersion, loaderVersion,
|
|
@@ -204,6 +363,8 @@ async function snapshot(state, input) {
|
|
|
204
363
|
}
|
|
205
364
|
let installed = null
|
|
206
365
|
if (packageDir !== null) {
|
|
366
|
+
const realPackageDir = await realpath(packageDir)
|
|
367
|
+
assert.equal(await realpath(join(dir, 'node_modules', name)), realPackageDir, 'Official resolution is not the direct profile package')
|
|
207
368
|
if (input.aggregateDirectory) assert.equal(await realpath(packageDir), input.aggregateDirectory, 'Resolved aggregate differs from retained installation')
|
|
208
369
|
const bytes = await regular(join(packageDir, 'package.json')), value = JSON.parse(bytes)
|
|
209
370
|
assert.equal(value.name, name, 'Resolved package identity conflict')
|
|
@@ -214,22 +375,34 @@ async function snapshot(state, input) {
|
|
|
214
375
|
const patchPath = resolve(packageDir, patch)
|
|
215
376
|
assert.ok(beneath(resolve(packageDir), patchPath), 'Aggregate patch escapes package')
|
|
216
377
|
assert.ok(beneath(await realpath(packageDir), await realpath(patchPath)), 'Aggregate patch resolves outside package')
|
|
217
|
-
installed = { name, version: value.version, packageDir, manifestSha256: sha(bytes), patchPath, patchSha256: sha(await regular(patchPath)) }
|
|
378
|
+
installed = { name, version: value.version, packageDir, realPackageDir, manifestSha256: sha(bytes), patchPath, patchSha256: sha(await regular(patchPath)) }
|
|
218
379
|
} else assert.equal(dependency, undefined, 'Existing aggregate dependency is unresolved; repair separately')
|
|
380
|
+
const lock = await regular(join(dir, 'pnpm-lock.yaml'), true)
|
|
219
381
|
return { dir, manifestSha256: sha(manifestBytes), foreignSha256: digest(foreignManifest(manifest, name)),
|
|
220
|
-
patches, configPins, installed,
|
|
382
|
+
patches, configPins, installed, dependency: dependency ?? null, bundles: [...bundles],
|
|
383
|
+
lockfileSha256: lock === null ? null : sha(lock), registered: bundles.includes(name) }
|
|
221
384
|
}
|
|
222
385
|
|
|
223
386
|
/** Read-only plan. Does not initialize profiles, install packages or write configuration. */
|
|
224
387
|
export async function planDshInstallation(options) {
|
|
225
388
|
const state = await protocol(options.adapter), input = await selectorInput(options)
|
|
226
389
|
if (state.pins.desktopHostManifest) assert.equal(input.profile, 'desktop', 'Desktop registration requires its reserved profile')
|
|
227
|
-
const
|
|
228
|
-
|
|
390
|
+
const product = await productIdentity(options.productRoot ?? options.product?.npmRoot)
|
|
391
|
+
if (product?.releaseSelection) {
|
|
392
|
+
assert.equal(input.aggregateSelector, product.releaseSelection.aggregateSelector, 'Selected aggregate differs from product release')
|
|
393
|
+
assert.equal(input.registry, product.releaseSelection.registry, 'Selected registry differs from product release')
|
|
394
|
+
}
|
|
395
|
+
const previous = options.previousReceipt === undefined ? null
|
|
396
|
+
: await previousOwnership(options.previousReceipt, state, input, product)
|
|
397
|
+
const before = await snapshot(state, previous?.input ?? input)
|
|
398
|
+
if (previous) assert.deepEqual(ownedState(before), previous.reference.owned, 'Owned A installation changed')
|
|
399
|
+
const plan = { schemaVersion: 2, host: 'dsh', route: 'official-dsh-bundle', ...input, product,
|
|
400
|
+
...(previous ? { previousReceipt: options.previousReceipt, previousInputSha256: previous.inputSha256, previous: previous.reference } : {}),
|
|
229
401
|
protocol: structuredClone(state.identity), before, command: state.pins.node.path,
|
|
230
402
|
// Official profiles contain pnpm-workspace.yaml; explicitly target their root.
|
|
231
403
|
args: [...state.pins.desktopHostManifest ? ['--expose-internals'] : [], state.pins.cli.path, 'plugin', '--profile', input.profile, 'add', '--workspace-root', '--save-exact', input.aggregateDirectory ? 'link:' + input.aggregateDirectory : input.aggregateSelector, '--registry', input.registry],
|
|
232
|
-
action:
|
|
404
|
+
action: previous && previous.input.package.version !== input.package.version ? 'update'
|
|
405
|
+
: before.installed && before.registered ? 'already-registered' : 'install',
|
|
233
406
|
runtimeMounted: false, bundleCycleVerified: false, releaseValidated: false }
|
|
234
407
|
return { ...plan, planSha256: digest(plan) }
|
|
235
408
|
}
|
|
@@ -271,6 +444,7 @@ export async function inspectDshInstallation(options) {
|
|
|
271
444
|
|
|
272
445
|
/** Runs only the pinned public CLI; retains partial state on failure, never rolls back. */
|
|
273
446
|
export async function applyDshInstallation(plan, { adapter, signal, timeoutMs = 0, logDirectory } = {}) {
|
|
447
|
+
plan = structuredClone(plan)
|
|
274
448
|
const { planSha256, ...unsigned } = plan
|
|
275
449
|
assert.equal(digest(unsigned), planSha256, 'Plan or profile changed; replan before execution')
|
|
276
450
|
const state = await protocol(adapter)
|
|
@@ -278,14 +452,17 @@ export async function applyDshInstallation(plan, { adapter, signal, timeoutMs =
|
|
|
278
452
|
assert.equal(fresh.planSha256, plan.planSha256, 'Plan or profile changed; replan before execution')
|
|
279
453
|
if (logDirectory !== undefined) {
|
|
280
454
|
await safeDirectory(logDirectory)
|
|
281
|
-
for (const root of [state.dshHome,
|
|
455
|
+
for (const root of [state.dshHome, plan.product?.npmRoot, plan.previous?.product.npmRoot,
|
|
456
|
+
...Object.values(state.pins).map(pin => dirname(pin.path))].filter(Boolean))
|
|
282
457
|
assert.ok(!beneath(root, logDirectory) && !beneath(logDirectory, root), 'Command logs overlap host inputs')
|
|
283
458
|
if (process.platform === 'win32') await assertWindowsPrivate(logDirectory, { directory: true })
|
|
284
459
|
}
|
|
285
460
|
let exitCode = 0, executionError = null, execution = { state: 'not-needed', retrySafe: false }, diagnosticError = null
|
|
286
461
|
if (fresh.action !== 'already-registered') {
|
|
287
462
|
const command = await executeDshInstall(fresh.command, fresh.args, { cwd: fresh.before.dir,
|
|
288
|
-
|
|
463
|
+
// ORT's supported policy skips its extra CUDA download, not dependency lifecycle scripts.
|
|
464
|
+
env: { ...process.env, ONNXRUNTIME_NODE_INSTALL_CUDA: 'skip', DSH_HOME: state.dshHome,
|
|
465
|
+
...(state.pins.desktopHostManifest ? { ELECTRON_RUN_AS_NODE: '1' } : {}) } },
|
|
289
466
|
{ run: state.run, signal, timeoutMs })
|
|
290
467
|
;({ exitCode, executionError, execution } = command)
|
|
291
468
|
try { await retainCommandOutput(logDirectory, command) }
|
|
@@ -296,11 +473,17 @@ export async function applyDshInstallation(plan, { adapter, signal, timeoutMs =
|
|
|
296
473
|
readback = await inspectDshInstallation({ ...plan, adapter })
|
|
297
474
|
userConfigurationPreserved = readback.foreignSha256 === plan.before.foreignSha256
|
|
298
475
|
&& digest(readback.patches) === digest(plan.before.patches) && digest(readback.configPins) === digest(plan.before.configPins)
|
|
476
|
+
&& (plan.action !== 'update' || digest(readback.bundles) === digest(plan.before.bundles))
|
|
299
477
|
} catch (error) { readbackError = error.message }
|
|
300
478
|
const succeeded = exitCode === 0 && diagnosticError === null && userConfigurationPreserved && readback?.configurationParsed === true
|
|
301
|
-
|
|
479
|
+
const ownershipEstablished = Boolean(succeeded && plan.product?.releaseSelection && (plan.previous
|
|
480
|
+
|| plan.action === 'install' && !plan.before.installed && !plan.before.registered && execution.state === 'completed'))
|
|
481
|
+
return { schemaVersion: 2, kind: 'dsh-installation', host: 'dsh', route: 'official-dsh-bundle',
|
|
482
|
+
product: plan.product, planSnapshot: plan, planSha256: plan.planSha256, operation: plan.action,
|
|
483
|
+
ownershipEstablished, source: { expected: expectedSource(plan), observed: readback === null ? null : ownedState(readback) },
|
|
484
|
+
registryAcquisitionVerified: false, scope: adapter.scope, status: succeeded ? 'registered-config-parsed' : 'failed-partial-state-retained',
|
|
302
485
|
exitCode, executionError, execution, diagnosticError, readbackError, readback, userConfigurationPreserved,
|
|
303
486
|
userConfigurationStatus: readback === null ? 'unverified' : userConfigurationPreserved ? 'observed-matching' : 'changed',
|
|
304
|
-
rollbackAttempted: false, partialStateRetained: !succeeded,
|
|
487
|
+
rollbackAttempted: false, rollbackScope: 'adapter-only; official CLI may perform limited compatibility restoration', partialStateRetained: !succeeded,
|
|
305
488
|
runtimeMounted: false, bundleCycleVerified: false, releaseValidated: false }
|
|
306
489
|
}
|
package/package.json
CHANGED