@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Yugoo Design Companion — beta
2
2
 
3
- Package: `@yugoo-design/companion@0.1.0-beta.3`.
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. This source
15
- candidate uses the packaged native Git library for runtime versioning; system Git
16
- is needed only for the explicit CLI diagnostic backend and development fixtures.
17
- The installed native binding must support the actual operating system and CPU;
18
- missing support is reported rather than silently spawning system Git. Resolve the
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.3 --registry=https://registry.npmjs.org
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
- // Only DSH consumes this local product mapping. Older candidates retain their
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,...aggregateDirectory ? {aggregateDirectory} : {}}
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 result=await applyDshInstallation(plan,{adapter,signal,timeoutMs:dshInstallTimeoutMs,logDirectory:output})
739
- const receipt={...result,host:'dsh',route:'official-dsh-bundle',planSha256:plan.planSha256}
740
- const receiptFile=await writeDshOutput(output,randomUUID()+'.dsh-result.json',receipt)
741
- return {...receipt,receiptFile}
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>':'Desktop: verify and reuse this owned preparation for a same-version repeat, or prepare a strictly newer native suite/npm version using its update boundary.',
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)assert.ok(!Object.hasOwn(values,key),`--${key} is only valid for direct desktop install`)
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.userRoot,env:s.env,timeout:30000,maxBuffer:1048576}) }
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, registered: bundles.includes(name) }
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 before = await snapshot(state, input)
228
- const plan = { schemaVersion: 1, host: 'dsh', route: 'official-dsh-bundle', ...input,
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: before.installed && before.registered ? 'already-registered' : 'install',
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, ...Object.values(state.pins).map(pin => dirname(pin.path))])
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
- env: { ...process.env, DSH_HOME: state.dshHome, ...(state.pins.desktopHostManifest ? { ELECTRON_RUN_AS_NODE: '1' } : {}) } },
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
- return { scope: adapter.scope, status: succeeded ? 'registered-config-parsed' : 'failed-partial-state-retained',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yugoo-design/companion",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Yugoo Design Companion beta: interactive Design Projects and Panels in supported native Agent Hosts.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "yugoo-design",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Yugoo Design: interactive Design Panels with Claude Code native agents and background design reviews."
5
5
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yugoo-design",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Yugoo Design: local Design Project Panels for an explicitly bound Codex conversation (development preview).",
5
5
  "author": {
6
6
  "name": "yugoo-design contributors"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "yugoo-design",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Yugoo Design: interactive Design Panels with Qoder-native Producers, Workers and background reviews."
5
5
  }