@hanamesh/dsh-app-host 0.6.20 → 0.6.22
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 +8 -4
- package/dist/client-ui.d.ts +4 -1
- package/dist/client-ui.js +27 -31
- package/dist/dsh.d.ts +1 -1
- package/dist/dsh.js +4 -6
- package/dist/install-target/fixtures/catalog.json +99 -8
- package/dist/install-target/fixtures/install-failure-cases.json +385 -35
- package/dist/install-target/fixtures/reference-consumer.js +1 -1
- package/dist/install-target/suite.d.ts +2 -1
- package/dist/install-target/suite.js +28 -8
- package/dist/library/catalog.js +24 -7
- package/dist/library/diagnostics.js +0 -1
- package/dist/library/github-target.js +42 -0
- package/dist/library/handoff.js +5 -2
- package/dist/library/install.js +27 -64
- package/dist/library/locate.js +4 -13
- package/dist/library/recovery.js +5 -3
- package/dist/library/routes.js +1 -0
- package/dist/library/service.js +35 -19
- package/dist/library/suite.js +9 -10
- package/docs/APP_PACKAGE.md +1 -1
- package/docs/CONTRACT.md +6 -6
- package/docs/DSH_COMPATIBILITY.md +1 -1
- package/docs/INSTALL_TARGET.md +22 -6
- package/docs/LIBRARY.md +4 -15
- package/docs/LICENSES.json +6 -6
- package/docs/SECURITY.md +2 -0
- package/node_modules/@hanamesh/lib-provision/bin/hanamesh-provision.js +0 -0
- package/package.json +27 -24
- package/schemas/catalog-provider-page.schema.json +97 -10
- package/schemas/install-target.schema.json +252 -38
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { requireCondition } from '../errors.js';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
|
|
4
|
+
const npmName=value=>typeof value==='string'&&value.length<=214&&/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
|
|
5
|
+
const own=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).every(key=>keys.includes(key));
|
|
6
|
+
|
|
7
|
+
/** Encode a reviewed immutable target for the official DSH CLI's native pnpm Git path. */
|
|
8
|
+
export function pinnedGithubSpec(repository,commit,subdirectory){
|
|
9
|
+
requireCondition(typeof repository==='string'&&/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[A-Za-z0-9_][A-Za-z0-9._-]*$/.test(repository),
|
|
10
|
+
'INVALID_INPUT','GitHub installation requires an owner/repository identity.');
|
|
11
|
+
requireCondition(typeof commit==='string'&&/^[0-9a-f]{40}$/i.test(commit),'INVALID_INPUT','GitHub installation requires a full commit SHA.');
|
|
12
|
+
requireCondition(subdirectory===undefined||subdirectory===null||(typeof subdirectory==='string'&&subdirectory.length>0&&
|
|
13
|
+
subdirectory.split('/').every(part=>part!=='.'&&part!=='..'&&/^[A-Za-z0-9_@+.-]+$/.test(part))),
|
|
14
|
+
'INVALID_INPUT','GitHub installation requires a relative repository subdirectory.');
|
|
15
|
+
return `github:${repository}#${commit.toLowerCase()}${subdirectory?'&path:'+subdirectory:''}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The Indexer's package target is a data selector, never an executable command. */
|
|
19
|
+
export function packageInstallTarget(value){
|
|
20
|
+
const github=value?.source==='github';
|
|
21
|
+
requireCondition(own(value,github?['source','repo','ref','path','packageName','allowBuild']:['source','name','version','allowBuild'])&&
|
|
22
|
+
(github||value.source==='npm')&&npmName(github?value.packageName:value.name)&&
|
|
23
|
+
Array.isArray(value.allowBuild)&&value.allowBuild.every(entry=>typeof entry==='string'&&entry.length>0&&entry.length<=2048&&!/[\s\u0000-\u001f\u007f-\u009f]/u.test(entry)),
|
|
24
|
+
'INVALID_INPUT','Invalid catalog package installation target.');
|
|
25
|
+
if(github){
|
|
26
|
+
requireCondition(value.path===null||typeof value.path==='string','INVALID_INPUT','GitHub path must be a relative path or null.');
|
|
27
|
+
pinnedGithubSpec(value.repo,value.ref,value.path);
|
|
28
|
+
return{source:'github',repo:value.repo,ref:value.ref.toLowerCase(),path:value.path,packageName:value.packageName,allowBuild:[...value.allowBuild]};
|
|
29
|
+
}
|
|
30
|
+
requireCondition(typeof value.version==='string'&&value.version.length<=128&&semver.valid(value.version)===value.version,
|
|
31
|
+
'INVALID_INPUT','npm target version must be the exact version fixed at collection.');
|
|
32
|
+
return{source:'npm',name:value.name,version:value.version,allowBuild:[...value.allowBuild]};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const installTargetName=target=>target.source==='github'?target.packageName:target.name;
|
|
36
|
+
export const sameInstallTarget=(left,right)=>JSON.stringify(packageInstallTarget(left))===JSON.stringify(packageInstallTarget(right));
|
|
37
|
+
export const catalogInstallationAllowed=item=>Array.isArray(item?.installMethods)&&item?.oneClickInstall?.allowed===true;
|
|
38
|
+
export function catalogPackageTargets(item){
|
|
39
|
+
if(!catalogInstallationAllowed(item))return[];
|
|
40
|
+
return (item.installMethods??[]).filter(method=>['hanamesh','dsh'].includes(method.channel)&&method.target)
|
|
41
|
+
.map(method=>packageInstallTarget(method.target));
|
|
42
|
+
}
|
package/dist/library/handoff.js
CHANGED
|
@@ -14,6 +14,7 @@ import { installationStatusCors, serveInstallationStatus } from './installation-
|
|
|
14
14
|
*/
|
|
15
15
|
import { createServer } from 'node:http';
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import {sameInstallTarget} from './github-target.js';
|
|
17
18
|
import { AppHostError, requireCondition } from '../errors.js';
|
|
18
19
|
|
|
19
20
|
export const HANDOFF_STATES = Object.freeze(['pending', 'shown', 'confirmed', 'installed', 'cancelled', 'failed']);
|
|
@@ -35,7 +36,7 @@ export const HANDOFF_RULES = Object.freeze([
|
|
|
35
36
|
{ rule: '只有「允许交接的网站」里的网站能探测本客户端、交接安装、读交接状态;别的网站什么也读不到', why: '否则任何网页都能知道你装了什么客户端、缺什么,用来识别你' },
|
|
36
37
|
{ rule: '网站交接只会在应用库里弹出确认;只有你点「确认安装」才会安装', why: '网站不能替你决定装什么' },
|
|
37
38
|
{ rule: '交接状态只给发起它的那个网站看', why: '一个网站不能读另一个网站的交接' },
|
|
38
|
-
{ rule: `交接请求体不超过 ${HANDOFF_BODY_MAX_BYTES} 字节`, why: '
|
|
39
|
+
{ rule: `交接请求体不超过 ${HANDOFF_BODY_MAX_BYTES} 字节`, why: '交接只包含目录身份、合约版本及已审核的结构化安装目标;正文上限为 4096 字节,不接受来源命令' },
|
|
39
40
|
{ rule: '端口在 1024–65535 之间、互不重复,至少一个', why: '1024 以下要管理员权限;没有端口就没有地方让网站找到' },
|
|
40
41
|
{ rule: '网站必须写成来源(协议 + 域名 [+ 端口]),http 只允许 127.0.0.1 / localhost', why: '浏览器按来源校验;明文 http 的公网网站可被中间人冒充' },
|
|
41
42
|
{ rule: `轮询间隔是 1–${TIMER_MAX_MS} 毫秒的整数,作废时间必须大于轮询间隔`, why: '计时器超过上限会立即触发;作废早于第一次轮询,交接永远不会被看到' },
|
|
@@ -75,6 +76,7 @@ export function createHandoffQueue({ ttlMs = () => HANDOFF_DEFAULTS.ttlMs, now =
|
|
|
75
76
|
const publish = record => { for (const listener of listeners) { try { listener(view(record)); } catch (error) { onError('hanamesh-app-host handoff: subscriber failed', error); } } };
|
|
76
77
|
const view = record => ({ handoffId: record.handoffId, state: record.state, origin: record.origin,
|
|
77
78
|
...(record.itemId ? { itemId: record.itemId } : {}), ...(record.packageName ? { packageName: record.packageName } : {}),
|
|
79
|
+
...(record.target.installTarget ? {installTarget:structuredClone(record.target.installTarget)} : {}),
|
|
78
80
|
...(record.displayName ? { displayName: record.displayName } : {}), ...(record.result ? { result: record.result } : {}),
|
|
79
81
|
...(record.error ? { error: record.error } : {}), createdAt: record.createdAt, updatedAt: record.updatedAt });
|
|
80
82
|
const move = (record, state, extra = {}) => {
|
|
@@ -97,7 +99,7 @@ export function createHandoffQueue({ ttlMs = () => HANDOFF_DEFAULTS.ttlMs, now =
|
|
|
97
99
|
return Object.freeze({
|
|
98
100
|
create({ origin, target, item, alreadyInstalled }) {
|
|
99
101
|
expire();
|
|
100
|
-
const at = now(), record = { handoffId: randomUUID(), state: 'pending', origin, target,
|
|
102
|
+
const at = now(), record = { handoffId: randomUUID(), state: 'pending', origin, target:structuredClone({...target,...(item.installTarget?{installTarget:item.installTarget}:{})}),
|
|
101
103
|
itemId: item.id, packageName: item.package?.name, displayName: item.displayName, createdAt: at, updatedAt: at };
|
|
102
104
|
records.set(record.handoffId, record);
|
|
103
105
|
if (alreadyInstalled) move(record, 'installed', { result: { status: 'already-installed' } }); else publish(record);
|
|
@@ -132,6 +134,7 @@ export function createHandoffQueue({ ttlMs = () => HANDOFF_DEFAULTS.ttlMs, now =
|
|
|
132
134
|
const name = event?.packageName ?? event?.result?.packageName; if (!name) return;
|
|
133
135
|
for (const record of records.values()) {
|
|
134
136
|
if (record.packageName !== name || !['shown', 'confirmed'].includes(record.state)) continue;
|
|
137
|
+
if(record.target.installTarget&&(!event.installTarget&&!event.result?.installTarget||!sameInstallTarget(record.target.installTarget,event.installTarget??event.result?.installTarget)))continue;
|
|
135
138
|
if (event.type === 'library.install-started') move(record, 'confirmed');
|
|
136
139
|
else if (event.type === 'library.install-done') move(record, 'installed', { result: { status: event.status ?? event.result?.status } });
|
|
137
140
|
else if (event.type === 'library.install-failed') move(record, 'failed', { error: { code: event.code ?? 'INSTALL_FAILED', message: '安装失败' } });
|
package/dist/library/install.js
CHANGED
|
@@ -8,15 +8,15 @@ import { redact } from '../runtime.js';
|
|
|
8
8
|
import { AppHostError, requireCondition, SerialQueue } from '../errors.js';
|
|
9
9
|
import {noteFailurePhase} from './diagnostics.js';
|
|
10
10
|
import { validateDefinition } from '../descriptor.js';
|
|
11
|
-
import {
|
|
11
|
+
import { isApplication } from './catalog.js';
|
|
12
12
|
import { profileSnapshot, runtimeSnapshot, profileChecks, runtimeChecks, packageRestoreArgs, keepOriginals } from './recovery.js';
|
|
13
13
|
import {SUITE_BUNDLE,prepareSuiteUpgrade,readSuiteStatus,createCoreSuiteAdapter} from './suite.js';
|
|
14
|
+
import {packageInstallTarget,installTargetName,pinnedGithubSpec} from './github-target.js';
|
|
14
15
|
|
|
15
16
|
/** Suite packages are changed only by the confirmed suite upgrade command. */
|
|
16
17
|
export const PROTECTED_PACKAGES=Object.freeze([SUITE_BUNDLE,'@hanamesh/core','@hanamesh/usage','@hanamesh/dsh-app-host','hanamesh-brand']);
|
|
17
18
|
const blocked=new Set(PROTECTED_PACKAGES);
|
|
18
19
|
const packageName=value=>typeof value==='string'&&/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
|
|
19
|
-
const versionText=value=>typeof value==='string'&&/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value);
|
|
20
20
|
async function json(path){return JSON.parse(await readFile(path,'utf8'));}
|
|
21
21
|
const defaultSpawn=spawnGitSafe;
|
|
22
22
|
|
|
@@ -45,8 +45,8 @@ async function clearGeneratedRuntimeCache(result,runtimeRoot){
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Package mutations use the public CLI in an owned monitored process; upstream's in-process manager has no scoped env/monitor port. */
|
|
48
|
-
export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBinary,dshBin,desktopCli,desktopLauncher,pluginManagerVersion,
|
|
49
|
-
|
|
48
|
+
export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBinary,dshBin,desktopCli,desktopLauncher,pluginManagerVersion,registry,
|
|
49
|
+
spawn=defaultSpawn,provision,remove,ledger,coreSuitePlan,coreService=()=>undefined,suiteSourceMode=()=>'configured',suiteSourceService=()=>undefined,emit=()=>{},profileWriter=new SerialQueue()}){
|
|
50
50
|
requireCondition(typeof profileDir==='string'&&resolve(profileDir)===profileDir,'PROFILE_DIR_REQUIRED','library.profileDir must be an absolute path.');
|
|
51
51
|
requireCondition(typeof dataRoot==='string'&&resolve(dataRoot)===dataRoot,'INVALID_ROOT','dataRoot must be absolute.');
|
|
52
52
|
const launcherAvailable=typeof desktopLauncher?.command==='string'&&resolve(desktopLauncher.command)===desktopLauncher.command&&typeof desktopLauncher.home==='string'&&join(desktopLauncher.home,'profiles',profileName)===profileDir;
|
|
@@ -65,58 +65,15 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
65
65
|
const runtimeApi=async name=>injected[name]??(await defaults())[name];
|
|
66
66
|
async function installed(packageNameValue){const root=join(profileDir,'node_modules',...packageNameValue.split('/')),pkg=await json(join(root,'package.json'));const appFile=typeof pkg.hanamesh?.app==='string'?pkg.hanamesh.app:'app.json';return{pkg,definition:validateDefinition(await json(join(root,appFile)))};}
|
|
67
67
|
const commandPath=command=>[dirname(command),...(process.platform==='win32'?(process.env.PATH??process.env.Path??'').split(delimiter):['/opt/homebrew/bin','/usr/local/bin','/usr/bin','/bin'])].join(delimiter);
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const run=args=>runCli(['plugin','--profile',profileName,...args]);
|
|
69
|
+
async function runCli(tail){try{requireCondition(cliAvailable,'DSH_CLI_UNAVAILABLE','The official dsh plugin CLI is unavailable in this host.',{need:['nodeBinary','dshBin']},503);
|
|
70
|
+
const options={cwd:profileDir,workerNode:supervisorNode,env:safeGitEnv({HOME:process.env.HOME,DSH_HOME:process.env.DSH_HOME,LANG:process.env.LANG??'C.UTF-8',PATH:commandPath(nodeBinary??desktopLauncher.command)})};
|
|
70
71
|
if(launcherAvailable)options.env=safeGitEnv({...options.env,DSH_HOME:desktopLauncher.home,PATH:commandPath(desktopLauncher.command)});
|
|
71
72
|
const result=await spawn(launcherAvailable?desktopLauncher.command:nodeBinary,launcherAvailable?tail:[...(desktopCli?['--expose-internals',fileURLToPath(new URL('./desktop-cli.js',import.meta.url)),desktopCli.entry,desktopCli.runtimeDir,desktopCli.supportDir]:[dshBin]),...tail],options);
|
|
72
73
|
const clean=text=>redact(stripVTControlCharacters(String(text??''))).replace(/(https?:\/\/)[^/\s@]+@/g,'$1[REDACTED]@').slice(-2000);
|
|
73
74
|
const stdout=clean(result.stdout),stderr=clean(result.stderr);
|
|
74
75
|
requireCondition(result.code===0,'DSH_PLUGIN_FAILED','DSH plugin operation failed.',{code:result.code,signal:result.signal,stderr,diagnostic:[stdout,stderr].filter(Boolean).join('\n')||`DSH plugin exited with code ${result.code}${result.signal?' ('+result.signal+')':''}.`},502);return result;
|
|
75
76
|
}catch(error){noteFailurePhase(error,'CLI run');throw error;}}
|
|
76
|
-
/** Bound the entire pre-install lookup, including public source discovery and metadata readback. */
|
|
77
|
-
async function latest(name){
|
|
78
|
-
const controller=new AbortController();let timer,phase='registry configuration';
|
|
79
|
-
const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{
|
|
80
|
-
const error=new AppHostError('REGISTRY_LOOKUP_TIMEOUT','Registry lookup timed out after 30000ms. Check DSH registry settings and network, then retry explicitly.',{},504);
|
|
81
|
-
noteFailurePhase(error,phase);controller.abort(error);reject(error);
|
|
82
|
-
},30000);});
|
|
83
|
-
async function lookup(){
|
|
84
|
-
const manager=pluginManager();
|
|
85
|
-
requireCondition(typeof manager?.registries==='function','REGISTRY_CONFIG_UNAVAILABLE','DSH does not expose registry configuration. Restore the public plugin manager before installing.',{},503);
|
|
86
|
-
// Public DSH ports own ordering, normalization, de-duplication and private-registry isolation.
|
|
87
|
-
const ruleMessage='DSH does not expose its registry rules. Restore the public plugin manager before installing.';
|
|
88
|
-
requireCondition(typeof registryRules==='function','REGISTRY_CONFIG_UNAVAILABLE',ruleMessage,{},503);
|
|
89
|
-
let rules;try{rules=await registryRules();}catch(cause){const error=new AppHostError('REGISTRY_CONFIG_UNAVAILABLE',ruleMessage,{},503);error.cause=cause;throw error;}
|
|
90
|
-
const {registryPlan,attributeFailure}=rules??{};
|
|
91
|
-
requireCondition(typeof registryPlan==='function'&&typeof attributeFailure==='function','REGISTRY_CONFIG_UNAVAILABLE',ruleMessage,{},503);
|
|
92
|
-
controller.signal.throwIfAborted();const configured=await manager.registries();controller.signal.throwIfAborted();
|
|
93
|
-
const plan=registryPlan(registry,configured);
|
|
94
|
-
for(let index=0;index<plan.length;index++){
|
|
95
|
-
const selected=plan[index],source=selected??configured.resolved;let failureKind='unknown';
|
|
96
|
-
controller.signal.throwIfAborted();phase='registry configuration';
|
|
97
|
-
requireCondition(typeof source==='string','REGISTRY_CONFIG_UNAVAILABLE','DSH could not resolve its configured registry. Check DSH registry settings.',{},503);
|
|
98
|
-
const base=new URL(source);requireCondition(['https:','http:'].includes(base.protocol),'REGISTRY_DENIED','Registry URL is invalid.');
|
|
99
|
-
try{
|
|
100
|
-
phase='registry fetch';let response;
|
|
101
|
-
try{response=await fetchImpl(new URL(`${base.href.replace(/\/$/,'')}/${encodeURIComponent(name)}/latest`),{headers:{accept:'application/json','accept-encoding':'identity'},signal:controller.signal});}
|
|
102
|
-
catch(error){failureKind='network';throw error;}
|
|
103
|
-
controller.signal.throwIfAborted();if(!response.ok){failureKind=response.status===404?'not-found':response.status>=500?'network':'unknown';throw new AppHostError('REGISTRY_LOOKUP_FAILED','Registry latest lookup failed. Check DSH registry settings and network, then retry explicitly.',{status:response.status},502);}
|
|
104
|
-
phase='registry readback';const metadata=await response.json();controller.signal.throwIfAborted();const version=metadata.version;
|
|
105
|
-
requireCondition(versionText(version)&&(allowPrerelease||!version.includes('-')),'UNSTABLE_VERSION','Registry latest must be an exact stable version.');
|
|
106
|
-
return{version,registry:selected};
|
|
107
|
-
}catch(error){
|
|
108
|
-
// Ask only fallbacks configured by DSH, and only when its public rule permits another source.
|
|
109
|
-
if(!controller.signal.aborted&&index+1<plan.length&&attributeFailure(failureKind,'',{kind:'registry',name,spec:name})==='registry')continue;
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
try{return await Promise.race([lookup(),timeout]);}
|
|
115
|
-
catch(cause){
|
|
116
|
-
const error=cause instanceof AppHostError?cause:new AppHostError('REGISTRY_LOOKUP_FAILED','Registry lookup failed. Check DSH registry settings and network, then retry explicitly.',{},502);
|
|
117
|
-
if(error!==cause)error.cause=cause;noteFailurePhase(error,phase);throw error;
|
|
118
|
-
}finally{clearTimeout(timer);}
|
|
119
|
-
}
|
|
120
77
|
/**
|
|
121
78
|
* rc.10 compensation after a failed install: the package goes back to its baseline spec through the official CLI, runtime
|
|
122
79
|
* items this operation newly claimed are removed by ownership, a replaced baseline item is provisioned again from the
|
|
@@ -160,26 +117,33 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
160
117
|
const name=item.package.name,operationId=/^[A-Za-z0-9-]{1,64}$/.test(context?.operationId??'')?context.operationId:randomUUID();let stage='baseline',base,runtime,installRegistry=null;
|
|
161
118
|
try{
|
|
162
119
|
base=await profileSnapshot(profileDir,name);
|
|
163
|
-
stage='before-add';let version;
|
|
120
|
+
stage='before-add';let version;const target=item.installTarget,github=target?.source==='github';
|
|
121
|
+
if(target.source==='npm'){version=target.version;installRegistry=registry??null;}
|
|
122
|
+
const spec=github?pinnedGithubSpec(target.repo,target.ref,target.path):`${name}@${version}`;
|
|
164
123
|
const status='restart-required';
|
|
165
|
-
stage='add';await run(['add','--save-exact',...(installRegistry===null?[]:[`--registry=${installRegistry}`])
|
|
124
|
+
stage='add';await run(['add','--save-exact',...(installRegistry===null?[]:[`--registry=${installRegistry}`]),...(target?.allowBuild??[]).map(entry=>`--allow-build=${entry}`),spec]);
|
|
166
125
|
stage='readback';const added=await profileSnapshot(profileDir,name);
|
|
167
|
-
|
|
168
|
-
|
|
126
|
+
if(github){
|
|
127
|
+
const actual=await json(join(profileDir,'node_modules',...name.split('/'),'package.json'));
|
|
128
|
+
requireCondition(added.spec===spec&&actual.name===name&&typeof actual.version==='string'&&typeof actual.dsh?.bundle?.patch==='string'&&actual.dsh.bundle.patch.length>0&&added.bundles.includes(name),
|
|
129
|
+
'INSTALL_READBACK_MISMATCH','The profile does not resolve and enable the reviewed GitHub package.',{expected:spec,spec:added.spec,packageName:actual.name,active:added.bundles.includes(name)},502);
|
|
130
|
+
version=actual.version;
|
|
131
|
+
}else requireCondition(added.spec===version&&added.module===version,'INSTALL_READBACK_MISMATCH','The profile does not resolve the exact version that was added.',{expected:version,spec:added.spec,module:added.module},502);
|
|
132
|
+
if(kind==='plugin')return{status,kind,packageName:name,version,...(target?{installTarget:target}:{}),...(github?{commit:target.ref}:{})};
|
|
169
133
|
stage='definition';const record=await installed(name);const declared=record.definition.deployments.map(deployment=>deployment.runtime).find(Boolean);
|
|
170
134
|
if(declared){stage='provision';const root=runtimeRoot(record.definition.id);runtime={root,base:await runtimeSnapshot(await runtimeApi('ledger'),root)};
|
|
171
135
|
await (await runtimeApi('provision'))(declared.manifest,{root,only:[declared.item],onProgress:event=>emit({type:'library.provision-progress',appId:record.definition.id,...event})});}
|
|
172
|
-
return{status:'restart-required',kind,appId:record.definition.id,packageName:name,version};
|
|
136
|
+
return{status:'restart-required',kind,appId:record.definition.id,packageName:name,version,...(target?{installTarget:target}:{}),...(github?{commit:target.ref}:{})};
|
|
173
137
|
}catch(error){noteFailurePhase(error,{baseline:'profile baseline','before-add':'registry fetch',add:'CLI run',readback:'profile readback',definition:'application readback',provision:'runtime provision'}[stage]);error.recovery=await recover({name,stage,base,runtime,operationId,installRegistry});throw error;}
|
|
174
138
|
}
|
|
175
139
|
async function installAs(item,kind,context){
|
|
176
140
|
permitted(item.package.name);const name=item.package.name;
|
|
177
|
-
emit({type:'library.install-started',...(item.id?{itemId:item.id}:{}),packageName:name,kind});
|
|
141
|
+
emit({type:'library.install-started',...(item.id?{itemId:item.id}:{}),packageName:name,kind,...(item.installTarget?{installTarget:item.installTarget}:{})});
|
|
178
142
|
try{const result=await profileWriter.run(()=>transaction(item,kind,context));emit({type:'library.install-done',...result});return result;}
|
|
179
|
-
catch(error){emit({type:'library.install-failed',...(item.id?{itemId:item.id}:{}),packageName:name,kind,code:error.code??'INSTALL_FAILED',...(error.recovery?{recovery:error.recovery}:{})});throw error;}
|
|
143
|
+
catch(error){emit({type:'library.install-failed',...(item.id?{itemId:item.id}:{}),packageName:name,kind,...(item.installTarget?{installTarget:item.installTarget}:{}),code:error.code??'INSTALL_FAILED',...(error.recovery?{recovery:error.recovery}:{})});throw error;}
|
|
180
144
|
}
|
|
181
145
|
let suitePlan;
|
|
182
|
-
const suiteProvider=coreSuitePlan??createCoreSuiteAdapter({core:coreService,registry
|
|
146
|
+
const suiteProvider=coreSuitePlan??createCoreSuiteAdapter({core:coreService,registry,profileName,suiteSourceMode,suiteSourceService});
|
|
183
147
|
async function previewSuite(){
|
|
184
148
|
const next=await prepareSuiteUpgrade({coreSuitePlan:suiteProvider});
|
|
185
149
|
suitePlan={...next,planId:randomUUID()};return structuredClone(suitePlan);
|
|
@@ -195,7 +159,7 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
195
159
|
requireCondition(suitePlan?.planId===planId,'SUITE_PLAN_CHANGED','升级确认已变化,请重新查看并确认。',{},409);
|
|
196
160
|
const confirmed=suitePlan;suitePlan=undefined;
|
|
197
161
|
const fresh=await prepareSuiteUpgrade({coreSuitePlan:suiteProvider});
|
|
198
|
-
const identity=plan=>JSON.stringify({sourceBinding:plan.sourceBinding,bundle:plan.bundle,components:plan.components,declarationId:plan.declarationId,registry:plan.registry,selectedUpgrade:plan.selectedUpgrade,physicalBaseline:plan.physicalBaseline});
|
|
162
|
+
const identity=plan=>JSON.stringify({installation:plan.installation,sourceBinding:plan.sourceBinding,bundle:plan.bundle,components:plan.components,declarationId:plan.declarationId,registry:plan.registry,selectedUpgrade:plan.selectedUpgrade,physicalBaseline:plan.physicalBaseline});
|
|
199
163
|
requireCondition(identity(fresh)===identity(confirmed),'SUITE_PLAN_CHANGED','套件声明或版本已变化,请重新查看并确认。',{},409);
|
|
200
164
|
requireCondition(fresh.available,'SUITE_NO_UPDATE','当前没有兼容新版。',{},409);
|
|
201
165
|
const before=await readSuiteStatus(profileDir);
|
|
@@ -205,7 +169,7 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
205
169
|
requireCondition(identity(executing)===identity(confirmed),'SUITE_PLAN_CHANGED','执行前套件安装包来源或声明已变化,请重新查看并确认。',{},409);
|
|
206
170
|
emit({type:'library.install-started',itemId:'hanamesh:suite-upgrade',packageName:SUITE_BUNDLE,kind:'suite-upgrade'});
|
|
207
171
|
try{
|
|
208
|
-
await
|
|
172
|
+
await runCli(fresh.installation.args);
|
|
209
173
|
const actual=await readSuiteStatus(profileDir);
|
|
210
174
|
const saved=await json(join(profileDir,'package.json'));
|
|
211
175
|
requireCondition(actual.status==='installed'&&actual.version===fresh.bundle.to&&saved.dependencies?.[SUITE_BUNDLE]===fresh.bundle.to&&fresh.components.every((c,i)=>actual.components[i]?.version===c.to),'INSTALL_READBACK_MISMATCH','套件升级后的版本或启用状态不符,请完全退出后按补齐命令修复。',{},502);
|
|
@@ -216,12 +180,11 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
216
180
|
}
|
|
217
181
|
});},
|
|
218
182
|
async install(item,context){
|
|
219
|
-
|
|
220
|
-
|
|
183
|
+
requireCondition(item?.installTarget,'NOT_INSTALLABLE','A fixed catalog installation target is required.',{reason:'fixed-install-target-required'});
|
|
184
|
+
const target=packageInstallTarget(item.installTarget);item={...item,installTarget:target,package:{registry:target.source,name:installTargetName(target)}};
|
|
185
|
+
const kind=isApplication(item)?'application':'plugin';
|
|
221
186
|
return await installAs(item,kind,context);
|
|
222
187
|
},
|
|
223
|
-
/** rc.28: install a plugin by package name (already verified against the catalog by the service). */
|
|
224
|
-
async installPlugin({packageName:name},context){return await installAs({package:{registry:'npm',name}},'plugin',context);},
|
|
225
188
|
async provisionRuntime({appId,packageName:installedName,runtimeItem}){return await profileWriter.run(async()=>{const record=await installed(installedName);requireCondition(record.definition.id===appId,'APP_ID_MISMATCH','Installed package app id does not match.');const runtime=record.definition.deployments.map(deployment=>deployment.runtime).find(value=>value?.item===runtimeItem);requireCondition(runtime,'RUNTIME_NOT_DECLARED','Runtime item is not declared.');const result=await (await runtimeApi('provision'))(runtime.manifest,{root:runtimeRoot(appId),only:[runtime.item],onProgress:event=>emit({type:'library.provision-progress',appId,...event})});return{status:'restart-required',appId,packageName:installedName,result};});},
|
|
226
189
|
async uninstall({packageName:installedName,appId,runtimeItem,running}){permitted(installedName);requireCondition(!running,'INSTANCE_IN_USE','Stop all application instances before uninstalling.',{},409);return await profileWriter.run(async()=>{await run(['remove',installedName]);if(runtimeItem){const result=await (await runtimeApi('remove'))(runtimeItem,{root:runtimeRoot(appId)});await clearGeneratedRuntimeCache(result,runtimeRoot(appId));}return{status:'restart-required',kind:'application',appId,packageName:installedName,dataPreserved:true};});},
|
|
227
190
|
/** rc.28: `dsh plugin remove <name>`; the suite itself (PROTECTED_PACKAGES) is refused with PACKAGE_DENIED. */
|
package/dist/library/locate.js
CHANGED
|
@@ -28,18 +28,10 @@ async function pluginManagerFacts(anchor){
|
|
|
28
28
|
try{
|
|
29
29
|
const require=createRequire(anchor),manifestPath=require.resolve('@deepseek-ai/dsh-plugin-manager/package.json'),pkg=await json(manifestPath);
|
|
30
30
|
if(pkg?.name!=='@deepseek-ai/dsh-plugin-manager'||typeof pkg.version!=='string')return {};
|
|
31
|
-
|
|
32
|
-
try{const path=require.resolve('@deepseek-ai/dsh-plugin-manager/registry');if(path.startsWith(dirname(manifestPath)+sep))pluginManagerRegistry=path;}catch{}
|
|
33
|
-
return{pluginManagerVersion:pkg.version,...(pluginManagerRegistry?{pluginManagerRegistry}:{})};
|
|
31
|
+
return{pluginManagerVersion:pkg.version};
|
|
34
32
|
}catch{return {};}
|
|
35
33
|
}
|
|
36
34
|
export async function inferPluginManagerVersion(anchor){return(await pluginManagerFacts(anchor)).pluginManagerVersion;}
|
|
37
|
-
async function desktopRegistryPath(root,pkg){
|
|
38
|
-
const entry=pkg.exports?.['./registry'],target=typeof entry==='string'?entry:entry?.import??entry?.default;
|
|
39
|
-
if(typeof target!=='string'||!target.startsWith('./'))return undefined;
|
|
40
|
-
const path=resolve(root,target);if(!path.startsWith(root+sep))return undefined;
|
|
41
|
-
try{await access(path);return path;}catch{return undefined;}
|
|
42
|
-
}
|
|
43
35
|
/** A DSH profile manifest that carries HanaMesh: `dsh.profile` (written by `dsh plugin`) or a hanamesh dependency. */
|
|
44
36
|
export function isHanameshProfileManifest(manifest){
|
|
45
37
|
if(!manifest||typeof manifest!=='object'||Array.isArray(manifest))return false;
|
|
@@ -109,7 +101,6 @@ export async function inferDesktopLayout({moduleUrl=import.meta.url,argv=process
|
|
|
109
101
|
}
|
|
110
102
|
const managerManifest=await json(join(runtimeDir,'node_modules/@deepseek-ai/dsh-plugin-manager/package.json'));
|
|
111
103
|
const pluginManagerVersion=managerManifest?.name==='@deepseek-ai/dsh-plugin-manager'&&managerManifest.version===hostManifest.version?managerManifest.version:undefined;
|
|
112
|
-
const pluginManagerRegistry=pluginManagerVersion?await desktopRegistryPath(join(runtimeDir,'node_modules/@deepseek-ai/dsh-plugin-manager'),managerManifest):undefined;
|
|
113
104
|
// Current canonical runtime manifest only. No legacy descriptor inference or PATH search.
|
|
114
105
|
if(metadata?.platform===process.platform&&metadata?.arch===process.arch&&
|
|
115
106
|
metadata.desktopVersion===(await json(join(hostRoot,'package.json')))?.version&&/^24\.\d+\.\d+$/.test(metadata.node??'')) {
|
|
@@ -121,7 +112,7 @@ export async function inferDesktopLayout({moduleUrl=import.meta.url,argv=process
|
|
|
121
112
|
if(actual.node===metadata.node&&actual.electron===undefined)nodeBinary=await realpath(node);
|
|
122
113
|
} catch { /* Unavailable/unproven payload Node stays unavailable; never use Electron. */ }
|
|
123
114
|
}
|
|
124
|
-
return {profileDir:await realpath(profileDir),profileName:'desktop',dshBin,parentOrigin:'dsh-app://app',frameAncestors:[],desktopCli,desktopLauncher,pluginManagerVersion,
|
|
115
|
+
return {profileDir:await realpath(profileDir),profileName:'desktop',dshBin,parentOrigin:'dsh-app://app',frameAncestors:[],desktopCli,desktopLauncher,pluginManagerVersion,nodeBinary,runtimeDir};
|
|
125
116
|
}
|
|
126
117
|
|
|
127
118
|
/** The DSH CLI that started this process — `argv[1]` only when it IS `@deepseek-ai/dsh/lib/bin.js`. */
|
|
@@ -149,6 +140,6 @@ export async function resolveLibraryLocations(config={},{moduleUrl=import.meta.u
|
|
|
149
140
|
let dshBin=config.dshBin;if(dshBin===undefined){dshBin=desktop?.dshBin??inferDshBin(argv);if(dshBin)inferred.push('dshBin');}
|
|
150
141
|
let node=nodeBinary;if(node===undefined){node=desktop?.nodeBinary??inferNodeBinary({execPath,versions});if(node)inferred.push('nodeBinary');}
|
|
151
142
|
let sources=config.sources;if(sources===undefined){sources=structuredClone(DEFAULT_CATALOG_SOURCES);inferred.push('sources');}
|
|
152
|
-
const {pluginManagerVersion
|
|
153
|
-
return{profileDir,profileName,dshBin,nodeBinary:node,sources,inferred,...(pluginManagerVersion?{pluginManagerVersion}:{}),...(
|
|
143
|
+
const {pluginManagerVersion}=desktop??await pluginManagerFacts(inferDshBin(argv)??(profileDir?join(profileDir,'package.json'):undefined));
|
|
144
|
+
return{profileDir,profileName,dshBin,nodeBinary:node,sources,inferred,...(pluginManagerVersion?{pluginManagerVersion}:{}),...(desktop&&desktop.profileDir===profileDir&&config.dshBin===undefined?{desktopCli:desktop.desktopCli,desktopLauncher:desktop.desktopLauncher}:{})};
|
|
154
145
|
}
|
package/dist/library/recovery.js
CHANGED
|
@@ -15,11 +15,11 @@ async function optional(path){try{return await readFile(path);}catch(error){if(e
|
|
|
15
15
|
|
|
16
16
|
/** The profile facts one package operation can change: its direct spec, bundle selection, lockfile and resolved module. */
|
|
17
17
|
export async function profileSnapshot(profileDir,name){
|
|
18
|
-
const manifestBytes=await optional(join(profileDir,'package.json')),lockBytes=await optional(join(profileDir,'pnpm-lock.yaml'));
|
|
18
|
+
const manifestBytes=await optional(join(profileDir,'package.json')),lockBytes=await optional(join(profileDir,'pnpm-lock.yaml')),workspaceBytes=await optional(join(profileDir,'pnpm-workspace.yaml'));
|
|
19
19
|
const manifest=manifestBytes===null?{}:JSON.parse(manifestBytes.toString('utf8'));
|
|
20
20
|
const direct=Object.assign({},...DEPENDENCY_FIELDS.map(field=>manifest[field]??{}));
|
|
21
21
|
const moduleBytes=await optional(join(profileDir,'node_modules',...name.split('/'),'package.json'));
|
|
22
|
-
return{manifestBytes,lockBytes,spec:direct[name]??null,others:Object.fromEntries(Object.entries(direct).filter(([key])=>key!==name)),
|
|
22
|
+
return{manifestBytes,lockBytes,workspaceBytes,buildPermissions:digest(workspaceBytes),spec:direct[name]??null,others:Object.fromEntries(Object.entries(direct).filter(([key])=>key!==name)),
|
|
23
23
|
bundles:manifest.dsh?.profile?.bundles??[],lock:digest(lockBytes),module:moduleBytes===null?null:JSON.parse(moduleBytes.toString('utf8')).version??null};
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -39,6 +39,7 @@ export function profileChecks(base,now,name){
|
|
|
39
39
|
{item:'dependency',expected:base.spec,actual:now.spec},
|
|
40
40
|
{item:'module',expected:base.module,actual:now.module},
|
|
41
41
|
{item:'bundle',expected:base.bundles.includes(name),actual:now.bundles.includes(name)},
|
|
42
|
+
{item:'build-permissions',expected:base.buildPermissions,actual:now.buildPermissions},
|
|
42
43
|
].map(row=>({...row,ok:same(row.expected,row.actual)}));
|
|
43
44
|
if(concurrent.length)rows.push({item:'lockfile',expected:base.lock,actual:now.lock,ok:false,reason:'CONCURRENT_PROFILE_WRITE',concurrent});
|
|
44
45
|
else rows.push({item:'lockfile',expected:base.lock,actual:now.lock,ok:base.lock===now.lock},{item:'bundles',expected:base.bundles,actual:now.bundles,ok:same(base.bundles,now.bundles)});
|
|
@@ -51,7 +52,7 @@ export function runtimeChecks(base,now){
|
|
|
51
52
|
|
|
52
53
|
/** The official CLI arguments that put one package back to its baseline spec, or none when nothing package-side changed. */
|
|
53
54
|
export function packageRestoreArgs(base,now,name){
|
|
54
|
-
if(profileChecks(base,now,name).every(row=>row.ok||row.reason==='CONCURRENT_PROFILE_WRITE'))return null;
|
|
55
|
+
if(profileChecks(base,now,name).every(row=>row.ok||row.reason==='CONCURRENT_PROFILE_WRITE'||row.item==='build-permissions'))return null;
|
|
55
56
|
if(base.spec===null)return now.spec===null?['install']:['remove',name];
|
|
56
57
|
return['add',...(EXACT.test(base.spec)?['--save-exact']:[]),`${name}@${base.spec}`];
|
|
57
58
|
}
|
|
@@ -61,6 +62,7 @@ export async function keepOriginals(dir,base,runtimeBase){
|
|
|
61
62
|
await mkdir(dir,{recursive:true,mode:0o700});const files=[];
|
|
62
63
|
if(base.manifestBytes!==null){await writeFile(join(dir,'package.json'),base.manifestBytes,{mode:0o600});files.push('package.json');}
|
|
63
64
|
if(base.lockBytes!==null){await writeFile(join(dir,'pnpm-lock.yaml'),base.lockBytes,{mode:0o600});files.push('pnpm-lock.yaml');}
|
|
65
|
+
if(base.workspaceBytes){await writeFile(join(dir,'pnpm-workspace.yaml'),base.workspaceBytes,{mode:0o600});files.push('pnpm-workspace.yaml');}
|
|
64
66
|
if(runtimeBase){await writeFile(join(dir,'runtime-ledger.json'),JSON.stringify(runtimeBase,undefined,2)+'\n',{mode:0o600});files.push('runtime-ledger.json');}
|
|
65
67
|
return{dir,files,sha256:{...(base.manifestBytes===null?{}:{'package.json':digest(base.manifestBytes)}),...(base.lockBytes===null?{}:{'pnpm-lock.yaml':digest(base.lockBytes)})}};
|
|
66
68
|
}
|
package/dist/library/routes.js
CHANGED
|
@@ -52,6 +52,7 @@ export function createLibraryHttpHandler(library,{parentOrigin,transportOrigin,a
|
|
|
52
52
|
requireCondition(subject&&typeof subject.principalId==='string','UNAUTHENTICATED','Host authentication is required.',{},401);
|
|
53
53
|
if(readOnly)requireCondition(new Set(url.searchParams.keys()).size===[...url.searchParams.keys()].length,'INVALID_INPUT','Duplicate query fields are not accepted.');
|
|
54
54
|
const input=readOnly?Object.fromEntries(url.searchParams):await jsonBody(req);
|
|
55
|
+
if(readOnly&&path==='/hanamesh/library/target'&&input.installTarget!==undefined){try{input.installTarget=JSON.parse(input.installTarget);}catch{throw new AppHostError('INVALID_INPUT','installTarget must contain a structured catalog target.');}}
|
|
55
56
|
requireCondition(await authorize(subject,path,input)===true,'FORBIDDEN','Operation is not authorized.',{},403);
|
|
56
57
|
let result;
|
|
57
58
|
if(NODE_RUNTIME_ROUTES.includes(path)){
|
package/dist/library/service.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { AppHostError, requireCondition, SerialQueue } from '../errors.js';
|
|
3
3
|
import { loadCatalog, isApplication, itemKind, upgradeAvailable, validateManifestUrl } from './catalog.js';
|
|
4
|
-
import { scanInstalled, scanInstalledPlugins, snapshotProfileDependencies } from './installed.js';
|
|
4
|
+
import { scanInstalled, scanInstalledPlugins, snapshotProfileDependencies, readProfileManifest } from './installed.js';
|
|
5
|
+
import {packageInstallTarget,installTargetName,sameInstallTarget,catalogPackageTargets,pinnedGithubSpec} from './github-target.js';
|
|
5
6
|
import {SUITE_BUNDLE,SUITE_TARGET,SUITE_PACKAGES,readSuiteStatus,CORE_SUITE_DECLARATION_MISSING,readSuiteSourceState} from './suite.js';
|
|
6
7
|
import { PROTECTED_PACKAGES } from './install.js';
|
|
7
8
|
import { handoffSettings } from './handoff.js';
|
|
@@ -15,6 +16,7 @@ const text=(value,max)=>typeof value==='string'&&value.length>0&&value.length<=m
|
|
|
15
16
|
const RESTART_STATES=new Set(['installed-not-loaded','uninstalled-not-unloaded']);
|
|
16
17
|
const protectedPackage=name=>PROTECTED_PACKAGES.includes(name);
|
|
17
18
|
const present=row=>row&&['registered','installed','installed-not-loaded'].includes(row.state);
|
|
19
|
+
const npmUpgradeAvailable=(target,row)=>Boolean(target?.source==='npm'&&row?.version&&row.state!=='uninstalled-not-unloaded'&&upgradeAvailable(target.version,row.version));
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* `client`: which client this host is ('dsh' | 'hanamesh', see client-identity.js). `unavailable`: when there is no
|
|
@@ -64,45 +66,59 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
64
66
|
requireCondition(input&&typeof input==='object'&&!Array.isArray(input),'INVALID_INPUT','Only itemId and packageName are accepted.');
|
|
65
67
|
// rc.8: optional install-target contract declaration. Absent keeps the v1 semantics; anything but '1' is refused explicitly, before field checks.
|
|
66
68
|
requireCondition(input.contractVersion===undefined||input.contractVersion===INSTALL_TARGET_CONTRACT_VERSION,'CONTRACT_VERSION_UNSUPPORTED','This market does not implement the requested install-target contract version.',{supported:[INSTALL_TARGET_CONTRACT_VERSION]});
|
|
67
|
-
requireCondition(Object.keys(input).every(key=>['itemId','packageName','contractVersion'].includes(key)),'INVALID_INPUT','Only
|
|
69
|
+
requireCondition(Object.keys(input).every(key=>['itemId','packageName','contractVersion','installTarget'].includes(key)),'INVALID_INPUT','Only catalog identity and package installation target are accepted.');
|
|
68
70
|
const{itemId,packageName}=input;
|
|
71
|
+
const expected=input.installTarget===undefined?undefined:packageInstallTarget(input.installTarget);
|
|
72
|
+
requireCondition(!expected||itemId!==undefined,'INVALID_INPUT','A structured installation target requires itemId.');
|
|
69
73
|
if(itemId===SUITE_TARGET){
|
|
70
|
-
requireCondition(packageName===undefined,'INVALID_INPUT','套件升级不接受包名或任意替换参数。');
|
|
74
|
+
requireCondition(packageName===undefined&&!expected,'INVALID_INPUT','套件升级不接受包名或任意替换参数。');
|
|
71
75
|
requireCondition(typeof installer?.prepareSuiteUpgrade==='function','SUITE_UPGRADE_UNAVAILABLE','当前客户端还不支持安全套件升级,请使用补齐命令。',{},503);
|
|
72
76
|
const plan=await installer.prepareSuiteUpgrade();
|
|
73
|
-
return{contractVersion:'1',source:{kind:'suite',manifestUrl:'https://github.com/yzsnstotz'},item:{id:SUITE_TARGET,displayName:'HanaMesh 套件',kind:'suite-upgrade',package:{registry:'npm',name:SUITE_BUNDLE},
|
|
77
|
+
return{contractVersion:'1',source:{kind:'suite',manifestUrl:'https://github.com/yzsnstotz'},item:{id:SUITE_TARGET,displayName:'HanaMesh 套件',kind:'suite-upgrade',package:{registry:'npm',name:SUITE_BUNDLE},installed:{state:'installed',version:suiteStatus.version},upgradeAvailable:plan.available,suiteUpgrade:plan}};
|
|
74
78
|
}
|
|
75
79
|
requireCondition(itemId!==undefined||packageName!==undefined,'INVALID_INPUT','itemId or packageName is required.');
|
|
76
80
|
requireCondition(itemId===undefined||(text(itemId,160)&&!/[\u0000-\u001f\u007f]/.test(itemId)),'INVALID_INPUT','itemId is invalid.');
|
|
77
81
|
requireCondition(packageName===undefined||(text(packageName,214)&&PACKAGE_NAME.test(packageName)),'INVALID_INPUT','packageName is invalid.');
|
|
82
|
+
async function resolved(found,source){
|
|
83
|
+
requireCondition(found.oneClickInstall?.allowed===true,'NOT_INSTALLABLE','The catalog has not approved one-click installation.',{reason:found.oneClickInstall?.reason??'fixed-install-target-required'});
|
|
84
|
+
const targets=catalogPackageTargets(found),selected=targets.find(target=>expected?sameInstallTarget(target,expected):packageName===undefined||installTargetName(target)===packageName);
|
|
85
|
+
requireCondition(!expected||selected,'CATALOG_CHANGED','The reviewed installation source or build permission changed. Review the current target again.',{},409);
|
|
86
|
+
requireCondition(selected,'NOT_INSTALLABLE','This catalog entry has no fixed package installation target for the requested package.',{reason:'fixed-install-target-required'});
|
|
87
|
+
const name=installTargetName(selected);
|
|
88
|
+
requireCondition(!protectedPackage(name),'PACKAGE_DENIED','The HanaMesh suite is not managed through the market.',{},403);
|
|
89
|
+
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===name)??null;
|
|
90
|
+
let upgrade=npmUpgradeAvailable(selected,row);
|
|
91
|
+
if(selected?.source==='github'&&row){
|
|
92
|
+
const saved=await readProfileManifest(config.profileDir);
|
|
93
|
+
upgrade=saved.dependencies[name]!==pinnedGithubSpec(selected.repo,selected.ref,selected.path)||row.active===false;
|
|
94
|
+
}
|
|
95
|
+
const item={...found,package:{registry:selected.source,name},installTarget:selected,installed:row,upgradeAvailable:upgrade};
|
|
96
|
+
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source,item:{...item,kind:itemKind(item)}};
|
|
97
|
+
}
|
|
98
|
+
const packageMatches=row=>row.package?.name===packageName||(row.installMethods??[]).some(method=>method.target&&(method.target.packageName??method.target.name)===packageName);
|
|
78
99
|
let cursor;const seen=new Set(),matches=new Map();
|
|
79
100
|
do{
|
|
80
|
-
const page=await catalog({q:packageName??'',category:'',cursor});
|
|
81
|
-
const found=page.items.find(row=>(itemId===undefined||row.id===itemId)&&(packageName===undefined||row
|
|
101
|
+
const page=await catalog({q:itemId===undefined?packageName??'':'',category:'',cursor});
|
|
102
|
+
const found=page.items.find(row=>(itemId===undefined||row.id===itemId)&&(packageName===undefined||packageMatches(row)));
|
|
82
103
|
if(found&&itemId===undefined&&page.schemaVersion==='hanamesh.catalog.v1'){
|
|
83
104
|
for(const row of page.items.filter(row=>row.package?.name===packageName))matches.set(row.id,{row,source:page.source});
|
|
84
105
|
requireCondition(matches.size<=1,'CATALOG_TARGET_AMBIGUOUS','Choose an itemId: this npm name belongs to multiple catalog identities.',{packageName},409);
|
|
85
106
|
}else if(found){
|
|
86
|
-
|
|
87
|
-
requireCondition(!protectedPackage(found.package.name),'PACKAGE_DENIED','The HanaMesh suite is not managed through the market.',{},403);
|
|
88
|
-
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===found.package.name)??null;
|
|
89
|
-
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source:page.source,item:{...found,kind:itemKind(found),installed:row,upgradeAvailable:Boolean(row?.version&&row.state!=='uninstalled-not-unloaded'&&upgradeAvailable(found.latestVersion,row.version))}};
|
|
107
|
+
return await resolved(found,page.source);
|
|
90
108
|
}
|
|
91
109
|
cursor=page.page?.nextCursor;
|
|
92
110
|
requireCondition(!cursor||!seen.has(cursor),'INVALID_CURSOR','Catalog cursor repeated.');if(cursor)seen.add(cursor);
|
|
93
111
|
}while(cursor);
|
|
94
112
|
if(matches.size===1){
|
|
95
113
|
const{row:found,source}=matches.values().next().value;
|
|
96
|
-
|
|
97
|
-
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===found.package.name)??null;
|
|
98
|
-
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source,item:{...found,kind:itemKind(found),installed:row,upgradeAvailable:Boolean(row?.version&&row.state!=='uninstalled-not-unloaded'&&upgradeAvailable(found.latestVersion,row.version))}};
|
|
114
|
+
return await resolved(found,source);
|
|
99
115
|
}
|
|
100
116
|
throw new AppHostError('CATALOG_ITEM_MISSING','The market catalog does not contain this target.',{},404);
|
|
101
117
|
}
|
|
102
118
|
function installItem(item){
|
|
103
|
-
const key=item.package.name;if(installing.has(key))
|
|
119
|
+
const key=item.package.name;if(installing.has(key)){const prior=installing.get(key);requireCondition(JSON.stringify(prior.target)===JSON.stringify(item.installTarget),'CATALOG_CHANGED','Another installation of this package is using a different reviewed target.',{},409);return prior.started;}
|
|
104
120
|
const started=operation('install',async operationId=>{try{return await markLive(await installer.install(item,{operationId}));}finally{installing.delete(key);}});
|
|
105
|
-
installing.set(key,started);return started;
|
|
121
|
+
installing.set(key,{started,target:item.installTarget});return started;
|
|
106
122
|
}
|
|
107
123
|
async function findByPackage(packageName){
|
|
108
124
|
requireCondition(text(packageName,214)&&PACKAGE_NAME.test(packageName),'INVALID_INPUT','packageName is required.');
|
|
@@ -120,9 +136,9 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
120
136
|
const page=await catalog({q,category,cursor});const installedRows=await installed();const pluginRows=await installedPlugins();
|
|
121
137
|
suiteStatus={...await suiteFacts(),...(suiteStatus.restartRequired?{restartRequired:true}:{}),...(suiteStatus.upgradeError?{upgradeError:suiteStatus.upgradeError}:{})};
|
|
122
138
|
const byPackage=new Map([...pluginRows,...installedRows].map(row=>[row.packageName,row]));
|
|
123
|
-
let items=page.items.filter(item=>!protectedPackage(item.package?.name)).map(item=>{const row=byPackage.get(item.package?.name)??null;const kind=itemKind(item);
|
|
124
|
-
return{...item,kind,application:isApplication(item),installed:row,upgradeAvailable:
|
|
125
|
-
if(config.profileDir&&!cursor&&(!category||category==='plugin')&&(!q||'hanamesh 套件'.includes(q.toLowerCase())))items.unshift({id:'hanamesh:suite',displayName:'HanaMesh 套件',summary:'官方 · 不计分。Core、使用记录与应用库一次装齐。',kind:'suite',official:true,
|
|
139
|
+
let items=page.items.map(item=>{const selected=catalogPackageTargets(item)[0];return selected?{...item,installTarget:selected,package:{registry:selected.source,name:installTargetName(selected)}}:item;}).filter(item=>!protectedPackage(item.package?.name)).map(item=>{const row=byPackage.get(item.package?.name)??null;const kind=itemKind(item);
|
|
140
|
+
return{...item,kind,application:isApplication(item),installed:row,upgradeAvailable:npmUpgradeAvailable(item.installTarget,row)};});
|
|
141
|
+
if(config.profileDir&&!cursor&&(!category||category==='plugin')&&(!q||'hanamesh 套件'.includes(q.toLowerCase())))items.unshift({id:'hanamesh:suite',displayName:'HanaMesh 套件',summary:'官方 · 不计分。Core、使用记录与应用库一次装齐。',kind:'suite',official:true,installed:suiteStatus.status==='installed'?{state:'installed',version:suiteStatus.version}:null,suite:suiteStatus,...((installer?.suiteUpgradeUnavailable??suiteStatus.suiteUpgradeUnavailable)?{suiteUpgradeUnavailable:installer?.suiteUpgradeUnavailable??suiteStatus.suiteUpgradeUnavailable}:{})});
|
|
126
142
|
const categories=[...new Set(page.items.flatMap(item=>item.categories??[]))].sort();
|
|
127
143
|
return{source:page.source,items,categories,installed:installedRows,plugins:pluginRows,restartRequired:[...pluginRows,...installedRows].some(row=>RESTART_STATES.has(row.state)),...(bootSnapshotError?{restartBaseline:{known:false,code:bootSnapshotError}}:{}),page:page.page};},
|
|
128
144
|
sources(){return{sources:structuredClone(state.sources),revision:state.revision};},
|
|
@@ -154,7 +170,7 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
154
170
|
requireCondition(input&&Object.keys(input).length===1&&typeof input.planId==='string','INVALID_INPUT','Only the current planId is accepted.');
|
|
155
171
|
return await operation('install',async()=>{try{const result=await installer.upgradeSuite(input);suiteStatus={...await suiteFacts(),restartRequired:true};return result;}catch(error){if(error.details?.stage!=='suite-upgrade')throw error;const actual=await readSuiteStatus(config.profileDir);suiteStatus={...await suiteFacts(),restartRequired:true,upgradeError:error.code??'SUITE_UPGRADE_FAILED'};error.details={...error.details,stage:'suite-upgrade',actual,command:error.details.command};emit({type:'library.install-failed',packageName:SUITE_BUNDLE,code:error.code??'SUITE_UPGRADE_FAILED',details:{stage:'suite-upgrade',actual,command:error.details.command}});throw error;}});
|
|
156
172
|
},
|
|
157
|
-
async install(input){requireCondition(input?.itemId!==SUITE_TARGET,'SUITE_CONFIRMATION_REQUIRED','请在客户端确认完整升级计划。',{},403);requireInstaller('Install requires absolute profileDir, nodeBinary, dshBin and profileName.');const{item}=await resolveTarget(input??{});if(present(item.installed)&&!item.upgradeAvailable)return{status:'already-installed',itemId:item.id,packageName:item.package.name};return await installItem(item);},
|
|
173
|
+
async install(input){requireCondition(input?.itemId!==SUITE_TARGET,'SUITE_CONFIRMATION_REQUIRED','请在客户端确认完整升级计划。',{},403);requireInstaller('Install requires absolute profileDir, nodeBinary, dshBin and profileName.');const{item}=await resolveTarget(input??{});requireCondition(item.installTarget?.source!=='github'||input.installTarget,'INVALID_INPUT','Confirm the displayed fixed GitHub installation target before installing.');if(present(item.installed)&&!item.upgradeAvailable)return{status:'already-installed',itemId:item.id,packageName:item.package.name};return await installItem(item);},
|
|
158
174
|
async provision(input){requireInstaller('Runtime provision is unavailable.');
|
|
159
175
|
// LIB-PROVISION-INPUT: a missing field is the caller's error (400), not an internal TypeError inside the operation.
|
|
160
176
|
requireCondition(text(input?.appId,160),'INVALID_INPUT','appId is required.');requireCondition(text(input?.packageName,214)&&PACKAGE_NAME.test(input.packageName),'INVALID_INPUT','packageName is required.');requireCondition(text(input?.runtimeItem,160),'INVALID_INPUT','runtimeItem is required.');
|
package/dist/library/suite.js
CHANGED
|
@@ -39,13 +39,11 @@ const exact=value=>typeof value==='string'&&semver.valid(value)===value;
|
|
|
39
39
|
const digest=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value);
|
|
40
40
|
const nullableText=value=>value===null||text(value);
|
|
41
41
|
const invalid=()=>requireCondition(false,'CORE_SUITE_DECLARATION_INVALID','Core 套件声明无法用于精确安装。',{},503);
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
const prefix=`dsh plugin --profile desktop add --save-exact ${SUITE_BUNDLE}@`;
|
|
45
|
-
return typeof value==='string'&&value.startsWith(prefix)&&exact(value.slice(prefix.length));
|
|
42
|
+
function validInstallation(value,version){
|
|
43
|
+
return object(value)&&value.name===SUITE_BUNDLE&&value.version===version&&text(value.command)&&Array.isArray(value.args)&&value.args.length>0&&value.args.every(text);
|
|
46
44
|
}
|
|
47
45
|
function validateDeclaration(value){
|
|
48
|
-
if(!object(value)||value.schemaVersion!==1||!text(value.checkedAt)||!
|
|
46
|
+
if(!object(value)||value.schemaVersion!==1||!text(value.checkedAt)||!nullableText(value.installCommand)||!digest(value.declarationId)||!text(value.nodeVersion)||!nullableText(value.registry)||!(value.bundleVersion===null||exact(value.bundleVersion))||!['complete','incomplete','unknown'].includes(value.completeness))invalid();
|
|
49
47
|
if(value.completeness==='complete'&&!exact(value.bundleVersion))invalid();
|
|
50
48
|
const manifest=value.manifest;
|
|
51
49
|
if(!object(manifest)||manifest.schemaVersion!==1||!text(manifest.id)||!object(manifest.bundle)||manifest.bundle.name!==SUITE_BUNDLE||!text(manifest.bundle.repository)||!object(manifest.core)||manifest.core.name!==SUITE_PACKAGES[0].name||!exact(manifest.core.version)||!text(manifest.core.repository)||!Array.isArray(manifest.components)||manifest.components.length!==2)invalid();
|
|
@@ -61,6 +59,7 @@ function validateDeclaration(value){
|
|
|
61
59
|
if(!Object.values(value.peerFacts).every(nullableText)||!Object.values(value.metadataDigests).every(text))invalid();
|
|
62
60
|
for(const upgrade of value.upgrades){
|
|
63
61
|
if(!object(upgrade)||!object(upgrade.bundle)||!text(upgrade.bundle.id)||upgrade.bundle.moduleName!==SUITE_BUNDLE||!exact(upgrade.bundle.version)||!text(upgrade.bundle.repository)||!digest(upgrade.bundle.metadataDigest)||!text(upgrade.registry)||!Array.isArray(upgrade.targets)||upgrade.targets.length!==3)invalid();
|
|
62
|
+
if(!validInstallation(upgrade.installation,upgrade.bundle.version))invalid();
|
|
64
63
|
const targets=new Set();
|
|
65
64
|
for(const row of upgrade.targets){if(!object(row)||!text(row.id)||!SUITE_PACKAGES.some(p=>p.name===row.moduleName)||targets.has(row.moduleName)||!exact(row.version)||!text(row.repository)||!digest(row.metadataDigest))invalid();targets.add(row.moduleName);}
|
|
66
65
|
}
|
|
@@ -76,7 +75,7 @@ export function readSuiteSourceState(source){
|
|
|
76
75
|
return{protocolVersion:state.protocolVersion,status:state.status,selection:state.selection,registry:state.registry,archiveCount:state.archiveCount,lastError:state.lastError};
|
|
77
76
|
}
|
|
78
77
|
/** Consume only the optional protocol-1 Cordis method and the public C-CORE section D DTO. */
|
|
79
|
-
export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceMode=()=>'configured',suiteSourceService=()=>undefined}={}){
|
|
78
|
+
export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceMode=()=>'configured',suiteSourceService=()=>undefined,profileName}={}){
|
|
80
79
|
const current=()=>{const value=core();return value?.protocolVersion==='1'&&typeof value.getSuiteDeclaration==='function'?value:undefined;};
|
|
81
80
|
// AppHost-local service identity only: Core protocol 1 supplies no revision/generation.
|
|
82
81
|
const instances=new WeakMap();
|
|
@@ -101,17 +100,17 @@ export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceM
|
|
|
101
100
|
}
|
|
102
101
|
const get=async()=>{
|
|
103
102
|
const binding=sourceBinding();
|
|
104
|
-
const declaration=await read({registry:binding.registry});
|
|
103
|
+
const declaration=await read({...(binding.registry===undefined?{}:{registry:binding.registry}),...(profileName===undefined?{}:{profile:profileName})});
|
|
105
104
|
requireCondition(JSON.stringify(sourceBinding())===JSON.stringify(binding),'SUITE_PLAN_CHANGED','套件安装包来源已变化,请重新查看并确认。',{},409);
|
|
106
105
|
requireCondition(declaration.completeness==='complete','CORE_SUITE_CURRENT_INCOMPLETE','Core 套件完整性:'+declaration.completeness,{completeness:declaration.completeness,blocked:declaration.blocked},409);
|
|
107
106
|
let expected;try{const url=new URL(binding.registry);url.pathname=url.pathname.replace(/\/$/,'')+'/';expected=url.href;}catch{}
|
|
108
|
-
requireCondition(
|
|
107
|
+
requireCondition(text(declaration.registry)&&(binding.registry===undefined||declaration.registry===expected),'CORE_SUITE_SOURCE_MISMATCH','Core 声明来源与执行 registry 不符。',{blocked:declaration.blocked},409);
|
|
109
108
|
requireCondition(declaration.upgrades.length>0,'CORE_SUITE_NO_COMPATIBLE_TUPLE','Core 未提供可执行升级组合。',{blocked:declaration.blocked},409);
|
|
110
109
|
// The first displayed Core tuple is the presentation default; App-host never ranks versions.
|
|
111
110
|
const selectedUpgrade=declaration.upgrades[0];
|
|
112
111
|
requireCondition(selectedUpgrade.registry===declaration.registry,'CORE_SUITE_SOURCE_MISMATCH','Core 升级组合来源与执行 registry 不符。',{},409);
|
|
113
112
|
const components=SUITE_PACKAGES.map(({name,label})=>{const from=declaration.installed.find(row=>row.moduleName===name).actualVersion;if(!exact(from))invalid();return{name,label,from,to:selectedUpgrade.targets.find(row=>row.moduleName===name).version};});
|
|
114
|
-
return{available:true,bundle:{name:SUITE_BUNDLE,from:declaration.bundleVersion,to:selectedUpgrade.bundle.version},components,installCommand:declaration.installCommand,sourceBinding:binding,declarationId:declaration.declarationId,registry:declaration.registry,selectedUpgrade,physicalBaseline:declaration.installed,upgrades:declaration.upgrades,blocked:declaration.blocked};
|
|
113
|
+
return{available:true,bundle:{name:SUITE_BUNDLE,from:declaration.bundleVersion,to:selectedUpgrade.bundle.version},components,installation:selectedUpgrade.installation,installCommand:declaration.installCommand,sourceBinding:binding,declarationId:declaration.declarationId,registry:declaration.registry,selectedUpgrade,physicalBaseline:declaration.installed,upgrades:declaration.upgrades,blocked:declaration.blocked};
|
|
115
114
|
};
|
|
116
115
|
Object.defineProperty(get,'unavailable',{get:()=>current()?null:CORE_SUITE_DECLARATION_MISSING});
|
|
117
116
|
get.readFacts=async()=>{
|
|
@@ -132,7 +131,7 @@ export async function prepareSuiteUpgrade({coreSuitePlan}={}){
|
|
|
132
131
|
const plan=await coreSuitePlan();
|
|
133
132
|
requireCondition(plan!==undefined&&plan!==null,'CORE_SUITE_DECLARATION_MISSING',CORE_SUITE_DECLARATION_MISSING,{},503);
|
|
134
133
|
const invalid=()=>requireCondition(false,'CORE_SUITE_DECLARATION_INVALID','Core 套件声明无法用于精确安装。',{},503);
|
|
135
|
-
if(typeof plan!=='object'||Array.isArray(plan)||typeof plan.available!=='boolean'||!object(plan.bundle)||plan.bundle.name!==SUITE_BUNDLE||!exact(plan.bundle.from)||!exact(plan.bundle.to)||!
|
|
134
|
+
if(typeof plan!=='object'||Array.isArray(plan)||typeof plan.available!=='boolean'||!object(plan.bundle)||plan.bundle.name!==SUITE_BUNDLE||!exact(plan.bundle.from)||!exact(plan.bundle.to)||!nullableText(plan.installCommand)||!validInstallation(plan.installation,plan.bundle.to)||!Array.isArray(plan.components)||plan.components.length!==SUITE_PACKAGES.length)invalid();
|
|
136
135
|
const rows=new Map();
|
|
137
136
|
for(const row of plan.components){
|
|
138
137
|
if(!row||typeof row!=='object'||!SUITE_PACKAGES.some(p=>p.name===row.name)||rows.has(row.name)||typeof row.from!=='string'||semver.valid(row.from)!==row.from||typeof row.to!=='string'||semver.valid(row.to)!==row.to)invalid();
|
package/docs/APP_PACKAGE.md
CHANGED
|
@@ -35,7 +35,7 @@ export async function apply(ctx, config = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
预发布版本只用于隔离验收且不得成为 `latest
|
|
38
|
+
预发布版本只用于隔离验收且不得成为 `latest`;应用库安装路径只接受启用目录 installMethods 给出的审核精确版本,且条目 oneClickInstall.allowed:true;不在安装时读取可变标签。发布者必须同时保留 npm 包完整性、runtime 三方来源与 sha256、以及与 app-host 精确 peer 版本对应的可复核构建记录。
|
|
39
39
|
|
|
40
40
|
## Existing configuration and original upstream archives (D-230)
|
|
41
41
|
|