@hanamesh/dsh-app-host 0.6.25 → 0.6.27
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 +12 -2
- package/dist/client-ui.js +77 -15
- package/dist/dsh.d.ts +1 -1
- package/dist/dsh.js +16 -3
- package/dist/installation-status.d.ts +1 -1
- package/dist/library/install.js +17 -11
- package/dist/library/installation-cli.js +2 -1
- package/dist/library/installation-discovery.js +18 -5
- package/dist/library/installation-status.js +39 -8
- package/dist/library/locate.js +10 -2
- package/dist/library/routes.js +13 -3
- package/dist/library/service.js +42 -8
- package/dist/library/suite.js +5 -5
- package/dist/library/versions.js +70 -0
- package/docs/CONTRACT.md +12 -1
- package/docs/INSTALLATION_STATUS.md +10 -5
- package/docs/LIBRARY.md +30 -0
- package/docs/LICENSES.json +4 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
0.6.27 候选:客户端随附独立 Node 的启动识别上限由 1 秒改为 30 秒(`PAYLOAD_NODE_PROBE_TIMEOUT_MS`)。owner Mac 实测冷启动 4.07 s / 2.81 s(CPU 0.09 s),已安装应用 0.26 s,30 秒留出 7 倍以上余量;旧 1 秒上限会让整次会话没有 Node,Claude Code/Codex 只读检测恒为 `AGENT_READER_NODE_REQUIRED`、安装环境显示未准备。到上限或插件卸载时立即 SIGKILL 该子进程,Node 仍按原规则记为不可用。身份与版本校验不变。
|
|
2
|
+
|
|
3
|
+
R168:后台套件版本查询与显式安装确认分离,展示读取不替换已打开的默认/历史确认计划;来源、精确组合与物理基线的执行复读保护保持。见 [查询与确认边界](docs/LIBRARY.md#r168-后台套件查询与确认计划)。
|
|
4
|
+
|
|
5
|
+
R162:预装 `@hanamesh/brand` 的前后端保护身份统一;目录和已装事实先显示,版本信息按项异步补充,受控并发/短期复用并随页面取消。完整预算与边界见 [R162首屏与保护](docs/LIBRARY.md#r162-首屏与保护边界)。
|
|
6
|
+
|
|
7
|
+
应用库版本管理:卡片和详情显示已装与最新兼容版本;可升级或选择历史 npm 版本,逐次确认后交给正式 DSH CLI 精确安装。失败保留原诊断与逐项恢复结果;需要完全退出并重开。版本查询失败显示错误,不冒充已是最新。详见 [版本管理](docs/LIBRARY.md#vr1-市场版本管理)。
|
|
8
|
+
|
|
9
|
+
0.6.26 候选:Codex 官方 GitHub 市场 URL 的 `.git` 后缀按精确仓库身份归一;市场映射与每项安装来源独立校验,不同仓库、凭据、查询、fragment 仍拒绝。CD2 扩大只读覆盖:ChatGPT 桌面 App 内置 Codex CLI、SemVer 预发布版本(仍须不低于最低版本);官方用户资料目录不存在即判未安装;HanaMesh 客户端从磁盘读取 DSH 桌面版 profile(`~/.dsh/profiles/desktop`)给出 DSH 渠道已安装/未安装。检测不执行安装或配置写入,见[安装状态](docs/INSTALLATION_STATUS.md)。
|
|
10
|
+
|
|
1
11
|
0.6.25 候选:本机 HTTPS 目录允许回环端口及 *.localhost,修复彩排种子初始化阻断检测监听;公网 HTTPS 非标准端口仍拒绝。Claude 官方 GitHub 市场清单按 repo 身份核对,保留不同来源拒绝。安装物回归命令见[安装状态](docs/INSTALLATION_STATUS.md),不以页面打开代替检测通过。
|
|
2
12
|
|
|
3
13
|
PX3 跨平台候选:Windows 可以运行插件/公开服务、attach 应用、受控只读 CLI 与普通包安装;`owned` 应用仍在创建进程、数据目录及请求凭据之前明确拒绝 `PLATFORM_UNSUPPORTED`。Windows 单测保留真实 attach 的 HTTP/租约/网关断言,并验证 owned 的持久失败、无进程/文件写入及不消费凭据;这些断言不构成 Windows owned 运行支持。独立 Node 可用不扩大 owned 支持;受管 Node 准备仍遵守原平台限制。Windows 本地快照采用文件 flush 加同目录 rename,不承诺断电后目录项持久;Coding OAuth Gateway 文件源因缺私有 ACL 证明继续拒绝,HTTP 提供方不变。各平台实际结果以本分支 GitHub Actions 矩阵收据为准。
|
|
@@ -22,7 +32,7 @@ PK1 兼容候选:开发输入更新为 DSH 0.2.2-rc.1 / Cordis 4.1.0 / Schemas
|
|
|
22
32
|
|
|
23
33
|
> I51 R1 返修(待复审):供应商指引跟随宿主中英菜单,默认正文只写用途和结果;实现解释、交接固定规则及具名设置默认值保留在技术详情。交接的保存操作与套件来源选择前置,自有字段按单列排列;设置值、默认值、权限、读写协议均不变。
|
|
24
34
|
|
|
25
|
-
0.6.13 候选只消费新 npm 三包身份;插件 id
|
|
35
|
+
0.6.13 候选只消费新 npm 三包身份;插件 id、服务与存储不变。Core、Usage、AppHost、总包与当前 Brand 受保护,Recommender 不属于套件。0.6.12 原件保持原行为。
|
|
26
36
|
>
|
|
27
37
|
> 已知限制 / Known limitations:官方 DSH rc.2 设置页在 390 CSS px 下仍保留固定侧栏,插件内容可用宽度受宿主挤压;本插件不改变客户端或全局 CSS,不能保证该宿主窄屏下标题、原生下拉选项及输入值完整可见。完整设置操作建议使用宽窗口。At 390 CSS px, the official DSH rc.2 settings sidebar leaves limited room for plugin content; titles, native select options and input values may remain constrained. Use a wider window for full settings operations. AppHost only controls its own content and does not modify the client or global CSS. 此限制按编排本次裁定披露,不报告整页390通过。Brand M01既有错误可见性限制仍开;静态profile peers check红流不改写为通过。
|
|
28
38
|
|
|
@@ -52,7 +62,7 @@ CLI-8:未配置来源的新用户默认请求 HanaMesh 视图(`https://marke
|
|
|
52
62
|
|
|
53
63
|
0.6.5 历史交付:Git 依赖安装先清除继承的凭据 helper,再只使用 gh;安装与测试入口监测到 osxkeychain 即失败。套件能力沿用 0.6.4:套件只显示一张官方卡,读回实际组件版本;兼容升级展示三个固定组件的版本变化并需本人确认,升级后完全退出并重开。原有DSH 本地 `install-target` 与网站交接监听共用允许网站名单,未获准网站不能读客户端能力;`library.handoff.pollMs` 保存后在真实存储重启中保留,旧记录缺字段仍用默认值。
|
|
54
64
|
|
|
55
|
-
> 2026-10-15 · 0.6.1(核查修正):①探测只回答「允许交接的网站」,别的网站拿不到客户端、就绪、缺什么;②交接出错不再被吞:过期分成「客户端没来取 / 确认框打不开 / 弹出后没回复」,确认框打不开报 `HANDOFF_OPEN_FAILED` 带原因,上报失败、读取失败在应用库里提示并写日志;免重启装完读回 profile 失败会照实提示;③轮询间隔成为设置 `library.handoff.pollMs`(默认 1500 毫秒);去掉端口数、网站数、过期上下限、预检缓存、读包标识超时这些写死的限值,剩下的固定规则在设置页逐条列出并说明原因;④品牌插件
|
|
65
|
+
> 2026-10-15 · 0.6.1(核查修正):①探测只回答「允许交接的网站」,别的网站拿不到客户端、就绪、缺什么;②交接出错不再被吞:过期分成「客户端没来取 / 确认框打不开 / 弹出后没回复」,确认框打不开报 `HANDOFF_OPEN_FAILED` 带原因,上报失败、读取失败在应用库里提示并写日志;免重启装完读回 profile 失败会照实提示;③轮询间隔成为设置 `library.handoff.pollMs`(默认 1500 毫秒);去掉端口数、网站数、过期上下限、预检缓存、读包标识超时这些写死的限值,剩下的固定规则在设置页逐条列出并说明原因;④品牌插件 `@hanamesh/brand` 在保护名单,不可从市场卸载;⑤测试临时目录全部在进程退出时删除(失败也删),有守卫测试。
|
|
56
66
|
> 2026-10-15 · 0.6.0(第 2 段 Session E):①网站交接——应用库在 127.0.0.1 上另起交接监听(默认端口 39317/39318/39319),网站探测、交接、轮询状态,客户端窗口弹出原有确认,用户确认才安装(C-MARKET 2026-10-15 追加);②宿主有公开 `pluginManager` 时插件安装/卸载改由它完成,装完即加载、无需重启(结果 `applied`);③探测返回真实客户端 `dsh`/`hanamesh` 和「还不能安装」的具体原因;④设置页新增「网站安装交接」,端口、允许的网站、过期时间都可见可改。
|
|
57
67
|
|
|
58
68
|
> D-230 / 0.5.9 candidate: each acquired AppHost route, optional-service listener, observer, resolver, domain and host now registers its release immediately. Failed activation drains the acquired resources and preserves the original error; cleanup failures produce only a named APPHOST_CLEANUP_FAILED diagnostic. Normal unload uses the same once-only releases. Ordinary-app admission, original presentation/success facts, native isolation and final-package U2 remain separate gates.
|
package/dist/client-ui.js
CHANGED
|
@@ -5,6 +5,14 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
|
|
|
5
5
|
// Owned dictionaries; host locale remains the only language preference.
|
|
6
6
|
const COPY={
|
|
7
7
|
"zh": {
|
|
8
|
+
"versionLatest":"最新兼容版本:",
|
|
9
|
+
"versionHistory":"选择历史版本",
|
|
10
|
+
"versionLabel":"插件版本",
|
|
11
|
+
"versionApply":"安装所选版本",
|
|
12
|
+
"versionLoading":"正在读取版本…",
|
|
13
|
+
"versionFailure":"版本查询失败:",
|
|
14
|
+
"versionUnavailable":"暂无可确认的兼容版本",
|
|
15
|
+
"versionIncompatible":"不兼容",
|
|
8
16
|
"copy0": "自动",
|
|
9
17
|
"copy1": "手动指定",
|
|
10
18
|
"copy2": "无可用供应商",
|
|
@@ -221,7 +229,7 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
|
|
|
221
229
|
"copy213": "插件名",
|
|
222
230
|
"copy216": "测试目录(fixture)",
|
|
223
231
|
"copy217": "当前市场目录",
|
|
224
|
-
"copy218": "
|
|
232
|
+
"copy218": "确认后安装 HanaMesh 总包及其兼容组件;需要完全退出并重新打开客户端。",
|
|
225
233
|
"copy219": "确认安装",
|
|
226
234
|
"copy220": "取消",
|
|
227
235
|
"copy221": "正在查询市场目录…",
|
|
@@ -306,6 +314,14 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
|
|
|
306
314
|
"copy301": "支持 macOS 13.5+(Apple Silicon / Intel)。Windows 的安装、守护与取消验证尚未接通,本流程不声称支持 Windows。"
|
|
307
315
|
},
|
|
308
316
|
"en": {
|
|
317
|
+
"versionLatest":"Latest compatible version: ",
|
|
318
|
+
"versionHistory":"Choose a previous version",
|
|
319
|
+
"versionLabel":"Plugin version",
|
|
320
|
+
"versionApply":"Install selected version",
|
|
321
|
+
"versionLoading":"Reading versions…",
|
|
322
|
+
"versionFailure":"Version lookup failed: ",
|
|
323
|
+
"versionUnavailable":"No confirmed compatible version",
|
|
324
|
+
"versionIncompatible":"Incompatible",
|
|
309
325
|
"copy0": "Automatic",
|
|
310
326
|
"copy1": "Manually assigned",
|
|
311
327
|
"copy2": "No available provider",
|
|
@@ -522,7 +538,7 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
|
|
|
522
538
|
"copy213": "Plugin name",
|
|
523
539
|
"copy216": "Test catalog (fixture)",
|
|
524
540
|
"copy217": "Current market catalog",
|
|
525
|
-
"copy218": "Confirmation
|
|
541
|
+
"copy218": "Confirmation installs the HanaMesh bundle and its compatible components. Fully quit and reopen the client afterwards.",
|
|
526
542
|
"copy219": "Confirm installation",
|
|
527
543
|
"copy220": "Cancel",
|
|
528
544
|
"copy221": "Reading market catalog…",
|
|
@@ -654,6 +670,21 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
|
|
|
654
670
|
const response=await globalThis.fetch.bind(globalThis)(path,{...init,headers:{'x-hanamesh-client':'workspace-v1',...(init.body?{'content-type':'application/json'}:{}),...init.headers}});
|
|
655
671
|
const value=await response.json();if(!response.ok){const error=new Error(value?.error?.code??`HTTP_${response.status}`);error.response=value;throw error;}return value;
|
|
656
672
|
};
|
|
673
|
+
/** Per-view display metadata only; exact installation preview always re-reads its own target. */
|
|
674
|
+
function createVersionQueries({concurrency,cacheMs,read,notify}){
|
|
675
|
+
if(!Number.isSafeInteger(concurrency)||concurrency<1||concurrency>16||!Number.isSafeInteger(cacheMs)||cacheMs<=0)throw new Error('PLUGIN_VERSION_QUERY_CONFIG_INVALID');
|
|
676
|
+
const entries=new Map(),queue=[];let active=0,closed=false;
|
|
677
|
+
const publish=(entry,value)=>{if(!closed&&!entry.cancelled&&entries.get(entry.id)===entry)notify(entry.id,{key:entry.key,...value});};
|
|
678
|
+
const cancel=entry=>{if(entry.cancelled)return;entry.cancelled=true;if(entries.get(entry.id)===entry)entries.delete(entry.id);entry.controller.abort();entry.reject(new Error('PLUGIN_VERSION_QUERY_CANCELLED'));};
|
|
679
|
+
const pump=()=>{while(!closed&&active<concurrency&&queue.length){const entry=queue.shift();if(entry.cancelled)continue;active++;
|
|
680
|
+
Promise.resolve().then(()=>read(entry.id,entry.controller.signal)).then(result=>{if(entry.cancelled)return;entry.finishedAt=Date.now();entry.state='ready';publish(entry,{...result,loading:false,error:null});entry.resolve(result);},error=>{if(entry.cancelled)return;entry.finishedAt=Date.now();entry.state='failed';publish(entry,{loading:false,error:String(error.code??error.message??error),latestVersion:null,upgradeAvailable:false});entry.reject(error);}).finally(()=>{active--;pump();});
|
|
681
|
+
}};
|
|
682
|
+
const ensure=descriptor=>{const prior=entries.get(descriptor.id);if(prior?.key===descriptor.key&&(prior.state==='loading'||Date.now()-prior.finishedAt<cacheMs))return prior.promise;
|
|
683
|
+
if(prior)cancel(prior);if(closed)return Promise.reject(new Error('PLUGIN_VERSION_QUERY_CANCELLED'));
|
|
684
|
+
const entry={...descriptor,controller:new AbortController(),state:'loading',cancelled:false};entry.promise=new Promise((resolve,reject)=>{entry.resolve=resolve;entry.reject=reject;});entry.promise.catch(()=>{});entries.set(entry.id,entry);queue.push(entry);publish(entry,{loading:true,error:null});pump();return entry.promise;
|
|
685
|
+
};
|
|
686
|
+
return{read:ensure,reconcile(descriptors){const wanted=new Map(descriptors.map(row=>[row.id,row.key]));for(const entry of entries.values())if(wanted.get(entry.id)!==entry.key)cancel(entry);for(const row of descriptors)if(row.automatic)void ensure(row);},close(){if(closed)return;closed=true;for(const entry of entries.values())cancel(entry);queue.length=0;}};
|
|
687
|
+
}
|
|
657
688
|
const entryId=entry=>entry.projection==='file'?`file:${entry.base??'home'}:${entry.path}`:`env:${entry.env}`;
|
|
658
689
|
function ProvidersSection(){useLocale();
|
|
659
690
|
const[providers,setProviders]=React.useState([]),[apps,setApps]=React.useState([]),[plans,setPlans]=React.useState({}),[error,setError]=React.useState('');
|
|
@@ -697,18 +728,18 @@ window.__ModuleLoader__.load({id:'@hanamesh/dsh-app-host',factory:function(requi
|
|
|
697
728
|
const setTarget=value=>{targetSelection=value;for(const listener of targetListeners)listener(value);};
|
|
698
729
|
const setPending=update=>{pendingOperations=typeof update==='function'?update(pendingOperations):update;for(const listener of pendingListeners)listener(pendingOperations);};
|
|
699
730
|
/** Public client entry: identifiers only; the market fetches its own catalog and owns confirmation. */
|
|
700
|
-
async function openInstallTarget(input,{handoffId}={}){
|
|
731
|
+
async function openInstallTarget(input,{handoffId,versionSelection=false}={}){
|
|
701
732
|
const sequence=++targetSequence;if(entryVisible)showLibrary();
|
|
702
733
|
try{
|
|
703
734
|
if(!input||typeof input!=='object'||Array.isArray(input))throw new Error('INVALID_INPUT');
|
|
704
735
|
// rc.8: optional contract declaration; absent keeps v1 semantics, an unsupported version is refused before any request.
|
|
705
736
|
if(input.contractVersion!==undefined&&input.contractVersion!==INSTALL_TARGET_CONTRACT_VERSION)throw new Error('CONTRACT_VERSION_UNSUPPORTED');
|
|
706
|
-
if(Object.keys(input).some(key=>!['itemId','packageName','contractVersion','installTarget'].includes(key))||(!input.itemId&&!input.packageName)||
|
|
737
|
+
if(Object.keys(input).some(key=>!['itemId','packageName','contractVersion','installTarget'].includes(key)&&!(versionSelection&&key==='version'))||(!input.itemId&&!input.packageName)||
|
|
707
738
|
(input.installTarget!==undefined&&(!input.itemId||!input.installTarget||typeof input.installTarget!=='object'||Array.isArray(input.installTarget)))||
|
|
708
739
|
(input.itemId!==undefined&&(typeof input.itemId!=='string'||!input.itemId.length||input.itemId.length>160||/[\u0000-\u001f\u007f]/.test(input.itemId)))||
|
|
709
740
|
(input.packageName!==undefined&&(typeof input.packageName!=='string'||input.packageName.length>214||!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(input.packageName))))throw new Error('INVALID_INPUT');
|
|
710
741
|
setTarget({status:'resolving',input:{...input}});
|
|
711
|
-
const query=targetQuery(input),resolved=await request('/hanamesh/library/target?'+query);
|
|
742
|
+
const query=targetQuery(input),resolved=await request((versionSelection?'/hanamesh/library/version-target?':'/hanamesh/library/target?')+query);
|
|
712
743
|
if(sequence!==targetSequence)return{status:'superseded'};
|
|
713
744
|
const item=resolved.item,op=pendingOperations[item.id];
|
|
714
745
|
const status=op?.busy?'installing':['registered','installed','installed-not-loaded'].includes(item.installed?.state)&&!item.upgradeAvailable?'already-installed':'confirmation-required';
|
|
@@ -894,7 +925,7 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
894
925
|
const KIND_TEXT={application:t('应用'),plugin:t('插件'),listing:t('仅收录')};
|
|
895
926
|
const STATE_TEXT={registered:t('已安装'),installed:t('已安装'),'installed-not-loaded':t('需重启'),'uninstalled-not-unloaded':t('已卸载 · 需重启'),'runtime-missing':t('缺运行时'),invalid:t('包无效')};
|
|
896
927
|
// Mirrors PROTECTED_PACKAGES on the host: the suite is never offered an uninstall button (the host answers 403 anyway).
|
|
897
|
-
const PROTECTED=new Set(['@hanamesh/hanamesh','@hanamesh/core','@hanamesh/usage','@hanamesh/dsh-app-host','hanamesh
|
|
928
|
+
const PROTECTED=new Set(['@hanamesh/hanamesh','@hanamesh/core','@hanamesh/usage','@hanamesh/dsh-app-host','@hanamesh/brand']);
|
|
898
929
|
function RestartNotice({reason}){useLocale();
|
|
899
930
|
return h('p',{role:'status',className:'hm-restart-notice','data-hanamesh-restart':'required'},h('strong',null,t('需重新打开客户端')),' ',reason??t('插件更改需要重新打开客户端后生效。'),' ',h('span',{className:'hm-restart-hint'},t('请完全退出 HanaMesh 客户端或 DSH 后重新打开;关闭窗口不等于退出。')));
|
|
900
931
|
}
|
|
@@ -941,6 +972,8 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
941
972
|
Object.assign(COPY.en,{cl1Detail:'HanaMesh / Application details',cred0:'Not available',cred1:'No data available',cred2:'Public statistics',cred3:'DSH installs / 30 days',cred4:'Users / 30 days',cred5:'Signals updated: ',cred6:'Users updated: '});
|
|
942
973
|
function LibraryOverlay({embedded,mainPage=false,preferredSubsectionId,verification=false}={}){useLocale();
|
|
943
974
|
const[visible,setVisible]=React.useState(embedded?true:libraryVisible),[snapshot,setSnapshot]=React.useState(null),[error,setError]=React.useState(''),[receipt,setReceipt]=React.useState(null),[query,setQuery]=React.useState(''),[filter,setFilter]=React.useState(''),[extra,setExtra]=React.useState([]),[pending,updatePending]=React.useState(pendingOperations),[seenCategories,setSeenCategories]=React.useState([]),[changed,setChanged]=React.useState(false),[openingAppId,setOpeningAppId]=React.useState(null),[closing,setClosing]=React.useState(false);
|
|
975
|
+
const[versionLists,setVersionLists]=React.useState({});
|
|
976
|
+
const versionQueries=React.useRef(null);
|
|
944
977
|
const[detailId,setDetailId]=React.useState(null),[acceptedTarget,setAcceptedTarget]=React.useState(null),[libraryTab,setLibraryTab]=React.useState('all'),[credibility,setCredibility]=React.useState(null);
|
|
945
978
|
React.useEffect(()=>{let live=true;setCredibility(null);if(detailId&&visible)void readCredibility(snapshot?.source,{id:detailId}).then(value=>{if(live)setCredibility(value);});return()=>{live=false;};},[detailId,visible,snapshot?.source?.manifestUrl]);
|
|
946
979
|
const[target,updateTarget]=React.useState(targetSelection),[targetItemId,setTargetItemId]=React.useState(''),[targetPackageName,setTargetPackageName]=React.useState('');
|
|
@@ -1010,6 +1043,16 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
1010
1043
|
const load=React.useCallback(async()=>{try{setError('');setExtra([]);const page=await request('/hanamesh/library'+params());setSnapshot(page);remember(page);}catch(e){setError(e);}},[params]);
|
|
1011
1044
|
const more=async()=>{try{const cursor=(extra.at(-1)??snapshot)?.page?.nextCursor;if(!cursor)return;const next=await request('/hanamesh/library'+params(cursor));setExtra(list=>[...list,next]);remember(next);}catch(e){if(String(e.message??e)==='CATALOG_CHANGED'){setSnapshot(null);setExtra([]);}setError(e);}};
|
|
1012
1045
|
React.useEffect(()=>{if(visible)void load();},[visible,load]);
|
|
1046
|
+
const versionDescriptor=item=>({id:item.id,key:JSON.stringify([snapshot?.source,snapshot?.versionQueries?.scope,item.package?.name,item.installed?.version??item.suite?.version??null]),automatic:item.kind==='suite'||Boolean(item.installed&&item.installTarget?.source==='npm')});
|
|
1047
|
+
React.useEffect(()=>()=>{versionQueries.current?.close();versionQueries.current=null;},[]);
|
|
1048
|
+
React.useEffect(()=>{
|
|
1049
|
+
if(!visible||!snapshot?.versionQueries){if(versionQueries.current){versionQueries.current.close();versionQueries.current=null;if(!visible){setSnapshot(null);setExtra([]);setVersionLists({});}}return;}
|
|
1050
|
+
const settings=snapshot.versionQueries,signature=JSON.stringify([settings.concurrency,settings.cacheMs]);
|
|
1051
|
+
if(versionQueries.current?.signature!==signature){versionQueries.current?.close();versionQueries.current={...createVersionQueries({...settings,read:(id,signal)=>request('/hanamesh/library/versions?'+new URLSearchParams({itemId:id}),{signal}),notify:(id,value)=>setVersionLists(previous=>{const prior=previous[id]?.key===value.key?previous[id]:{};return{...previous,[id]:{...prior,...value,...(value.versions?{selected:prior.selected??value.versions.find(row=>row.compatible&&row.version!==value.installedVersion)?.version??''}:{})}};})}),signature};}
|
|
1052
|
+
const all=[snapshot,...extra].flatMap(page=>page?.items??[]),shown=filter==='plugin'?all.filter(item=>item.kind==='plugin'||item.kind==='suite'):all;
|
|
1053
|
+
versionQueries.current.reconcile(shown.map(versionDescriptor));
|
|
1054
|
+
},[visible,snapshot,extra,filter]);
|
|
1055
|
+
|
|
1013
1056
|
React.useEffect(()=>{if(visible&&['installed','installed-live'].includes(target?.status))void load();},[visible,load,target?.status]);
|
|
1014
1057
|
// rc.18: install/provision/uninstall are 202 + events. Track the operation per card so the user sees
|
|
1015
1058
|
// 「安装中…」 and, on failure, the code with a readable reason (user 2026-09-20: clicked install, nothing happened).
|
|
@@ -1023,15 +1066,15 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
1023
1066
|
const selected=targetSelection;if(selected?.status!=='confirmation-required'||acceptedTarget!==selected||pendingOperations[selected.item.id]?.busy)return;
|
|
1024
1067
|
setTarget({...selected,status:'installing'});
|
|
1025
1068
|
const suite=selected.item.kind==='suite-upgrade';
|
|
1026
|
-
const reviewed={itemId:selected.item.id,packageName:selected.item.package.name,...(selected.item.installTarget?{installTarget:selected.item.installTarget}:{})};
|
|
1027
|
-
const outcome=await operate(suite?'/hanamesh/library/suite/upgrade':'/hanamesh/library/install',suite?{planId:selected.item.suiteUpgrade.planId}:reviewed,selected.item.id,suite?t('升级套件'):t('安装'),true);
|
|
1069
|
+
const reviewed={itemId:selected.item.id,packageName:selected.item.package.name,...(selected.item.installTarget?{installTarget:selected.item.installTarget}:{}),...(selected.item.selectedVersion?{version:selected.item.selectedVersion}:{})};
|
|
1070
|
+
const outcome=await operate(suite?'/hanamesh/library/suite/upgrade':selected.item.selectedVersion?'/hanamesh/library/version/install':'/hanamesh/library/install',suite?{planId:selected.item.suiteUpgrade.planId}:reviewed,selected.item.id,suite?t('升级套件'):t('安装'),true);
|
|
1028
1071
|
if(outcome){
|
|
1029
1072
|
if(!outcome.ok){if(targetSelection?.item?.id===selected.item.id)setTarget({...targetSelection,status:outcome.unknown?'result-unavailable':suite?'suite-upgrade-failed':'failed',error:outcome.code,details:outcome.details});return;}
|
|
1030
1073
|
if(suite){
|
|
1031
|
-
try{const snapshot=await request('/hanamesh/library/
|
|
1074
|
+
try{const snapshot=await request('/hanamesh/library/suite/status');if(snapshot.status!=='installed'||snapshot.version!==outcome.result.version||!outcome.result.components.every(c=>snapshot.components.some(p=>p.name===c.name&&p.version===c.to&&p.active)))throw new Error('INSTALL_READBACK_MISMATCH');setPending(p=>({...p,[selected.item.id]:{label:t("升级套件"),text:()=>(t('套件升级已读回,请完全退出并重新打开客户端。'))}}));setChanged(true);setTarget({...selected,status:'installed'});}
|
|
1032
1075
|
catch(error){setPending(p=>({...p,[selected.item.id]:{label:t("升级套件"),text:()=>(t('套件版本读回失败,请检查三个组件。')),failed:true}}));setTarget({...selected,status:'readback-failed',error:String(error.message??error)});}return;
|
|
1033
1076
|
}
|
|
1034
|
-
try{const fresh=await request('/hanamesh/library/target?'+targetQuery(reviewed));
|
|
1077
|
+
try{const fresh=await request((selected.item.selectedVersion?'/hanamesh/library/version-target?':'/hanamesh/library/target?')+targetQuery(reviewed));
|
|
1035
1078
|
const row=fresh.item?.installed;
|
|
1036
1079
|
if(row?.packageName!==selected.item.package.name||!['registered','installed','installed-not-loaded'].includes(row.state)||(outcome.result?.version&&row.version!==outcome.result.version))throw new Error('INSTALL_READBACK_MISMATCH');
|
|
1037
1080
|
const live=outcome.result?.status==='applied';setPending(p=>({...p,[selected.item.id]:{label:t("安装"),text:()=>(live?t('安装完成,已加载,可以直接使用(已按精确版本读回)')+baselineText(outcome.result):t('安装完成,已按精确版本读回'))}}));if(!live)setChanged(true);
|
|
@@ -1078,15 +1121,33 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
1078
1121
|
};
|
|
1079
1122
|
const close=async()=>{if(closing)return;setClosing(true);try{await heldView.current?.close();setReceipt(null);setError('');}catch(e){setError(e);}finally{if(mounted.current)setClosing(false);}};
|
|
1080
1123
|
if(!visible)return null;
|
|
1081
|
-
const pages=[snapshot,...extra].filter(Boolean),all=pages.flatMap(page=>page.items??[]),items=filter==='plugin'?all.filter(item=>item.kind==='plugin'||item.kind==='suite'):all,installed=snapshot?.installed??[],plugins=snapshot?.plugins??[],nextCursor=(extra.at(-1)??snapshot)?.page?.nextCursor??null;
|
|
1124
|
+
const pages=[snapshot,...extra].filter(Boolean),all=pages.flatMap(page=>page.items??[]),items=(filter==='plugin'?all.filter(item=>item.kind==='plugin'||item.kind==='suite'):all).map(item=>{const state=versionLists[item.id];return state&&(!state.key||state.key===versionDescriptor(item).key)?{...item,...(Object.hasOwn(state,'latestVersion')?{latestVersion:state.latestVersion,upgradeAvailable:state.upgradeAvailable??item.upgradeAvailable}:{}),versionError:state.error??item.versionError}:item;}),installed=snapshot?.installed??[],plugins=snapshot?.plugins??[],nextCursor=(extra.at(-1)??snapshot)?.page?.nextCursor??null;
|
|
1082
1125
|
const restartRequired=changed||Boolean(snapshot?.restartRequired);
|
|
1083
1126
|
const filters=[...BASE_FILTERS,...seenCategories.filter(c=>c!=='hanamesh-app').map(c=>[c,c])];
|
|
1084
1127
|
const openButton=row=>h('button',{type:'button',disabled:Boolean(openingAppId),onClick:()=>void open(row)},openingAppId===row.appId?t('打开中…'):t('打开'));
|
|
1128
|
+
const reviewVersion=(item,version)=>openInstallTarget({itemId:item.kind==='suite'?'hanamesh:suite-upgrade':item.id,version},{versionSelection:true}).catch(()=>{});
|
|
1129
|
+
const versionControls=item=>{
|
|
1130
|
+
if(item.kind!=='suite'&&item.installTarget?.source!=='npm')return null;
|
|
1131
|
+
const saved=versionLists[item.id],state=saved?.key&&saved.key!==versionDescriptor(item).key?undefined:saved;
|
|
1132
|
+
const read=async()=>{
|
|
1133
|
+
const descriptor=versionDescriptor(item);
|
|
1134
|
+
setVersionLists(previous=>({...previous,[item.id]:{...(previous[item.id]?.key===descriptor.key?previous[item.id]:{}),key:descriptor.key,expanded:true}}));
|
|
1135
|
+
if(versionQueries.current){void versionQueries.current.read(descriptor).catch(()=>{});return;}
|
|
1136
|
+
setVersionLists(previous=>({...previous,[item.id]:{...previous[item.id],loading:true}}));
|
|
1137
|
+
try{const result=await request('/hanamesh/library/versions?'+new URLSearchParams({itemId:item.id}));setVersionLists(previous=>({...previous,[item.id]:{...result,expanded:true,selected:result.versions.find(row=>row.compatible&&row.version!==item.installed?.version)?.version??''}}));}
|
|
1138
|
+
catch(error){setVersionLists(previous=>({...previous,[item.id]:{expanded:true,error:String(error.message??error)}}));}
|
|
1139
|
+
};
|
|
1140
|
+
return h('div',{className:'hm-plugin-versions'},
|
|
1141
|
+
item.latestVersion?h('p',{'data-hanamesh-latest-version':item.latestVersion},t('最新兼容版本:')+item.latestVersion):item.versionError?h('p',{role:'alert'},t('版本查询失败:')+item.versionError):null,
|
|
1142
|
+
h('button',{type:'button',disabled:state?.loading,onClick:()=>void read()},t(state?.loading?'正在读取版本…':'选择历史版本')),
|
|
1143
|
+
state?.error&&state.error!==item.versionError?h('p',{role:'alert'},t('版本查询失败:')+state.error):null,
|
|
1144
|
+
state?.expanded&&state?.versions?h('div',null,h('select',{'aria-label':t('插件版本'),value:state.selected,onChange:event=>setVersionLists(previous=>({...previous,[item.id]:{...previous[item.id],selected:event.target.value}}))},h('option',{value:'',disabled:true},t('插件版本')),...state.versions.map(row=>h('option',{key:row.version,value:row.version,disabled:!row.compatible||row.version===item.installed?.version},row.version+(row.compatible?'':' · '+t('不兼容'))))),h('button',{type:'button',disabled:!state.selected,onClick:()=>void reviewVersion(item,state.selected)},t('安装所选版本'))):null);
|
|
1145
|
+
};
|
|
1085
1146
|
const card=(item,detail=false)=>{const row=item.installed,op=pending[item.id],busy=op?.busy;const installBody={itemId:item.id,...(item.package?.name?{packageName:item.package.name}:{}),...(item.installTarget?{installTarget:item.installTarget}:{})};
|
|
1086
|
-
const reviewOrInstall=label=>item.installTarget?openInstallTarget(installBody).catch(()=>{}):operate('/hanamesh/library/install',installBody,item.id,label);
|
|
1147
|
+
const reviewOrInstall=label=>row&&item.latestVersion?reviewVersion(item,item.latestVersion):item.installTarget?openInstallTarget(installBody).catch(()=>{}):operate('/hanamesh/library/install',installBody,item.id,label);
|
|
1087
1148
|
let action=h('span',null,t('仅收录,不可安装')),state=null;
|
|
1088
1149
|
if(row)state=h('span',{className:'hm-market-state','data-hanamesh-state':row.state},(t(STATE_TEXT[row.state]??row.state))+(row.version?' '+row.version:''));
|
|
1089
|
-
if(item.kind==='suite')return h('article',{key:item.id,'data-hanamesh-library-item':item.id,'data-hanamesh-kind':'suite'},h('h3',null,t('HanaMesh 套件')),h('p',null,item.summary),h('p',null,item.suite?.status==='installed'?t('已安装 · HanaMesh ')+item.suite.version:item.suite?.status==='incomplete'?t('套件不完整,请查看组件状态'):t('套件状态未知,请查看 Core 声明与阻断原因')),item.suite?.completeness?h('p',null,t('Core 完整性:')+item.suite.completeness):null,...(item.suite?.components??[]).map(c=>h('p',{key:c.name},c.requiredRange?c.label+t(' · 要求 ')+c.requiredRange+t(' · 实际 ')+(c.actualVersion??t('检测不到'))+' · '+c.status:c.label+' · '+(c.version??t('检测不到'))+(c.active?'':t(' · 未启用')))),...(item.suite?.blocked??[]).map((row,i)=>h('p',{key:'blocked-'+i,role:'status'},row.code+(row.moduleName?' · '+row.moduleName:'')+(row.bundleVersion?' · HanaMesh '+row.bundleVersion:''))),item.suiteUpgradeUnavailable?h('p',{role:'status'},item.suiteUpgradeUnavailable):null,h('button',{type:'button',disabled:Boolean(item.suiteUpgradeUnavailable),onClick:()=>void openInstallTarget({itemId:'hanamesh:suite-upgrade'}).catch(()=>{})},t('检查兼容升级')));
|
|
1150
|
+
if(item.kind==='suite')return h('article',{key:item.id,'data-hanamesh-library-item':item.id,'data-hanamesh-kind':'suite'},h('h3',null,t('HanaMesh 套件')),h('p',null,item.summary),h('p',null,item.suite?.status==='installed'?t('已安装 · HanaMesh ')+item.suite.version:item.suite?.status==='incomplete'?t('套件不完整,请查看组件状态'):t('套件状态未知,请查看 Core 声明与阻断原因')),item.suite?.completeness?h('p',null,t('Core 完整性:')+item.suite.completeness):null,...(item.suite?.components??[]).map(c=>h('p',{key:c.name},c.requiredRange?c.label+t(' · 要求 ')+c.requiredRange+t(' · 实际 ')+(c.actualVersion??t('检测不到'))+' · '+c.status:c.label+' · '+(c.version??t('检测不到'))+(c.active?'':t(' · 未启用')))),...(item.suite?.blocked??[]).map((row,i)=>h('p',{key:'blocked-'+i,role:'status'},row.code+(row.moduleName?' · '+row.moduleName:'')+(row.bundleVersion?' · HanaMesh '+row.bundleVersion:''))),item.suiteUpgradeUnavailable?h('p',{role:'status'},item.suiteUpgradeUnavailable):null,versionControls(item),h('button',{type:'button',disabled:busy||Boolean(item.suiteUpgradeUnavailable),onClick:()=>void (item.latestVersion&&item.upgradeAvailable?reviewVersion(item,item.latestVersion):openInstallTarget({itemId:'hanamesh:suite-upgrade'})).catch(()=>{})},t(item.upgradeAvailable?'升级':'检查兼容升级')),!detail?h('button',{type:'button',onClick:()=>setDetailId(item.id)},t('详情')):null,op?operationNotice(op):null);
|
|
1090
1151
|
if(busy)action=h('button',{type:'button',disabled:true},t(op.text));
|
|
1091
1152
|
else if(item.kind!=='listing'&&!row)action=h('button',{type:'button',onClick:()=>void reviewOrInstall(t('安装'))},op?.failed?t('重试安装'):t('安装'));
|
|
1092
1153
|
else if(row&&item.upgradeAvailable)action=h('span',{className:'hm-market-actions'},h('button',{type:'button',onClick:()=>void reviewOrInstall(t('升级'))},item.installTarget?.source==='github'?t('查看安装目标'):t('升级')),row.state==='registered'?openButton(row):null);
|
|
@@ -1098,7 +1159,7 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
1098
1159
|
return h('article',{key:item.id,'data-hanamesh-library-item':item.id,'data-hanamesh-kind':item.kind,className:detail?'hm-app-detail':'hm-app-card'},detail&&mainPage?null:h('h3',null,item.displayName),detail&&mainPage?null:h('p',null,item.summary),
|
|
1099
1160
|
detail?h('div',{className:'hm-app-detail-content'},h('h4',null,t('关于这个应用')),h('p',null,item.description||item.summary),h('h4',null,t('可信度')),h('div',{className:'hm-credibility'},...credibilityFacts(credibility?.itemId===item.id?credibility:null).map(row=>h('div',{key:row.id},h('span',null,t(row.label)),h('strong',{'data-hanamesh-credibility':row.id},row.value===null?t('暂无'):row.value.toLocaleString()),h('small',null,row.value===null?t('当前暂无数据'):t('公开统计'))))),h('p',{className:'hm-market-state'},t('可信度数据缺失不代表 0;本机安装状态与平台采用度分别显示。')),credibility?.checkedAt?h('small',null,t('信号更新:')+credibility.checkedAt):null,credibility?.asOf?h('small',null,t('使用人数更新:')+credibility.asOf):null,h('h4',null,t('来源与安装目标')),h('p',null,item.package?.name??t('仅收录,不可安装')),item.installTarget?h('code',null,item.installTarget.source==='github'?'https://github.com/'+item.installTarget.repo+' @ '+item.installTarget.ref:item.installTarget.name+' @ '+item.installTarget.version):null,...(item.source?.sourceLinks??[]).map((link,index)=>h('p',{key:index},h('a',{href:link.sourceUrl,target:'_blank',rel:'noopener noreferrer'},link.sourceId+' · '+link.sourceRef))),catalogWebsiteLink(snapshot?.source,item)?h('a',{href:catalogWebsiteLink(snapshot?.source,item),target:'_blank',rel:'noopener noreferrer',className:'hm-website-link'},t('在网站查看')):null):null,
|
|
1100
1161
|
h('small',null,t(KIND_TEXT[item.kind])+' · '+(item.publisher?.name??t('未知发布者'))+((item.categories??[]).length?' · '+item.categories.join('、'):'')),
|
|
1101
|
-
state,h('div',{className:'hm-market-actions'},action,!detail?h('button',{type:'button',onClick:()=>setDetailId(item.id)},t('详情')):null),op&&!busy?operationNotice(op):null);};
|
|
1162
|
+
state,versionControls(item),h('div',{className:'hm-market-actions'},action,!detail?h('button',{type:'button',onClick:()=>setDetailId(item.id)},t('详情')):null),op&&!busy?operationNotice(op):null);};
|
|
1102
1163
|
const detailItem=items.find(item=>item.id===detailId);
|
|
1103
1164
|
const catalogSection=receipt?h('div',{className:'hm-app-frame'},h('div',null,h('strong',null,receipt.instance.appId),h('button',{type:'button',disabled:closing,onClick:()=>void close()},closing?t('关闭中…'):t('关闭视图'))),h('iframe',{ref:frame,src:receipt.uiUrl,title:receipt.instance.appId,sandbox:'allow-forms allow-modals allow-popups allow-same-origin allow-scripts'})):
|
|
1104
1165
|
detailItem?h('div',{key:'detail'},h('button',{type:'button',onClick:()=>setDetailId(null)},t('返回应用库')),card(detailItem,true)):h('div',{className:'hm-library-grid',key:'catalog'},...items.map(item=>card(item)),snapshot&&items.length===0?h('p',{role:'status'},t('没有找到符合条件的应用。')):null);
|
|
@@ -1126,7 +1187,8 @@ h('div',{className:'hm-source-add'},h('input',{value:draft,placeholder:'https://
|
|
|
1126
1187
|
target.item?.installTarget?.source==='npm'?h('dl',{'data-npm-confirmation':true},h('dt',null,t('精确版本')),h('dd',null,target.item.installTarget.version)):null,
|
|
1127
1188
|
target.item?.installTarget?.source==='github'?h('dl',{'data-github-confirmation':true},h('dt',null,t('来源仓库')),h('dd',null,'https://github.com/'+target.item.installTarget.repo),h('dt',null,t('固定 commit')),h('dd',null,target.item.installTarget.ref),...(target.item.installTarget.path?[h('dt',null,t('仓库子目录')),h('dd',null,target.item.installTarget.path)]:[])):null,
|
|
1128
1189
|
target.item?.installTarget?(target.item.installTarget.allowBuild.length?h('div',{role:'alert','data-build-warning':true},h('strong',null,t('会在本机执行构建脚本')),h('p',null,target.item.installTarget.allowBuild.join(', ')),h('p',null,t('确认后将按名单允许构建;pnpm 会把许可保存到当前 profile。'))):h('p',null,t('未授予构建脚本许可'))):null,
|
|
1129
|
-
target.item?.suiteUpgrade?h('
|
|
1190
|
+
target.item?.suiteUpgrade?h('dl',{'data-suite-confirmation':true},h('dt',null,t('精确版本')),h('dd',null,target.item.suiteUpgrade.bundle.from+' → '+target.item.suiteUpgrade.bundle.to)):null,
|
|
1191
|
+
target.item?.suiteUpgrade?h('ul',null,...target.item.suiteUpgrade.components.map(c=>h('li',{key:c.name},c.label+':'+c.from+' → '+c.to)),h('li',null,t('确认后安装 HanaMesh 总包及其兼容组件;需要完全退出并重新打开客户端。'))):null,
|
|
1130
1192
|
target.item?.suiteUpgrade?.registry?h('p',null,'Registry:'+target.item.suiteUpgrade.registry):null,
|
|
1131
1193
|
...(target.item?.suiteUpgrade?.blocked??target.details?.blocked??[]).map((row,i)=>h('p',{key:'suite-blocked-'+i,role:'status'},row.code+(row.moduleName?' · '+row.moduleName:'')+(row.bundleVersion?' · HanaMesh '+row.bundleVersion:''))),
|
|
1132
1194
|
target.status==='confirmation-required'?h('div',null,h('label',{className:'hm-install-ack'},h('input',{type:'checkbox','aria-label':t('我已核对来源与安装目标'),checked:acceptedTarget===target,onChange:event=>setAcceptedTarget(event.currentTarget.checked?target:null)}),t('我已核对来源与安装目标')),h('button',{type:'button',disabled:acceptedTarget!==target,onClick:()=>void confirmTarget()},t('确认安装')),h('button',{type:'button',onClick:()=>{targetSequence++;void reportHandoff(targetSelection?.handoffId,'cancelled');setTarget({...targetSelection,status:'cancelled'});}},t('取消'))):
|
package/dist/dsh.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface DshPluginConfig {
|
|
|
12
12
|
/** `sources` undefined → the HanaMesh catalog source; explicit `[]` → none. `profileDir`/`profileName`/`dshBin` (and
|
|
13
13
|
* `nodeBinary`) are inferred from the proven Desktop layout or plain DSH when undefined — see docs/CONTRACT.md 「应用库配置」; explicit values win. */
|
|
14
14
|
/** rc.28: the same keys serve the HanaMesh market (plugins + applications; routes `/hanamesh/library*`, see docs/CONTRACT.md「市场」). */
|
|
15
|
-
library?:{fixture?:string;sources?:Array<{manifestUrl:string;enabled?:boolean}>;profileDir?:string;profileName?:string;dshBin?:string;registry?:string;eventRetention?:number};
|
|
15
|
+
library?:{fixture?:string;sources?:Array<{manifestUrl:string;enabled?:boolean}>;profileDir?:string;profileName?:string;dshBin?:string;registry?:string;versionLookupTimeoutMs?:number;versionLookupMaxBytes?:number;versionLookupConcurrency?:number;versionLookupCacheMs?:number;eventRetention?:number};
|
|
16
16
|
applications?: AppDefinition[]; leaseTtlMs?: number; sweepIntervalMs?: number;
|
|
17
17
|
}
|
|
18
18
|
/** Adapt an open `ctx.storageDomain` handle (single layout, global snapshot) to the store binding. */
|
package/dist/dsh.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {pathToFileURL} from 'node:url';
|
|
2
|
-
import { createInstallationStatusReader } from './library/installation-status.js';
|
|
2
|
+
import { createInstallationStatusReader,officialDshDesktopProfile } from './library/installation-status.js';
|
|
3
3
|
/**
|
|
4
4
|
* DSH plugin entry for @hanamesh/dsh-app-host, using the public APIs audited in
|
|
5
5
|
* official DSH 0.2.0-rc.2 and 0.2.1-alpha.2:
|
|
@@ -31,6 +31,7 @@ import { createProviderSources } from './router/sources.js';
|
|
|
31
31
|
import { createRouter } from './router/broker.js';
|
|
32
32
|
import { createRouterHttpHandler, ROUTER_ROUTES } from './router/routes.js';
|
|
33
33
|
import { libraryDomainSpec } from './library/domain.js';
|
|
34
|
+
import {createNpmVersionCatalog,hostVersionReader} from './library/versions.js';
|
|
34
35
|
import { createLibraryService } from './library/service.js';
|
|
35
36
|
import { createLibraryInstaller } from './library/install.js';
|
|
36
37
|
import { inferDesktopLayout, resolveLibraryLocations } from './library/locate.js';
|
|
@@ -74,6 +75,10 @@ export const Config = z.object({
|
|
|
74
75
|
/** Locations are inferred from the verified Desktop argv/profile or plain DSH (see library/locate.js). */
|
|
75
76
|
profileDir:z.string(),profileName:z.string(),dshBin:z.string(),
|
|
76
77
|
registry:z.string(),
|
|
78
|
+
versionLookupConcurrency:z.number().min(1).max(16).default(4),
|
|
79
|
+
versionLookupCacheMs:z.number().min(1).max(2147483647).default(30000),
|
|
80
|
+
versionLookupTimeoutMs:z.number().min(1).max(2147483647).default(30000),
|
|
81
|
+
versionLookupMaxBytes:z.number().min(1).default(16777216),
|
|
77
82
|
/** Publicly named event history capacity; a lost cursor reports resetRequired. */
|
|
78
83
|
eventRetention:z.number().default(512),
|
|
79
84
|
/** Website → client handoff listener (library/handoff.js). Unset fields use the defaults shown in the library
|
|
@@ -130,7 +135,13 @@ export async function apply(ctx, config) {
|
|
|
130
135
|
config ??= {};
|
|
131
136
|
const dataRoot = config.dataRoot ?? join(process.env.DSH_HOME ?? process.env.HOME ?? '.', 'data', 'hanamesh-apps');
|
|
132
137
|
requireCondition(typeof dataRoot === 'string', 'INVALID_ROOT', 'dataRoot is required.');
|
|
133
|
-
|
|
138
|
+
// The payload Node probe may wait for a cold OS start; unloading the plugin kills it instead of waiting.
|
|
139
|
+
// Cordis runs effect cleanups only after apply() returns, so the cancel listens for this fiber's disposal
|
|
140
|
+
// (Cordis emits internal/plugin with uid cleared as soon as dispose() starts).
|
|
141
|
+
const layoutProbe = new AbortController();
|
|
142
|
+
ctx.on('internal/plugin', fiber => { if (fiber === ctx.fiber && fiber.uid === null) layoutProbe.abort(); });
|
|
143
|
+
const desktop = await inferDesktopLayout({ signal: layoutProbe.signal });
|
|
144
|
+
if (layoutProbe.signal.aborted) return; // unloaded during the probe: acquire nothing
|
|
134
145
|
const transportOrigin = `http://127.0.0.1:${webServer.port}`;
|
|
135
146
|
const parentOrigin = config.parentOrigin ?? desktop?.parentOrigin ?? transportOrigin;
|
|
136
147
|
const frameAncestors = config.frameAncestors ?? desktop?.frameAncestors ?? [];
|
|
@@ -258,7 +269,9 @@ export async function apply(ctx, config) {
|
|
|
258
269
|
};
|
|
259
270
|
const identity=await detectClient({desktop});
|
|
260
271
|
if(typeof ctx.logger?.info==='function')ctx.logger.info('hanamesh-app-host library: client %s (%s)',identity.client,identity.evidence);
|
|
261
|
-
|
|
272
|
+
const versionAnchor=desktop?.runtimeDir?join(desktop.runtimeDir,'package.json'):dshBin;
|
|
273
|
+
const versionCatalog=versionAnchor?createNpmVersionCatalog({registry:libraryConfig.registry||undefined,anchor:versionAnchor,peerVersion:hostVersionReader(versionAnchor),timeoutMs:libraryConfig.versionLookupTimeoutMs,maxBytes:libraryConfig.versionLookupMaxBytes}):undefined;
|
|
274
|
+
library=createLibraryService({versionCatalog,domain:{global:libraryDomain.global,close:closeLibraryDomain},host,config:libraryConfig,dataRoot,ledgerReader:provisionApi?.ledger??(async()=>({schema:1,items:{}})),installer,client:identity.client,unavailable,suiteSourceService:()=>suiteSourceService,installationStatus:nodeStatusBinding(()=>nodeRuntime.executable,workerNode=>createInstallationStatusReader({client:identity.client,core:()=>coreService,workerNode,otherProfileDir:identity.client==='hanamesh'?()=>officialDshDesktopProfile(locations.profileDir):null})),logger:ctx.logger});
|
|
262
275
|
own('library', () => library.close());
|
|
263
276
|
await library.init();
|
|
264
277
|
// rc.27: app bundles register plain `app.json` (no package name); the installed scan knows which npm package ships
|
|
@@ -3,7 +3,7 @@ export const INSTALLATION_STATUS_CAPABILITY:Readonly<{contract:'hanamesh.install
|
|
|
3
3
|
export interface InstallationStatusRequest {contractVersion:'1';consent:'read-installations'}
|
|
4
4
|
export interface InstallationObservation {
|
|
5
5
|
channel:'hanamesh'|'dsh'|'claude'|'codex';
|
|
6
|
-
scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';kind:'responding-runtime'|'
|
|
6
|
+
scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';kind:'responding-runtime'|'other-client'|'official-user-profile'|'desktop-app';label:string};
|
|
7
7
|
status:'installed'|'not-installed'|'incomplete'|'unknown';version:string|null;versionKind:'suite-bundle'|'agent-distribution';
|
|
8
8
|
components:readonly {name:string;version:string|null;active?:boolean;status?:string}[];
|
|
9
9
|
identity:{pluginName:string;marketplace:string}|null;
|
package/dist/library/install.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, lstat, unlink, rmdir } from 'node:fs/promises';
|
|
2
|
-
import { join, resolve, dirname, sep, delimiter } from 'node:path';
|
|
2
|
+
import { join, resolve, dirname, sep, delimiter, isAbsolute } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import {safeGitEnv,guardNodeBinary,spawnGitSafe} from '../git-process.js';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -14,7 +14,7 @@ import {SUITE_BUNDLE,prepareSuiteUpgrade,readSuiteStatus,createCoreSuiteAdapter}
|
|
|
14
14
|
import {packageInstallTarget,installTargetName,pinnedGithubSpec} from './github-target.js';
|
|
15
15
|
|
|
16
16
|
/** Suite packages are changed only by the confirmed suite upgrade command. */
|
|
17
|
-
export const PROTECTED_PACKAGES=Object.freeze([SUITE_BUNDLE,'@hanamesh/core','@hanamesh/usage','@hanamesh/dsh-app-host','hanamesh
|
|
17
|
+
export const PROTECTED_PACKAGES=Object.freeze([SUITE_BUNDLE,'@hanamesh/core','@hanamesh/usage','@hanamesh/dsh-app-host','@hanamesh/brand']);
|
|
18
18
|
const blocked=new Set(PROTECTED_PACKAGES);
|
|
19
19
|
const packageName=value=>typeof value==='string'&&/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
|
|
20
20
|
async function json(path){return JSON.parse(await readFile(path,'utf8'));}
|
|
@@ -67,7 +67,9 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
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
68
|
const run=args=>runCli(['plugin','--profile',profileName,...args]);
|
|
69
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
|
|
70
|
+
const dataHome=process.env.XDG_DATA_HOME;
|
|
71
|
+
const dataEnv=typeof dataHome==='string'&&isAbsolute(dataHome)&&!/[\x00-\x1f]/.test(dataHome)?{XDG_DATA_HOME:dataHome}:{};
|
|
72
|
+
const options={cwd:profileDir,workerNode:supervisorNode,env:safeGitEnv({...dataEnv,HOME:process.env.HOME,DSH_HOME:process.env.DSH_HOME,LANG:process.env.LANG??'C.UTF-8',PATH:commandPath(nodeBinary??desktopLauncher.command)})};
|
|
71
73
|
if(launcherAvailable)options.env=safeGitEnv({...options.env,DSH_HOME:desktopLauncher.home,PATH:commandPath(desktopLauncher.command)});
|
|
72
74
|
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);
|
|
73
75
|
const clean=text=>redact(stripVTControlCharacters(String(text??''))).replace(/(https?:\/\/)[^/\s@]+@/g,'$1[REDACTED]@').slice(-2000);
|
|
@@ -118,10 +120,10 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
118
120
|
try{
|
|
119
121
|
base=await profileSnapshot(profileDir,name);
|
|
120
122
|
stage='before-add';let version;const target=item.installTarget,github=target?.source==='github';
|
|
121
|
-
if(target.source==='npm'){version=target.version;installRegistry=registry??null;}
|
|
123
|
+
if(target.source==='npm'){version=target.version;installRegistry=context?.registry??registry??null;}
|
|
122
124
|
const spec=github?pinnedGithubSpec(target.repo,target.ref,target.path):`${name}@${version}`;
|
|
123
125
|
const status='restart-required';
|
|
124
|
-
stage='add';await run(['add','--save-exact',...(installRegistry===null?[]:[`--registry=${installRegistry}`]),...(target?.allowBuild??[]).map(entry=>`--allow-build=${entry}`),spec]);
|
|
126
|
+
stage='add';if(context?.installation)await runCli(context.installation.args);else await run(['add','--save-exact',...(installRegistry===null?[]:[`--registry=${installRegistry}`]),...(target?.allowBuild??[]).map(entry=>`--allow-build=${entry}`),spec]);
|
|
125
127
|
stage='readback';const added=await profileSnapshot(profileDir,name);
|
|
126
128
|
if(github){
|
|
127
129
|
const actual=await json(join(profileDir,'node_modules',...name.split('/'),'package.json'));
|
|
@@ -129,6 +131,7 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
129
131
|
'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
132
|
version=actual.version;
|
|
131
133
|
}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);
|
|
134
|
+
if(context?.verify)await context.verify();
|
|
132
135
|
if(kind==='plugin')return{status,kind,packageName:name,version,...(target?{installTarget:target}:{}),...(github?{commit:target.ref}:{})};
|
|
133
136
|
stage='definition';const record=await installed(name);const declared=record.definition.deployments.map(deployment=>deployment.runtime).find(Boolean);
|
|
134
137
|
if(declared){stage='provision';const root=runtimeRoot(record.definition.id);runtime={root,base:await runtimeSnapshot(await runtimeApi('ledger'),root)};
|
|
@@ -144,9 +147,11 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
144
147
|
}
|
|
145
148
|
let suitePlan;
|
|
146
149
|
const suiteProvider=coreSuitePlan??createCoreSuiteAdapter({core:coreService,registry,profileName,suiteSourceMode,suiteSourceService});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
// Display reads validate the same Core composition without registering an execution confirmation.
|
|
151
|
+
const readSuite=({bundleVersion}={})=>prepareSuiteUpgrade({coreSuitePlan:suiteProvider,...(bundleVersion===undefined?{}:{bundleVersion})});
|
|
152
|
+
async function previewSuite({bundleVersion}={}){
|
|
153
|
+
const next=await readSuite({bundleVersion});
|
|
154
|
+
suitePlan={...next,requestedVersion:bundleVersion??null,planId:randomUUID()};return structuredClone(suitePlan);
|
|
150
155
|
}
|
|
151
156
|
return{
|
|
152
157
|
get suiteUpgradeUnavailable(){return suiteProvider.unavailable??null;},
|
|
@@ -154,22 +159,23 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
154
159
|
if(!suiteProvider.readFacts)return await readSuiteStatus(profileDir);
|
|
155
160
|
try{return await suiteProvider.readFacts();}catch(error){return{...await readSuiteStatus(profileDir),status:'unknown',completeness:'unknown',blocked:[{code:error.code??'CORE_SUITE_DECLARATION_UNAVAILABLE'}],suiteUpgradeUnavailable:error.code==='CORE_SUITE_DECLARATION_MISSING'?suiteProvider.unavailable:'Core 套件声明读取失败('+(error.code??'CORE_SUITE_DECLARATION_UNAVAILABLE')+')'};}
|
|
156
161
|
},
|
|
162
|
+
readSuiteUpgrade:readSuite,
|
|
157
163
|
prepareSuiteUpgrade:previewSuite,
|
|
158
164
|
async upgradeSuite({planId}={}){return await profileWriter.run(async()=>{
|
|
159
165
|
requireCondition(suitePlan?.planId===planId,'SUITE_PLAN_CHANGED','升级确认已变化,请重新查看并确认。',{},409);
|
|
160
166
|
const confirmed=suitePlan;suitePlan=undefined;
|
|
161
|
-
const fresh=await prepareSuiteUpgrade({coreSuitePlan:suiteProvider});
|
|
167
|
+
const fresh=await prepareSuiteUpgrade({coreSuitePlan:suiteProvider,...(confirmed.requestedVersion===null?{}:{bundleVersion:confirmed.requestedVersion})});
|
|
162
168
|
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});
|
|
163
169
|
requireCondition(identity(fresh)===identity(confirmed),'SUITE_PLAN_CHANGED','套件声明或版本已变化,请重新查看并确认。',{},409);
|
|
164
170
|
requireCondition(fresh.available,'SUITE_NO_UPDATE','当前没有兼容新版。',{},409);
|
|
165
171
|
const before=await readSuiteStatus(profileDir);
|
|
166
172
|
requireCondition(before.status==='installed'&&before.version===fresh.bundle.from&&fresh.components.every((c,i)=>before.components[i]?.version===c.from),'SUITE_PLAN_CHANGED','已装版本或启用状态已变化,请重新查看并确认。',{},409);
|
|
167
173
|
requireCondition(cliAvailable,'DSH_CLI_UNAVAILABLE','套件升级需客户端随附的官方 CLI;请恢复后重试。',{},503);
|
|
168
|
-
const executing=await prepareSuiteUpgrade({coreSuitePlan:suiteProvider});
|
|
174
|
+
const executing=await prepareSuiteUpgrade({coreSuitePlan:suiteProvider,...(confirmed.requestedVersion===null?{}:{bundleVersion:confirmed.requestedVersion})});
|
|
169
175
|
requireCondition(identity(executing)===identity(confirmed),'SUITE_PLAN_CHANGED','执行前套件安装包来源或声明已变化,请重新查看并确认。',{},409);
|
|
170
176
|
emit({type:'library.install-started',itemId:'hanamesh:suite-upgrade',packageName:SUITE_BUNDLE,kind:'suite-upgrade'});
|
|
171
177
|
try{
|
|
172
|
-
await
|
|
178
|
+
await transaction({id:'hanamesh:suite-upgrade',package:{name:SUITE_BUNDLE},installTarget:{source:'npm',name:SUITE_BUNDLE,version:fresh.bundle.to,allowBuild:[]}},'plugin',{registry:fresh.registry,installation:fresh.installation,verify:async()=>{const actual=await readSuiteStatus(profileDir);requireCondition(actual.status==='installed'&&actual.version===fresh.bundle.to&&fresh.components.every((c,i)=>actual.components[i]?.version===c.to),'INSTALL_READBACK_MISMATCH','Suite component readback differs from the confirmed versions.',{},502);}});
|
|
173
179
|
const actual=await readSuiteStatus(profileDir);
|
|
174
180
|
const saved=await json(join(profileDir,'package.json'));
|
|
175
181
|
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);
|
|
@@ -22,7 +22,8 @@ export function installationEnvironment(source,platform=process.platform){
|
|
|
22
22
|
const fail=code=>{throw new AppHostError(code,'The automatically discovered official installation reader could not complete.',{},503)};
|
|
23
23
|
export const MINIMUM_READER_VERSIONS=Object.freeze({claude:'2.1.301',codex:'0.162.1'});
|
|
24
24
|
export function readerVersionFailure(channel,output){
|
|
25
|
-
|
|
25
|
+
// SemVer prerelease is allowed: desktop apps bundle prerelease CLIs (ChatGPT.app: codex-cli 0.167.0-alpha.3.2).
|
|
26
|
+
const pattern=channel==='claude'?/^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?) \(Claude Code\)$/:/^codex-cli (\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
|
|
26
27
|
const version=pattern.exec(output.trim())?.[1];
|
|
27
28
|
if(!version||semver.valid(version)!==version)return 'AGENT_READER_VERSION_INVALID';
|
|
28
29
|
return semver.gte(version,MINIMUM_READER_VERSIONS[channel])?null:'AGENT_READER_VERSION_UNSUPPORTED';
|
|
@@ -36,15 +36,26 @@ export async function readLoginEnvironment({home=homedir(),env=process.env,signa
|
|
|
36
36
|
if(!value||typeof value!=='object'||Array.isArray(value)||typeof value.PATH!=='string'||!Object.entries(value).every(([key,path])=>INSTALLATION_ENV_KEYS.includes(key)&&typeof path==='string'&&!/[\x00-\x1f]/.test(path)))throw failure('AGENT_SHELL_ENV_UNAVAILABLE');
|
|
37
37
|
return installationEnvironment(value);
|
|
38
38
|
}
|
|
39
|
-
export async function readLoginPath(options){return(await readLoginEnvironment(options)).PATH}
|
|
40
39
|
|
|
41
40
|
/** Select once: inherited PATH, login PATH, then documented layouts; never try another CLI after execution. */
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/** The official user profile directory each CLI reads (CLAUDE_CONFIG_DIR / CODEX_HOME, else ~/.claude / ~/.codex). */
|
|
42
|
+
export function agentProfileDir({channel,home=process.env.HOME??process.env.USERPROFILE??homedir(),env=process.env}={}){
|
|
44
43
|
if(!['claude','codex'].includes(channel)||!absolute(home))throw failure('AGENT_PROFILE_UNAVAILABLE');
|
|
45
|
-
signal?.throwIfAborted();
|
|
46
44
|
const configDir=env[channel==='claude'?'CLAUDE_CONFIG_DIR':'CODEX_HOME']??join(home,channel==='claude'?'.claude':'.codex');
|
|
47
45
|
if(!absolute(configDir))throw failure('AGENT_PROFILE_UNAVAILABLE');
|
|
46
|
+
return configDir;
|
|
47
|
+
}
|
|
48
|
+
/** Read-only existence check: an absent profile directory holds no user-scope plugin; unreadable is not absence. */
|
|
49
|
+
export async function agentProfileState({channel,home,env}={}){
|
|
50
|
+
const dir=agentProfileDir({channel,...(home===undefined?{}:{home}),...(env===undefined?{}:{env})});
|
|
51
|
+
try{if(!(await stat(dir)).isDirectory())throw failure('AGENT_PROFILE_UNAVAILABLE');return 'present'}
|
|
52
|
+
catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return 'absent';throw error.code==='AGENT_PROFILE_UNAVAILABLE'?error:failure('AGENT_PROFILE_UNAVAILABLE')}
|
|
53
|
+
}
|
|
54
|
+
export async function discoverOfficialReader({channel,home=process.env.HOME??process.env.USERPROFILE??homedir(),env=process.env,platform=process.platform,
|
|
55
|
+
systemBinDirs=platform==='win32'?[]:['/opt/homebrew/bin','/usr/local/bin','/home/linuxbrew/.linuxbrew/bin','/usr/bin','/bin'],
|
|
56
|
+
applicationDirs=platform==='darwin'?['/Applications',join(home,'Applications')]:[],loginPath,signal,workerNode}={}){
|
|
57
|
+
const configDir=agentProfileDir({channel,home,env});
|
|
58
|
+
signal?.throwIfAborted();
|
|
48
59
|
const appData=env.APPDATA??join(home,'AppData','Roaming'),local=env.LOCALAPPDATA??join(home,'AppData','Local');
|
|
49
60
|
const runtimeEnv=installationEnvironment(env,platform);
|
|
50
61
|
const reader={home,configDir,...AGENT_INSTALLATION_TARGET,...INSTALLATION_READ_BUDGET,runtimeEnv};
|
|
@@ -81,7 +92,9 @@ export async function discoverOfficialReader({channel,home=process.env.HOME??pro
|
|
|
81
92
|
...[env.npm_config_prefix,env.NPM_CONFIG_PREFIX,join(home,'.npm-global'),join(home,'.npm')].filter(absolute).map(root=>platform==='win32'?root:join(root,'bin')),
|
|
82
93
|
env.PNPM_HOME,...(absolute(env.PNPM_HOME)?[join(env.PNPM_HOME,'bin')]:[]),join(home,'.local','share','pnpm'),join(home,'.local','share','pnpm','bin'),join(home,'Library','pnpm'),join(home,'Library','pnpm','bin'),join(home,'.yarn','bin'),join(home,'.config','yarn','global','node_modules','.bin'),
|
|
83
94
|
join(env.VOLTA_HOME??join(home,'.volta'),'bin'),join(asdf,'shims'),join(mise,'shims'),
|
|
84
|
-
...(platform==='win32'?[join(appData,'npm'),join(local,'pnpm'),join(local,'Yarn','bin'),join(local,'Programs','OpenAI','Codex','bin'),join(local,'Microsoft','WinGet','Links'),join(local,'Volta','bin'),...(absolute(env.ProgramFiles)?[join(env.ProgramFiles,'nodejs')]:[])]:[]),...systemBinDirs
|
|
95
|
+
...(platform==='win32'?[join(appData,'npm'),join(local,'pnpm'),join(local,'Yarn','bin'),join(local,'Programs','OpenAI','Codex','bin'),join(local,'Microsoft','WinGet','Links'),join(local,'Volta','bin'),...(absolute(env.ProgramFiles)?[join(env.ProgramFiles,'nodejs')]:[])]:[]),...systemBinDirs,
|
|
96
|
+
// Desktop app bundles last, so a standalone CLI the user installed wins: ChatGPT.app ships the Codex CLI.
|
|
97
|
+
...(channel==='codex'?applicationDirs.map(root=>join(root,'ChatGPT.app','Contents','Resources','codex-cli','bin')):[])];
|
|
85
98
|
selected=await find(fixed);if(selected)return selected;
|
|
86
99
|
const managed=[];
|
|
87
100
|
for(const [root,suffix]of [[join(nvm,'versions','node'),['bin']],[env.NVM_HOME,[]],[join(fnm,'node-versions'),['installation',...(platform==='win32'?[]:['bin'])]],
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/** Read fresh public installation facts. No compatibility calculation, persistence or polling. */
|
|
2
2
|
import semver from 'semver';
|
|
3
3
|
import {AppHostError} from '../errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import {readFile} from 'node:fs/promises';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
import {realpathSync} from 'node:fs';
|
|
7
|
+
import {homedir} from 'node:os';
|
|
8
|
+
import {SUITE_BUNDLE,createCoreSuiteAdapter,SUITE_PACKAGES,readSuiteStatus} from './suite.js';
|
|
5
9
|
import {readerVersionFailure,validReader,runOfficialInstallationCommand} from './installation-cli.js';
|
|
6
|
-
import {discoverOfficialReader} from './installation-discovery.js';
|
|
10
|
+
import {discoverOfficialReader,agentProfileState,AGENT_INSTALLATION_TARGET} from './installation-discovery.js';
|
|
7
11
|
const object=x=>x!==null&&typeof x==='object'&&!Array.isArray(x);
|
|
8
12
|
const exact=x=>typeof x==='string'&&semver.valid(x)===x?x:null;
|
|
9
13
|
const reason=(code,message,action)=>({code,message,action});
|
|
@@ -17,10 +21,12 @@ const unknown=(row,code,message,action)=>({...row,status:'unknown',version:null,
|
|
|
17
21
|
const agentBase=channel=>base(channel,'agent-cli','official-user-profile',channel==='claude'?'Claude Code 用户级插件(不覆盖项目或其他隔离资料)':'Codex CLI 本地市场用户资料(不代表桌面 App、项目或其他隔离资料)');
|
|
18
22
|
const inventoryError=()=>{throw new AppHostError('AGENT_INVENTORY_INVALID','Official inventory shape is unsupported.',{},503)};
|
|
19
23
|
const qualified=x=>typeof x==='string'&&/^[a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-9._-]*$/.test(x)&&x.length<=257;
|
|
24
|
+
function githubGitUrl(url){return typeof url==='string'&&/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(url)?url.slice(0,-4):url;}
|
|
25
|
+
function codexMarketplaceUrl(source){return source?.sourceType==='git'?githubGitUrl(source.source):source?.sourceType==='url'?source.source:null;}
|
|
20
26
|
function claudeMarketplaceUrl(value){
|
|
21
27
|
if(value.source==='github'&&typeof value.repo==='string'&&/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repo))return `https://github.com/${value.repo}`;
|
|
22
28
|
const url=['url','git'].includes(value.source)?value.url:null;
|
|
23
|
-
return value.source==='git'
|
|
29
|
+
return value.source==='git'?githubGitUrl(url):url;
|
|
24
30
|
}
|
|
25
31
|
function parseInventory(channel,plugins,marketplaces,target){
|
|
26
32
|
let rows,markets;
|
|
@@ -34,7 +40,7 @@ function parseInventory(channel,plugins,marketplaces,target){
|
|
|
34
40
|
if(!markets.every(r=>object(r)&&typeof r.name==='string'&&r.name.length>0&&r.name.length<=128))inventoryError();
|
|
35
41
|
const sourceRows=markets.filter(r=>r.name===target.marketplace);
|
|
36
42
|
if(sourceRows.length>1)throw new AppHostError('AGENT_INVENTORY_CONFLICT','Conflicting marketplace identities.',{},503);
|
|
37
|
-
if(sourceRows.length){const m=sourceRows[0],url=channel==='claude'?claudeMarketplaceUrl(m):(
|
|
43
|
+
if(sourceRows.length){const m=sourceRows[0],url=channel==='claude'?claudeMarketplaceUrl(m):codexMarketplaceUrl(m.marketplaceSource);
|
|
38
44
|
if(url!==target.marketplaceUrl)throw new AppHostError('AGENT_MARKETPLACE_SOURCE_MISMATCH','The configured marketplace source is not the registered target.',{},503);
|
|
39
45
|
}
|
|
40
46
|
const id=`${target.pluginName}@${target.marketplace}`;
|
|
@@ -47,7 +53,7 @@ function parseInventory(channel,plugins,marketplaces,target){
|
|
|
47
53
|
const installedSource=scoped[0].marketplaceSource;
|
|
48
54
|
if(channel==='codex'){
|
|
49
55
|
if(!installedSource||!['git','url'].includes(installedSource.sourceType)||typeof installedSource.source!=='string')throw new AppHostError('AGENT_MARKETPLACE_SOURCE_UNAVAILABLE','Installed marketplace source is not observable.',{},503);
|
|
50
|
-
if(installedSource
|
|
56
|
+
if(codexMarketplaceUrl(installedSource)!==target.marketplaceUrl)throw new AppHostError('AGENT_MARKETPLACE_SOURCE_MISMATCH','Installed marketplace source conflicts with the registered target.',{},503);
|
|
51
57
|
}
|
|
52
58
|
const row=scoped[0],version=exact(channel==='claude'&&row.readFromFolder!==undefined?row.folderVersion:row.version);
|
|
53
59
|
const failed=channel==='claude'?Boolean(row.errors?.length||row.notes?.length):!row.installed;
|
|
@@ -55,8 +61,27 @@ function parseInventory(channel,plugins,marketplaces,target){
|
|
|
55
61
|
reason:failed?reason(channel==='claude'?'AGENT_PLUGIN_LOAD_FAILED':'AGENT_PLUGIN_PAYLOAD_MISSING','官方清单报告插件负载或加载问题。','在该官方客户端的插件详情中检查并修复。'):version===null?reason('AGENT_VERSION_UNAVAILABLE','已确认该作用域的安装记录,但实际版本未知。','在该官方客户端插件详情中核对实际版本。'):null};
|
|
56
62
|
}
|
|
57
63
|
const failureCodes=new Set(['AGENT_SHELL_ENV_UNAVAILABLE','AGENT_READER_DISCOVERY_INVALID','AGENT_READER_ENTRYPOINT_UNAVAILABLE','AGENT_READER_UNAVAILABLE','AGENT_READER_PERMISSION_DENIED','AGENT_READER_NODE_REQUIRED','AGENT_PROFILE_UNAVAILABLE','AGENT_READER_TIMEOUT','AGENT_READER_OUTPUT_LIMIT','AGENT_READER_CLEANUP_FAILED','AGENT_READER_DIAGNOSTIC','AGENT_READER_FAILED','AGENT_INVENTORY_INVALID','AGENT_INVENTORY_CONFLICT','AGENT_MARKETPLACE_SOURCE_MISMATCH','AGENT_MARKETPLACE_SOURCE_UNAVAILABLE','AGENT_SCOPE_UNOBSERVABLE']);
|
|
58
|
-
/**
|
|
59
|
-
|
|
64
|
+
/** Physical on-disk DSH profile inventory: no @hanamesh/hanamesh dependency (or no profile) is authoritative absence. */
|
|
65
|
+
async function profileSuite(row,profileDir){
|
|
66
|
+
let profile;try{profile=JSON.parse(await readFile(join(profileDir,'package.json'),'utf8'))}
|
|
67
|
+
catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return{...row,status:'not-installed'};return unknown(row,'DSH_PROFILE_UNREADABLE','DSH 桌面版 profile 清单无法读取。','打开 DSH 确认其插件页可用后重新检测。')}
|
|
68
|
+
if(!object(profile)||!object(profile.dependencies))return unknown(row,'DSH_PROFILE_UNREADABLE','DSH 桌面版 profile 清单无法读取。','打开 DSH 确认其插件页可用后重新检测。');
|
|
69
|
+
if(!Object.hasOwn(profile.dependencies,SUITE_BUNDLE))return{...row,status:'not-installed'};
|
|
70
|
+
const value=await readSuiteStatus(profileDir);
|
|
71
|
+
const components=value.components.map(c=>({name:c.name,version:c.version,active:c.active}));
|
|
72
|
+
if(value.status==='installed'&&value.version)return{...row,status:'installed',version:value.version,components};
|
|
73
|
+
if(value.status==='unknown')return unknown(row,'DSH_PROFILE_UNREADABLE','DSH 桌面版 profile 清单无法读取。','打开 DSH 确认其插件页可用后重新检测。');
|
|
74
|
+
return{...row,status:'incomplete',version:value.version,components,reason:reason('DSH_PROFILE_SUITE_INCOMPLETE','DSH 桌面版 profile 中的 HanaMesh 套件缺组件或未启用。','在 DSH 插件页启用 HanaMesh,或按 DSH 安装说明重新安装总包后重启 DSH。')};
|
|
75
|
+
}
|
|
76
|
+
/** Official DSH desktop profile; only distinct, resolvable physical profiles may answer for another client. */
|
|
77
|
+
export function officialDshDesktopProfile(ownProfileDir,home=homedir()){
|
|
78
|
+
const dir=join(home,'.dsh','profiles','desktop');
|
|
79
|
+
if(typeof ownProfileDir!=='string'||!ownProfileDir)return null;
|
|
80
|
+
try{return realpathSync(ownProfileDir)===realpathSync(dir)?null:dir}catch{return null}
|
|
81
|
+
}
|
|
82
|
+
/** Automatic current-user discovery only; `core` is the optional public protocol-1 Cordis service accessor.
|
|
83
|
+
* `otherProfileDir`: a fresh selector of the official DSH desktop profile (HanaMesh client only); trusted fixtures may supply a path. */
|
|
84
|
+
export function createInstallationStatusReader({client='dsh',core=()=>undefined,workerNode,runCommand=runOfficialInstallationCommand,discoverReader=discoverOfficialReader,profileState=agentProfileState,otherProfileDir=null}={}){
|
|
60
85
|
if(!['dsh','hanamesh'].includes(client))throw new TypeError('client must be dsh or hanamesh');
|
|
61
86
|
const active=new Set();let closed=false;
|
|
62
87
|
const suite=createCoreSuiteAdapter({core});
|
|
@@ -72,6 +97,7 @@ export function createInstallationStatusReader({client='dsh',core=()=>undefined,
|
|
|
72
97
|
async function agent(channel,signal){
|
|
73
98
|
const initial=agentBase(channel);let row=initial;
|
|
74
99
|
try{
|
|
100
|
+
if(await abortable(profileState({channel}),signal)==='absent')return{...initial,status:'not-installed',identity:{pluginName:AGENT_INSTALLATION_TARGET.pluginName,marketplace:AGENT_INSTALLATION_TARGET.marketplace}};
|
|
75
101
|
const config=await abortable(discoverReader({channel,signal,workerNode}),signal);
|
|
76
102
|
if(!validReader(config))throw new AppHostError('AGENT_READER_DISCOVERY_INVALID','Invalid automatic reader.',{},503);
|
|
77
103
|
row={...initial,identity:{pluginName:config.pluginName,marketplace:config.marketplace}};
|
|
@@ -85,11 +111,16 @@ export function createInstallationStatusReader({client='dsh',core=()=>undefined,
|
|
|
85
111
|
return{...row,...parseInventory(channel,plugins,marketplaces,config)};
|
|
86
112
|
}catch(error){if(signal.aborted||error.code==='INSTALLATION_STATUS_CANCELLED')throw interrupted();const code=failureCodes.has(error.code)?error.code:'AGENT_READER_FAILED';const messages={AGENT_READER_UNAVAILABLE:'未在当前用户的 PATH 或常见安装位置找到官方 CLI。',AGENT_SHELL_ENV_UNAVAILABLE:'登录 shell 的 PATH 未能读取。',AGENT_PROFILE_UNAVAILABLE:'当前用户的官方资料目录不存在或不可访问。',AGENT_READER_TIMEOUT:'本次官方只读检测超时。',AGENT_INVENTORY_INVALID:'官方清单 JSON 不符合受支持的格式。',AGENT_READER_DISCOVERY_INVALID:'自动发现的官方 CLI 信息无效。',AGENT_READER_ENTRYPOINT_UNAVAILABLE:'已发现 Windows 命令入口,但官方 npm 程序文件缺失。'};return unknown(row,code,messages[code]??'本次官方清单读取未能提供完整且一致的安装事实。','在终端确认官方 CLI 可运行,并在其用户级插件页面检查清单、权限和市场来源后重新检测。')}
|
|
87
113
|
}
|
|
114
|
+
async function other(signal){
|
|
115
|
+
const profileDir=client==='hanamesh'?(typeof otherProfileDir==='function'?otherProfileDir():otherProfileDir):null;
|
|
116
|
+
if(profileDir)return abortable(profileSuite(base('dsh','other-runtime','other-client','DSH 桌面版 profile(磁盘只读清单;运行中的 DSH 由其自身监听回答)'),profileDir),signal);
|
|
117
|
+
return unknown(base(client==='dsh'?'hanamesh':'dsh','other-runtime','other-client','另一客户端 runtime(须由其自身监听回答)'),'OTHER_RUNTIME_NOT_OBSERVED','本监听未读取另一客户端的 runtime。','启动该客户端,并向其已广告的状态入口显式检测。');
|
|
118
|
+
}
|
|
88
119
|
const read=async({signal}={})=>{
|
|
89
120
|
if(closed||signal?.aborted)throw interrupted();const controller=new AbortController();active.add(controller);const combined=signal?AbortSignal.any([signal,controller.signal]):controller.signal;
|
|
90
121
|
try{const current=await runtime(combined);const agents=await Promise.all(['claude','codex'].map(channel=>agent(channel,combined)));if(combined.aborted)throw interrupted();
|
|
91
122
|
return{schemaVersion:1,client,checkedAt:new Date().toISOString(),observations:[current,
|
|
92
|
-
|
|
123
|
+
await other(combined),...agents,
|
|
93
124
|
unknown(base('codex','desktop-app','desktop-app','Codex / ChatGPT 桌面 App(独立作用域)'),'CODEX_DESKTOP_SCOPE_UNOBSERVABLE','Codex CLI 清单不能证明桌面 App 的安装。','在桌面 App 的插件目录核对;需官方公开桌面状态协议后才可接入。')]};
|
|
94
125
|
}finally{active.delete(controller)}
|
|
95
126
|
};
|
package/dist/library/locate.js
CHANGED
|
@@ -69,7 +69,15 @@ export async function inferProfileDir(moduleUrl){
|
|
|
69
69
|
* The shell passes runtimeDir and profileDir to its named entry; prove the entry and this package's
|
|
70
70
|
* profile link before using them. Never infer from cwd, HOME, a port, or a supply overlay.
|
|
71
71
|
*/
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Upper bound for the one startup probe of the Desktop payload Node. Its first execution waits for the OS, not for
|
|
74
|
+
* Node: measured 4.07 s and 2.81 s (0.09 s CPU) on the owner Mac when the client ran from its DMG volume, against
|
|
75
|
+
* 0.26 s for the installed app. 30 s keeps more than 7x headroom over the slowest measured cold start; the former
|
|
76
|
+
* 1 s cap made the whole session run without Node. A probe that never answers is killed at this bound or when the
|
|
77
|
+
* plugin is disposed (`signal`), and the payload Node then stays unavailable as before.
|
|
78
|
+
*/
|
|
79
|
+
export const PAYLOAD_NODE_PROBE_TIMEOUT_MS=30_000;
|
|
80
|
+
export async function inferDesktopLayout({moduleUrl=import.meta.url,argv=process.argv,signal,probeTimeoutMs=PAYLOAD_NODE_PROBE_TIMEOUT_MS}={}) {
|
|
73
81
|
const [entry,runtimeDir,profileDir]=[argv?.[1],argv?.[2],argv?.[3]];
|
|
74
82
|
if (![entry,runtimeDir,profileDir].every(value=>typeof value==='string'&&isAbsolute(value))) return undefined;
|
|
75
83
|
const hostRoot=join(runtimeDir,'node_modules/@deepseek-ai/dsh-desktop-host'),hostManifest=await json(join(hostRoot,'package.json'));
|
|
@@ -107,7 +115,7 @@ export async function inferDesktopLayout({moduleUrl=import.meta.url,argv=process
|
|
|
107
115
|
const node=join(payload,'dependencies/node/bin',process.platform==='win32'?'node.exe':'node');
|
|
108
116
|
try {
|
|
109
117
|
await access(node,constants.X_OK);
|
|
110
|
-
const {stdout}=await promisify(execFile)(node,['-p','JSON.stringify({node:process.versions.node,electron:process.versions.electron})'],{
|
|
118
|
+
const {stdout}=await promisify(execFile)(node,['-p','JSON.stringify({node:process.versions.node,electron:process.versions.electron})'],{env:{},maxBuffer:1024,timeout:probeTimeoutMs,killSignal:'SIGKILL',...(signal?{signal}:{})});
|
|
111
119
|
const actual=JSON.parse(stdout);
|
|
112
120
|
if(actual.node===metadata.node&&actual.electron===undefined)nodeBinary=await realpath(node);
|
|
113
121
|
} catch { /* Unavailable/unproven payload Node stays unavailable; never use Electron. */ }
|
package/dist/library/routes.js
CHANGED
|
@@ -8,8 +8,8 @@ import { HANDOFF_DEFAULTS, HANDOFF_RULES, HANDOFF_SETTING_NAMES, HANDOFF_BODY_MA
|
|
|
8
8
|
// Keep older route order; dsh.js registers each exact path on the public host API.
|
|
9
9
|
export const LIBRARY_ROUTES=['/hanamesh/library','/hanamesh/library/sources','/hanamesh/library/install','/hanamesh/library/provision','/hanamesh/library/uninstall','/hanamesh/library/events','/hanamesh/library/installedPlugins','/hanamesh/library/plugins/install','/hanamesh/library/plugins/uninstall','/hanamesh/library/target','/hanamesh/library/install-target','/hanamesh/library/handoffs','/hanamesh/library/handoffs/state','/hanamesh/library/handoff-settings','/hanamesh/library/suite/upgrade','/hanamesh/library/installation-status'];
|
|
10
10
|
export const NODE_RUNTIME_ROUTES=['/hanamesh/library/node-runtime','/hanamesh/library/node-runtime/settings','/hanamesh/library/node-runtime/plan','/hanamesh/library/node-runtime/prepare','/hanamesh/library/node-runtime/cancel'];
|
|
11
|
-
LIBRARY_ROUTES.push(
|
|
12
|
-
const READ_ROUTES=new Set(['/hanamesh/library/suite-source','/hanamesh/library/node-runtime','/hanamesh/library','/hanamesh/library/sources','/hanamesh/library/events','/hanamesh/library/installedPlugins','/hanamesh/library/target','/hanamesh/library/handoffs','/hanamesh/library/handoff-settings']);
|
|
11
|
+
LIBRARY_ROUTES.push('/hanamesh/library/version-target','/hanamesh/library/version/install','/hanamesh/library/suite/status','/hanamesh/library/versions',...NODE_RUNTIME_ROUTES,'/hanamesh/library/suite-source');
|
|
12
|
+
const READ_ROUTES=new Set(['/hanamesh/library/version-target','/hanamesh/library/suite/status','/hanamesh/library/versions','/hanamesh/library/suite-source','/hanamesh/library/node-runtime','/hanamesh/library','/hanamesh/library/sources','/hanamesh/library/events','/hanamesh/library/installedPlugins','/hanamesh/library/target','/hanamesh/library/handoffs','/hanamesh/library/handoff-settings']);
|
|
13
13
|
async function jsonBody(req){
|
|
14
14
|
requireCondition(req.headers['content-type']?.split(';')[0].trim()==='application/json','CONTENT_TYPE_REQUIRED','Expected application/json.',{},415);
|
|
15
15
|
const chunks=[];let total=0;for await(const chunk of req){total+=chunk.length;requireCondition(total<=65_536,'BODY_TOO_LARGE','Request body is too large.',{},413);chunks.push(chunk);}
|
|
@@ -44,7 +44,7 @@ export function createLibraryHttpHandler(library,{parentOrigin,transportOrigin,a
|
|
|
44
44
|
requireCondition(subject&&typeof subject.principalId==='string','UNAUTHENTICATED','Host authentication is required.',{},401);
|
|
45
45
|
if(readOnly)requireCondition(new Set(url.searchParams.keys()).size===[...url.searchParams.keys()].length,'INVALID_INPUT','Duplicate query fields are not accepted.');
|
|
46
46
|
const input=readOnly?Object.fromEntries(url.searchParams):await jsonBody(req);
|
|
47
|
-
if(readOnly&&
|
|
47
|
+
if(readOnly&&['/hanamesh/library/target','/hanamesh/library/version-target'].includes(path)&&input.installTarget!==undefined){try{input.installTarget=JSON.parse(input.installTarget);}catch{throw new AppHostError('INVALID_INPUT','installTarget must contain a structured catalog target.');}}
|
|
48
48
|
requireCondition(await authorize(subject,path,input)===true,'FORBIDDEN','Operation is not authorized.',{},403);
|
|
49
49
|
let result;
|
|
50
50
|
if(NODE_RUNTIME_ROUTES.includes(path)){
|
|
@@ -70,6 +70,16 @@ export function createLibraryHttpHandler(library,{parentOrigin,transportOrigin,a
|
|
|
70
70
|
else if(path==='/hanamesh/library/handoffs/state'){send(res,200,{...handoff.queue.report(input),traceId});return;}
|
|
71
71
|
else if(path==='/hanamesh/library/handoff-settings'&&readOnly){requireCondition(Object.keys(input).length===0,'UNKNOWN_FIELDS','handoff-settings takes no parameters.');result={settings:library.handoffSettings(),defaults:HANDOFF_DEFAULTS,names:HANDOFF_SETTING_NAMES,rules:HANDOFF_RULES,listener:handoff.listener.state()};}
|
|
72
72
|
else if(path==='/hanamesh/library/handoff-settings'){const settings=await library.replaceHandoffSettings(input);send(res,200,{settings,defaults:HANDOFF_DEFAULTS,names:HANDOFF_SETTING_NAMES,rules:HANDOFF_RULES,listener:await handoff.restart(),traceId});return;}
|
|
73
|
+
else if(path==='/hanamesh/library/suite/status'){requireCondition(Object.keys(input).length===0,'INVALID_INPUT','Suite status takes no parameters.');result=await library.suiteStatus();}
|
|
74
|
+
else if(path==='/hanamesh/library/version-target')result=await library.resolveVersionTarget(input);
|
|
75
|
+
else if(path==='/hanamesh/library/version/install')result=await library.installVersion(input);
|
|
76
|
+
else if(path==='/hanamesh/library/versions'){
|
|
77
|
+
// Same transport lifetime as the existing read-only installation-status route.
|
|
78
|
+
const controller=new AbortController(),abort=()=>{if(!res.writableEnded)controller.abort();};req.once('aborted',abort);res.once('close',abort);
|
|
79
|
+
try{if(req.aborted||res.destroyed)controller.abort();result=await library.versions(input,{signal:controller.signal});if(controller.signal.aborted)return;}
|
|
80
|
+
catch(error){if(controller.signal.aborted)return;throw error;}
|
|
81
|
+
finally{req.off('aborted',abort);res.off('close',abort);}
|
|
82
|
+
}
|
|
73
83
|
else if(path==='/hanamesh/library/target')result=await library.resolveTarget(input);
|
|
74
84
|
else if(path==='/hanamesh/library'&&readOnly){requireCondition(Object.keys(input).every(key=>['q','category','cursor'].includes(key)),'UNKNOWN_FIELDS','Unknown library parameters.');result=await library.list(input);}
|
|
75
85
|
else if(path==='/hanamesh/library/sources'&&readOnly)result=library.sources();
|
package/dist/library/service.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { AppHostError, requireCondition, SerialQueue } from '../errors.js';
|
|
3
4
|
import { loadCatalog, isApplication, itemKind, upgradeAvailable, validateManifestUrl } from './catalog.js';
|
|
@@ -22,13 +23,16 @@ const npmUpgradeAvailable=(target,row)=>Boolean(target?.source==='npm'&&row?.ver
|
|
|
22
23
|
* `client`: which client this host is ('dsh' | 'hanamesh', see client-identity.js). `unavailable`: when there is no
|
|
23
24
|
* installer, the facts that are missing, in words a person can act on (`[{need, how}]`); probes and 503s carry them.
|
|
24
25
|
*/
|
|
25
|
-
export function createLibraryService({domain,host,config={},dataRoot,ledgerReader,installer,client='dsh',unavailable=()=>[],installationStatus,suiteSourceService=()=>undefined,logger}){
|
|
26
|
+
export function createLibraryService({domain,host,config={},dataRoot,ledgerReader,installer,client='dsh',unavailable=()=>[],installationStatus,versionCatalog,suiteSourceService=()=>undefined,logger}){
|
|
26
27
|
const warn=(message,...args)=>{if(typeof logger?.warn==='function')logger.warn(message,...args);else console.warn(message,...args);};
|
|
27
28
|
const ready=()=>typeof installer?.install==='function'&&installer.ready!==false;
|
|
28
29
|
let suiteStatus={status:'unknown',version:null,components:[]};
|
|
29
30
|
const suiteFacts=async()=>installer?.suiteFacts?await installer.suiteFacts():{...await readSuiteStatus(config.profileDir),status:'unknown',completeness:'unknown',blocked:[{code:'CORE_SUITE_DECLARATION_MISSING'}],suiteUpgradeUnavailable:CORE_SUITE_DECLARATION_MISSING};
|
|
30
31
|
const events=[];let sequence=0;let state;const running=new Map(),installing=new Map();let bootDependencies;
|
|
31
32
|
const eventRetention=config.eventRetention??512;
|
|
33
|
+
const versionLookupConcurrency=config.versionLookupConcurrency??4,versionLookupCacheMs=config.versionLookupCacheMs??30000;
|
|
34
|
+
requireCondition(Number.isSafeInteger(versionLookupConcurrency)&&versionLookupConcurrency>=1&&versionLookupConcurrency<=16,'INVALID_INPUT','library.versionLookupConcurrency must be an integer from 1 to 16.');
|
|
35
|
+
requireCondition(Number.isSafeInteger(versionLookupCacheMs)&&versionLookupCacheMs>0&&versionLookupCacheMs<=2147483647,'INVALID_INPUT','library.versionLookupCacheMs must be a positive timer budget.');
|
|
32
36
|
requireCondition(Number.isSafeInteger(eventRetention)&&eventRetention>0,'INVALID_INPUT','library.eventRetention must be a positive safe integer.');
|
|
33
37
|
const observers=new Set();
|
|
34
38
|
const emit=event=>{const value={sequence:++sequence,at:Date.now(),...event};events.push(value);if(events.length>eventRetention)events.shift();for(const observer of observers){try{observer(value);}catch(error){warn('hanamesh-app-host library: event observer failed (%s): %s',value.type,error?.message??error);}}return value;};
|
|
@@ -37,6 +41,13 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
37
41
|
const catalog=({q='',category='hanamesh-app',cursor}={})=>loadCatalog({fixture:config.fixture,sources:state.sources,fetchImpl:config.fetchImpl,query:q,category,cursor});
|
|
38
42
|
async function installed(){return config.profileDir?await scanInstalled({profileDir:config.profileDir,host,ledgerReader,dataRoot}):[];}
|
|
39
43
|
async function installedPlugins(){return config.profileDir?await scanInstalledPlugins({profileDir:config.profileDir,bootDependencies}):[];}
|
|
44
|
+
function suiteVersionCatalog(){
|
|
45
|
+
if(state.suiteSource?.mode!=='core')return versionCatalog;
|
|
46
|
+
const source=readSuiteSourceState(suiteSourceService());
|
|
47
|
+
requireCondition(source.status==='ready'&&source.registry,'CORE_SUITE_SOURCE_NOT_READY','The selected suite registry is not ready.',{},409);
|
|
48
|
+
requireCondition(versionCatalog?.forRegistry,'PLUGIN_VERSIONS_UNAVAILABLE','The selected suite registry cannot be queried.',{},503);
|
|
49
|
+
return versionCatalog.forRegistry(source.registry);
|
|
50
|
+
}
|
|
40
51
|
const settingsWriter=new SerialQueue();
|
|
41
52
|
function persist(update){return settingsWriter.run(async()=>{const next={...update(state),revision:state.revision+1};await domain.global.set(next);state=next;});}
|
|
42
53
|
async function operation(type,task){const operationId=randomUUID();emit({type:`library.${type}-started`,operationId});const work=Promise.resolve().then(()=>task(operationId)).then(result=>{emit({type:`library.${type}-done`,operationId,result});return result;},error=>{
|
|
@@ -62,18 +73,19 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
62
73
|
if(now[result.packageName])bootDependencies[result.packageName]=now[result.packageName];else delete bootDependencies[result.packageName];
|
|
63
74
|
return{...result,restartBaseline:{updated:true}};
|
|
64
75
|
}
|
|
65
|
-
async function resolveTarget(input){
|
|
76
|
+
async function resolveTarget(input,versionSelection=false){
|
|
66
77
|
requireCondition(input&&typeof input==='object'&&!Array.isArray(input),'INVALID_INPUT','Only itemId and packageName are accepted.');
|
|
67
78
|
// rc.8: optional install-target contract declaration. Absent keeps the v1 semantics; anything but '1' is refused explicitly, before field checks.
|
|
68
79
|
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]});
|
|
69
|
-
requireCondition(Object.keys(input).every(key=>['itemId','packageName','contractVersion','installTarget'].includes(key)),'INVALID_INPUT','Only catalog identity and package installation target are accepted.');
|
|
80
|
+
requireCondition(Object.keys(input).every(key=>['itemId','packageName','contractVersion','installTarget'].includes(key)||versionSelection&&key==='version'),'INVALID_INPUT','Only catalog identity and package installation target are accepted.');
|
|
70
81
|
const{itemId,packageName}=input;
|
|
82
|
+
requireCondition(input.version===undefined||typeof input.version==='string'&&semver.valid(input.version)===input.version,'INVALID_INPUT','Select an exact published version.');
|
|
71
83
|
const expected=input.installTarget===undefined?undefined:packageInstallTarget(input.installTarget);
|
|
72
84
|
requireCondition(!expected||itemId!==undefined,'INVALID_INPUT','A structured installation target requires itemId.');
|
|
73
85
|
if(itemId===SUITE_TARGET){
|
|
74
86
|
requireCondition(packageName===undefined&&!expected,'INVALID_INPUT','套件升级不接受包名或任意替换参数。');
|
|
75
87
|
requireCondition(typeof installer?.prepareSuiteUpgrade==='function','SUITE_UPGRADE_UNAVAILABLE','当前客户端还不支持安全套件升级,请使用补齐命令。',{},503);
|
|
76
|
-
const plan=await installer.prepareSuiteUpgrade();
|
|
88
|
+
const plan=await installer.prepareSuiteUpgrade(input.version===undefined?undefined:{bundleVersion:input.version});
|
|
77
89
|
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}};
|
|
78
90
|
}
|
|
79
91
|
requireCondition(itemId!==undefined||packageName!==undefined,'INVALID_INPUT','itemId or packageName is required.');
|
|
@@ -81,18 +93,19 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
81
93
|
requireCondition(packageName===undefined||(text(packageName,214)&&PACKAGE_NAME.test(packageName)),'INVALID_INPUT','packageName is invalid.');
|
|
82
94
|
async function resolved(found,source){
|
|
83
95
|
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)
|
|
96
|
+
const targets=catalogPackageTargets(found);let selected=targets.find(target=>expected?sameInstallTarget(input.version&&target.source==='npm'?{...target,version:input.version}:target,expected):packageName===undefined||installTargetName(target)===packageName);
|
|
85
97
|
requireCondition(!expected||selected,'CATALOG_CHANGED','The reviewed installation source or build permission changed. Review the current target again.',{},409);
|
|
86
98
|
requireCondition(selected,'NOT_INSTALLABLE','This catalog entry has no fixed package installation target for the requested package.',{reason:'fixed-install-target-required'});
|
|
87
99
|
const name=installTargetName(selected);
|
|
100
|
+
if(input.version!==undefined){requireCondition(selected.source==='npm'&&versionCatalog,'PLUGIN_VERSIONS_UNAVAILABLE','Version selection is unavailable for this source.',{},503);await versionCatalog.select(name,input.version);selected={...selected,version:input.version};}
|
|
88
101
|
requireCondition(!protectedPackage(name),'PACKAGE_DENIED','The HanaMesh suite is not managed through the market.',{},403);
|
|
89
102
|
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===name)??null;
|
|
90
|
-
let upgrade=npmUpgradeAvailable(selected,row);
|
|
103
|
+
let upgrade=input.version!==undefined?row?.version!==input.version:npmUpgradeAvailable(selected,row);
|
|
91
104
|
if(selected?.source==='github'&&row){
|
|
92
105
|
const saved=await readProfileManifest(config.profileDir);
|
|
93
106
|
upgrade=saved.dependencies[name]!==pinnedGithubSpec(selected.repo,selected.ref,selected.path)||row.active===false;
|
|
94
107
|
}
|
|
95
|
-
const item={...found,package:{registry:selected.source,name},installTarget:selected,installed:row,upgradeAvailable:upgrade};
|
|
108
|
+
const item={...found,...(input.version?{selectedVersion:input.version}:{}),package:{registry:selected.source,name},installTarget:selected,installed:row,upgradeAvailable:upgrade};
|
|
96
109
|
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source,item:{...item,kind:itemKind(item)}};
|
|
97
110
|
}
|
|
98
111
|
const packageMatches=row=>row.package?.name===packageName||(row.installMethods??[]).some(method=>method.target&&(method.target.packageName??method.target.name)===packageName);
|
|
@@ -139,8 +152,26 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
139
152
|
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
153
|
return{...item,kind,application:isApplication(item),installed:row,upgradeAvailable:npmUpgradeAvailable(item.installTarget,row)};});
|
|
141
154
|
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}:{})});
|
|
155
|
+
// Version discovery is optional and starts only through the per-item read route after this list paints.
|
|
156
|
+
let coreSource;
|
|
157
|
+
if(state.suiteSource?.mode==='core')try{coreSource=readSuiteSourceState(suiteSourceService());}catch(error){coreSource={code:error.code??'CORE_SUITE_SOURCE_INVALID'};}
|
|
158
|
+
const versionQueries=versionCatalog?{concurrency:versionLookupConcurrency,cacheMs:versionLookupCacheMs,scope:JSON.stringify([state.revision,config.registry??'https://registry.npmjs.org/',state.suiteSource??null,coreSource??null])}:null;
|
|
142
159
|
const categories=[...new Set(page.items.flatMap(item=>item.categories??[]))].sort();
|
|
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};},
|
|
160
|
+
return{source:page.source,items,categories,...(versionQueries?{versionQueries}:{}),installed:installedRows,plugins:pluginRows,restartRequired:[...pluginRows,...installedRows].some(row=>RESTART_STATES.has(row.state)),...(bootSnapshotError?{restartBaseline:{known:false,code:bootSnapshotError}}:{}),page:page.page};},
|
|
161
|
+
async versions(input,{signal}={}){
|
|
162
|
+
requireCondition(input&&Object.keys(input).length===1&&typeof input.itemId==='string','INVALID_INPUT','Only itemId is accepted.');
|
|
163
|
+
requireCondition(versionCatalog,'PLUGIN_VERSIONS_UNAVAILABLE','Version discovery is unavailable.',{},503);
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
if(input.itemId==='hanamesh:suite'){
|
|
166
|
+
const releases=await suiteVersionCatalog().read(SUITE_BUNDLE,{signal}),installedVersion=(await suiteFacts()).version;
|
|
167
|
+
signal?.throwIfAborted();let latestVersion=releases.latestVersion;
|
|
168
|
+
if(latestVersion&&installedVersion&&semver.gt(latestVersion,installedVersion)){const plan=await installer.readSuiteUpgrade();signal?.throwIfAborted();latestVersion=plan.bundle.to;}
|
|
169
|
+
return{...releases,latestVersion,installedVersion,upgradeAvailable:Boolean(latestVersion&&installedVersion&&semver.gt(latestVersion,installedVersion))};
|
|
170
|
+
}
|
|
171
|
+
const {item}=await resolveTarget(input);requireCondition(item.installTarget.source==='npm','NOT_INSTALLABLE','Version selection requires an npm source.');
|
|
172
|
+
const releases=await versionCatalog.read(item.package.name,{signal}),installedVersion=item.installed?.version??null;signal?.throwIfAborted();
|
|
173
|
+
return{...releases,installedVersion,upgradeAvailable:Boolean(releases.latestVersion&&installedVersion&&semver.gt(releases.latestVersion,installedVersion))};
|
|
174
|
+
},
|
|
144
175
|
sources(){return{sources:structuredClone(state.sources),revision:state.revision};},
|
|
145
176
|
/** Suite package registry selection is independent of catalog manifest sources. */
|
|
146
177
|
suiteSourceSettings(){
|
|
@@ -160,6 +191,9 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
160
191
|
async replaceSources(input){requireCondition(Array.isArray(input?.sources)&&input.sources.length<=16,'INVALID_SOURCES','sources must be an array of at most 16 entries.');const sources=input.sources.map(source=>({manifestUrl:validateManifestUrl(source.manifestUrl).href,enabled:source.enabled===true}));requireCondition(sources.length===0||sources.filter(source=>source.enabled).length===1,'CATALOG_SOURCE_REQUIRED','Exactly one catalog source must be enabled.');await persist(current=>({...current,sources}));return this.sources();},
|
|
161
192
|
/** `{itemId, packageName?}`: applications and plugins take the same path (`dsh plugin add --save-exact`); also the upgrade path. */
|
|
162
193
|
resolveTarget,
|
|
194
|
+
resolveVersionTarget:input=>{requireCondition(typeof input?.version==='string','INVALID_INPUT','Select an exact version.');return resolveTarget(input,true);},
|
|
195
|
+
suiteStatus:()=>readSuiteStatus(config.profileDir),
|
|
196
|
+
async installVersion(input){requireInstaller('Version installation is unavailable.');requireCondition(typeof input?.version==='string'&&input.itemId!==SUITE_TARGET,'INVALID_INPUT','Select an exact catalog plugin version.');const {item}=await resolveTarget(input,true);if(present(item.installed)&&!item.upgradeAvailable)return{status:'already-installed',itemId:item.id,packageName:item.package.name};return installItem(item);},
|
|
163
197
|
/** Public discovery is metadata only: no profile, authentication or installed rows. */
|
|
164
198
|
installationStatus,
|
|
165
199
|
installTarget(){const need=ready()?[]:unavailable();return{client,ready:ready(),...(typeof installationStatus==='function'?{installationStatus:INSTALLATION_STATUS_CAPABILITY}:{}),...((suiteStatus.status!=='unknown'||suiteStatus.restartRequired)&&!installer?.suiteUpgradeUnavailable?{suite:structuredClone(suiteStatus)}:{}),installTarget:{contract:'hanamesh.install-target',contractVersion:INSTALL_TARGET_CONTRACT_VERSION,supported:[INSTALL_TARGET_CONTRACT_VERSION],fragmentPrefix:'#hanamesh-install?'},
|
package/dist/library/suite.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function readSuiteStatus(profileDir){
|
|
|
29
29
|
return{name,label,version:pkg?.name===name&&semver.valid(pkg.version)?pkg.version:null,active:selected};
|
|
30
30
|
}));
|
|
31
31
|
const version=bundle?.name===SUITE_BUNDLE&&semver.valid(bundle.version)?bundle.version:null;
|
|
32
|
-
return{status:version&&selected&&components.every(c=>c.version&&bundle.dependencies?.[c.name]===c.version)?'installed':'incomplete',version,components};
|
|
32
|
+
return{status:version&&selected&&components.every(c=>c.version&&typeof bundle.dependencies?.[c.name]==='string'&&semver.validRange(bundle.dependencies[c.name])&&semver.satisfies(c.version,bundle.dependencies[c.name]))?'installed':'incomplete',version,components};
|
|
33
33
|
}catch{return{status:'unknown',version:null,components:[]};}
|
|
34
34
|
}
|
|
35
35
|
export const CORE_SUITE_DECLARATION_MISSING='Core 未提供套件声明';
|
|
@@ -98,9 +98,9 @@ export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceM
|
|
|
98
98
|
requireCondition(current()===service,'CORE_SUITE_DECLARATION_MISSING',CORE_SUITE_DECLARATION_MISSING,{},503);
|
|
99
99
|
return validateDeclaration(result);
|
|
100
100
|
}
|
|
101
|
-
const get=async()=>{
|
|
101
|
+
const get=async({bundleVersion}={})=>{
|
|
102
102
|
const binding=sourceBinding();
|
|
103
|
-
const declaration=await read({...(binding.registry===undefined?{}:{registry:binding.registry}),...(profileName===undefined?{}:{profile:profileName})});
|
|
103
|
+
const declaration=await read({...(binding.registry===undefined?{}:{registry:binding.registry}),...(profileName===undefined?{}:{profile:profileName}),...(bundleVersion===undefined?{}:{bundleVersion})});
|
|
104
104
|
requireCondition(JSON.stringify(sourceBinding())===JSON.stringify(binding),'SUITE_PLAN_CHANGED','套件安装包来源已变化,请重新查看并确认。',{},409);
|
|
105
105
|
requireCondition(declaration.completeness==='complete','CORE_SUITE_CURRENT_INCOMPLETE','Core 套件完整性:'+declaration.completeness,{completeness:declaration.completeness,blocked:declaration.blocked},409);
|
|
106
106
|
let expected;try{const url=new URL(binding.registry);url.pathname=url.pathname.replace(/\/$/,'')+'/';expected=url.href;}catch{}
|
|
@@ -126,9 +126,9 @@ export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceM
|
|
|
126
126
|
* Core owns candidate selection, completeness, ranges and compatibility; this module only checks
|
|
127
127
|
* that an exact execution plan is well formed and confined to our three protected packages.
|
|
128
128
|
*/
|
|
129
|
-
export async function prepareSuiteUpgrade({coreSuitePlan}={}){
|
|
129
|
+
export async function prepareSuiteUpgrade({coreSuitePlan,bundleVersion}={}){
|
|
130
130
|
requireCondition(typeof coreSuitePlan==='function','CORE_SUITE_DECLARATION_MISSING',CORE_SUITE_DECLARATION_MISSING,{},503);
|
|
131
|
-
const plan=await coreSuitePlan();
|
|
131
|
+
const plan=await coreSuitePlan(bundleVersion===undefined?undefined:{bundleVersion});
|
|
132
132
|
requireCondition(plan!==undefined&&plan!==null,'CORE_SUITE_DECLARATION_MISSING',CORE_SUITE_DECLARATION_MISSING,{},503);
|
|
133
133
|
const invalid=()=>requireCondition(false,'CORE_SUITE_DECLARATION_INVALID','Core 套件声明无法用于精确安装。',{},503);
|
|
134
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();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** npm release discovery for the market; mutations still use the official DSH plugin CLI. */
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import {createRequire} from 'node:module';
|
|
4
|
+
import {readFile} from 'node:fs/promises';
|
|
5
|
+
import {dirname,join} from 'node:path';
|
|
6
|
+
import {pathToFileURL} from 'node:url';
|
|
7
|
+
import {AppHostError,requireCondition} from '../errors.js';
|
|
8
|
+
const packageName=value=>typeof value==='string'&&value.length<=214&&/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(value);
|
|
9
|
+
|
|
10
|
+
/** Resolve public host metadata from the running installation, never the plugin's development dependencies. */
|
|
11
|
+
export function hostVersionReader(anchor){
|
|
12
|
+
const resolver=createRequire(anchor);
|
|
13
|
+
return async name=>{
|
|
14
|
+
try{return JSON.parse(await readFile(resolver.resolve(name+'/package.json'),'utf8')).version;}catch{
|
|
15
|
+
try{let directory=dirname(resolver.resolve(name));for(;;){try{const value=JSON.parse(await readFile(join(directory,'package.json'),'utf8'));if(value.name===name)return value.version;}catch{}const parent=dirname(directory);if(parent===directory)break;directory=parent;}}catch{}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** List published releases and select an exact compatible release; network failures never become 'up to date'. */
|
|
22
|
+
export function createNpmVersionCatalog({registry='https://registry.npmjs.org/',fetchImpl=fetch,peerVersion=async()=>null,nodeVersion=process.versions.node,anchor,timeoutMs=30000,maxBytes=16777216}={}){
|
|
23
|
+
const url=new URL(registry);
|
|
24
|
+
requireCondition((url.protocol==='https:'||url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname))&&!url.username&&!url.password&&!url.search&&!url.hash,'INVALID_INPUT','Plugin registry must be a credential-free HTTPS or loopback URL.');
|
|
25
|
+
url.pathname=url.pathname.replace(/\/$/,'')+'/';
|
|
26
|
+
async function reason(record,peer){
|
|
27
|
+
if(!record?.dsh?.bundle?.patch)return 'PLUGIN_BUNDLE_MISSING';
|
|
28
|
+
if(record.engines?.node&&(!semver.validRange(record.engines.node)||!semver.satisfies(nodeVersion,record.engines.node)))return 'PLUGIN_NODE_INCOMPATIBLE';
|
|
29
|
+
if(record.engines!==undefined&&(!record.engines||typeof record.engines!=='object'||Array.isArray(record.engines)))return 'PLUGIN_METADATA_INVALID';
|
|
30
|
+
if(record.peerDependencies!==undefined&&(!record.peerDependencies||typeof record.peerDependencies!=='object'||Array.isArray(record.peerDependencies)))return 'PLUGIN_METADATA_INVALID';
|
|
31
|
+
const dshPeer=name=>name==='@deepseek-ai/dsh'||name.startsWith('@deepseek-ai/dsh-');
|
|
32
|
+
if(record.engines?.dsh!==undefined||Object.keys(record.peerDependencies??{}).some(dshPeer)){
|
|
33
|
+
const actual=await peer('@deepseek-ai/dsh');if(!anchor||!actual||!semver.valid(actual))return 'PLUGIN_PEER_UNKNOWN';
|
|
34
|
+
try{const resolver=createRequire(anchor),sdk=await import(pathToFileURL(resolver.resolve('@deepseek-ai/dsh-app-boot')).href);if(sdk.evaluatePluginCompatibility(record,{},actual))return 'PLUGIN_PEER_INCOMPATIBLE';}catch{return 'PLUGIN_PEER_UNKNOWN';}
|
|
35
|
+
}
|
|
36
|
+
for(const [name,range]of Object.entries(record.peerDependencies??{})){
|
|
37
|
+
if(dshPeer(name))continue;
|
|
38
|
+
const actual=await peer(name);
|
|
39
|
+
if(actual===null&&record.peerDependenciesMeta?.[name]?.optional===true)continue;
|
|
40
|
+
if(!actual||!semver.valid(actual))return 'PLUGIN_PEER_UNKNOWN';
|
|
41
|
+
if(typeof range!=='string'||!semver.validRange(range)||!semver.satisfies(actual,range))return 'PLUGIN_PEER_INCOMPATIBLE';
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
async function read(name,{signal}={}){
|
|
46
|
+
requireCondition(packageName(name),'INVALID_INPUT','A valid npm package name is required.');
|
|
47
|
+
const budget=AbortSignal.timeout(timeoutMs),readSignal=signal?AbortSignal.any([signal,budget]):budget;
|
|
48
|
+
try{
|
|
49
|
+
readSignal.throwIfAborted();
|
|
50
|
+
const response=await fetchImpl(new URL(encodeURIComponent(name),url).href,{method:'GET',credentials:'omit',redirect:'error',headers:{accept:'application/json'},signal:readSignal});
|
|
51
|
+
requireCondition(response.ok,'PLUGIN_VERSIONS_UNAVAILABLE','Could not read published plugin versions.',{status:response.status},502);
|
|
52
|
+
const chunks=[];let bytes=0;
|
|
53
|
+
for await(const chunk of response.body){bytes+=chunk.length;requireCondition(bytes<=maxBytes,'PLUGIN_VERSIONS_UNAVAILABLE','Plugin version metadata exceeds the configured limit.',{},502);chunks.push(chunk);}
|
|
54
|
+
const data=JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
55
|
+
requireCondition(data.name===name&&data.versions&&typeof data.versions==='object'&&!Array.isArray(data.versions),'PLUGIN_VERSIONS_UNAVAILABLE','The registry returned invalid plugin metadata.',{},502);
|
|
56
|
+
const versions=[],peers=new Map();const peer=name=>{if(!peers.has(name))peers.set(name,Promise.resolve().then(()=>peerVersion(name)));return peers.get(name);};
|
|
57
|
+
for(const version of Object.keys(data.versions).filter(v=>semver.valid(v)===v).sort(semver.rcompare)){
|
|
58
|
+
readSignal.throwIfAborted();
|
|
59
|
+
const record=data.versions[version];if(record?.name!==name||record.version!==version)continue;
|
|
60
|
+
const failure=record.deprecated?'PLUGIN_VERSION_DEPRECATED':await reason(record,peer);
|
|
61
|
+
versions.push({version,compatible:failure===null,...(failure?{reason:failure}:{})});
|
|
62
|
+
}
|
|
63
|
+
// Prereleases remain selectable; the default follows latest's stable/prerelease channel.
|
|
64
|
+
const latest=data['dist-tags']?.latest,prerelease=typeof latest==='string'&&semver.valid(latest)&&semver.prerelease(latest)!==null;
|
|
65
|
+
const latestVersion=versions.find(row=>row.compatible&&(prerelease||semver.prerelease(row.version)===null))?.version??null;
|
|
66
|
+
readSignal.throwIfAborted();return{packageName:name,registry:url.href,latestVersion,versions};
|
|
67
|
+
}catch(error){if(error instanceof AppHostError)throw error;throw new AppHostError('PLUGIN_VERSIONS_UNAVAILABLE','Could not read published plugin versions.',{cause:String(error.message??error)},502);}
|
|
68
|
+
}
|
|
69
|
+
return{read,forRegistry:value=>createNpmVersionCatalog({registry:value,fetchImpl,peerVersion,nodeVersion,anchor,timeoutMs,maxBytes}),async select(name,version){requireCondition(typeof version==='string'&&semver.valid(version)===version,'INVALID_INPUT','Select an exact published version.');const result=await read(name),row=result.versions.find(row=>row.version===version);requireCondition(row?.compatible,'PLUGIN_VERSION_INCOMPATIBLE','The selected version is unavailable or incompatible with this client.',{version,reason:row?.reason??'PLUGIN_VERSION_MISSING'},409);return row;}};
|
|
70
|
+
}
|
package/docs/CONTRACT.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
0.6.27:Desktop payload 独立 Node 的启动识别上限为 30 s(实测最慢冷启动 4.07 s 的 7 倍以上余量),超时或插件卸载即终止该子进程;版本、平台与非 Electron 校验不变。
|
|
2
|
+
|
|
3
|
+
0.6.26:Codex 官方 GitHub `git` 来源的精确 `.git` 后缀在市场映射与每项安装来源分别归一;只读覆盖扩大到 ChatGPT.app 内置 Codex CLI、预发布 CLI 版本、不存在的官方资料目录(未安装)和 HanaMesh 客户端读取的 DSH 桌面版磁盘 profile;`other-runtime` 的 kind 改名为 `other-client`。只读命令与来源冲突拒绝不变,见 INSTALLATION_STATUS.md。
|
|
4
|
+
|
|
1
5
|
0.6.7 adds the explicitly consented public installation-status capability on both existing library listeners. Full request/response, strict refusals, automatic official-reader discovery and minimum-version support and scope/lifecycle are defined in [INSTALLATION_STATUS.md](INSTALLATION_STATUS.md); it does not authorize installation or alter recording consent.
|
|
2
6
|
|
|
3
7
|
# 公开契约 v1(候选,0.1.0-rc.41;rc.41 卸载 runtime 时清理生成的 Python 字节码缓存、未知残留仍 fail-closed;rc.40 市场只读查询待启动实例,ready 后一次 resume;前台即时 heartbeat 与失效帧显式清理;rc.39 Market seat 内打开应用时将原有应用帧固定铺满原生窗口并覆盖 Settings,同一回执/租约/关闭动作;rc.38 投射时仅提升 shell.overlay 层级以盖过原生 Settings;rc.35 市场开关按钮即时反馈、owned 应用已退出时提前结束 TERM 等待并确认精确进程组终止;rc.34 内置市场消费 heartbeat/视图清理;rc.33 owned 回收与启动握手分阶段限时;rc.32 客户端 `market` 席位与 dshmarket 互斥、壳内重启改走 postMessage 桥(见「市场席位」);rc.28 HanaMesh 市场:插件与应用同路径装卸、已装含插件、可升级/需重启、`installedPlugins` 与 `plugins/*` 路由、`LIB-PROVISION-INPUT` 400(见「市场」);rc.27 应用使用证据经 usage 插件 record 座位上报(见「应用使用证据」);rc.26 应用库默认目录源 + 纯 DSH 位置推断(见「应用库配置」);rc.15 目录条目校验补 updatedAt;rc.16 应用库默认 category=hanamesh-app、q/cursor 透传;rc.13/rc.14 与 rc.12 契约相同:rc.13 补许可证/repository/精确 peer,rc.14 去掉客户端对 hanameshCore 的读取)
|
|
@@ -157,7 +161,7 @@ DSH 绑定必须提供同一个 `single` domain 的一次完整镜像 publish
|
|
|
157
161
|
| `/hanamesh/library/provision` | POST | `{appId, packageName, runtimeItem}` 三者必填 | 202;缺字段/非法 → 400 `INVALID_INPUT`(`LIB-PROVISION-INPUT`) |
|
|
158
162
|
| `/hanamesh/library/uninstall` | POST | `{appId, packageName, runtimeItem?}` | 202;缺字段/非法 → 400 `INVALID_INPUT` |
|
|
159
163
|
|
|
160
|
-
事件流 `library.install-*` 增加 `kind`;`install-done.result` 对插件为 `{status:'restart-required', kind:'plugin', packageName, version}`。受保护包(`@hanamesh/
|
|
164
|
+
事件流 `library.install-*` 增加 `kind`;`install-done.result` 对插件为 `{status:'restart-required', kind:'plugin', packageName, version}`。受保护包(`@hanamesh/hanamesh`、`@hanamesh/core`、`@hanamesh/usage`、`@hanamesh/dsh-app-host`、`@hanamesh/brand`)任何路径都 `PACKAGE_DENIED`:`plugins/*` 在启动操作前同步答 403,目录条目路径在操作内以 `-failed` 事件给出。「需重启」判定只来自本进程启动时的 profile 依赖快照(钉版本内核不暴露已加载插件集合);快照不可读时一切已装计为已加载。客户端提示完全退出并重新打开;本包不重启 DSH。
|
|
161
165
|
|
|
162
166
|
0.6.13 候选:套件 npm 身份为 `@hanamesh/core`、`@hanamesh/usage`、`@hanamesh/dsh-app-host`;Core 声明的 `core.name`、`moduleName`、升级目标和返回的 `packageName` 按这些新名精确比较,旧名声明不当作新三包。插件 id、`hanameshMarket` 服务、路由与存储键保持;普通 Recommender 不受保护、不加入套件。首装/补齐精确命令见 LIBRARY.md;已发布 0.6.12 原件不包含该适配。
|
|
163
167
|
|
|
@@ -290,3 +294,10 @@ for the reviewed `/catalog-source.json?view=hanamesh` provider. Other catalog
|
|
|
290
294
|
providers and unavailable/CORS-denied public reads retain “暂无”. The source's
|
|
291
295
|
browser access rules are respected; there is no proxy/CORS bypass. Closing or
|
|
292
296
|
switching details ignores stale replies.
|
|
297
|
+
|
|
298
|
+
## VR1 版本切换(2026-11-10)
|
|
299
|
+
|
|
300
|
+
新增市场本机版本路由、兼容检查、确认与恢复义务见 [LIBRARY.md 的 VR1 章节](LIBRARY.md#vr1-市场版本管理)。此能力不扩大 install-target v1/网站交接输入;普通安装协议仍不接受外部版本覆盖。总包依赖可为兼容范围,物理完整性按范围判断;Core 是总包兼容组合的唯一提供方,AppHost 只执行并读回。明确选择的旧总包版本在相同校验下可切换,默认操作仍只升级。
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
R168:后台套件版本展示使用不登记确认的 Core 只读组合查询,只有显式目标预览登记执行计划。后台完成或在Core读取期间取消,不替换已打开的默认/历史确认;planId、来源、tuple、物理baseline复读与单次确认保护保留。见 [后台查询与确认计划](LIBRARY.md#r168-后台套件查询与确认计划)。
|
|
@@ -37,7 +37,7 @@ interface InstallationStatus {
|
|
|
37
37
|
interface InstallationObservation {
|
|
38
38
|
channel:'hanamesh'|'dsh'|'claude'|'codex';
|
|
39
39
|
scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';
|
|
40
|
-
kind:'responding-runtime'|'
|
|
40
|
+
kind:'responding-runtime'|'other-client'|'official-user-profile'|'desktop-app';label:string};
|
|
41
41
|
status:'installed'|'not-installed'|'incomplete'|'unknown';
|
|
42
42
|
version:string|null;
|
|
43
43
|
versionKind:'suite-bundle'|'agent-distribution';
|
|
@@ -69,6 +69,7 @@ Selection is deterministic: the first executable in the inherited absolute PATH
|
|
|
69
69
|
| Homebrew / system | `/opt/homebrew/bin`, `/usr/local/bin`, `/home/linuxbrew/.linuxbrew/bin`, `/usr/bin`, `/bin`. [Homebrew prefixes](https://docs.brew.sh/Installation); [Claude Homebrew setup](https://code.claude.com/docs/en/setup), [Codex distribution](https://learn.chatgpt.com/docs/cli). Brew-linked native binaries use the same discovery path. |
|
|
70
70
|
| nvm / fnm / installed manager versions | `NVM_BIN`, `NVM_SYMLINK`; `NVM_DIR/versions/node/*/bin` or `~/.nvm/versions/node/*/bin`; Windows `NVM_HOME/*`; `FNM_MULTISHELL_PATH`; `FNM_DIR/node-versions/*/installation[/bin]`, `~/.local/share/fnm/node-versions/*/installation/bin`, `~/Library/Application Support/fnm/node-versions/*/installation/bin`, `%APPDATA%/fnm/node-versions/*/installation`; asdf `installs/nodejs/*/bin`, mise `installs/node/*/bin`. Only SemVer directory names are scanned. [nvm](https://github.com/nvm-sh/nvm), [fnm](https://github.com/Schniz/fnm/blob/master/docs/commands.md), [asdf](https://asdf-vm.com/manage/configuration.html), [mise](https://mise.jdx.dev/directories.html). |
|
|
71
71
|
| Windows native links / npm shims | `%LOCALAPPDATA%/Microsoft/WinGet/Links` ([Claude WinGet](https://code.claude.com/docs/en/setup)). `.exe` precedes `.cmd` per directory. A `.cmd` is never sent to a shell: resolve only the adjacent official npm `node_modules/@anthropic-ai/claude-code/cli.js` or `node_modules/@openai/codex/bin/codex.js`, run it with the verified standalone Node. Missing JS is a named failure, not a guessed shell command. |
|
|
72
|
+
| Desktop app bundle (macOS, Codex only, searched last) | `/Applications` and `~/Applications` `ChatGPT.app/Contents/Resources/codex-cli/bin/codex` (the ChatGPT desktop app ships the Codex CLI; it reads the same `CODEX_HOME` / `~/.codex`). A standalone CLI in any earlier layout wins. |
|
|
72
73
|
|
|
73
74
|
HOME is the host user's HOME/USERPROFILE. Honor existing `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment paths, otherwise use `~/.claude` / `~/.codex`. These directories must already exist; discovery never creates them, reads their contents, or reads credentials, sessions or private configuration files. Only official read commands observe plugin inventory. The named profile scope is not a machine-wide inventory. Electron hosts retain the existing verified standalone Node requirement.
|
|
74
75
|
|
|
@@ -78,12 +79,12 @@ Minimum CLI version and fixed syntax (no new upper bound):
|
|
|
78
79
|
|
|
79
80
|
| Channel | Minimum version / accepted `--version` syntax | Fixed subsequent read argv |
|
|
80
81
|
|---|---|---|
|
|
81
|
-
| Claude Code | `>=2.1.301`, `MAJOR.MINOR.PATCH (Claude Code)` | `plugin list --json`; `plugin marketplace list --json` |
|
|
82
|
-
| Codex CLI | `>=0.162.1`, `codex-cli MAJOR.MINOR.PATCH` | `plugin list --json --disable remote_plugin`; `plugin marketplace list --json --disable remote_plugin` |
|
|
82
|
+
| Claude Code | `>=2.1.301`, `MAJOR.MINOR.PATCH[-prerelease] (Claude Code)` | `plugin list --json`; `plugin marketplace list --json` |
|
|
83
|
+
| Codex CLI | `>=0.162.1`, `codex-cli MAJOR.MINOR.PATCH[-prerelease]` (desktop bundles report e.g. `0.167.0-alpha.3.2`; SemVer ordering applies, so `0.162.1-alpha.1` is below the minimum) | `plugin list --json --disable remote_plugin`; `plugin marketplace list --json --disable remote_plugin` |
|
|
83
84
|
|
|
84
85
|
Malformed version output is `AGENT_READER_VERSION_INVALID`; below-minimum versions are `AGENT_READER_VERSION_UNSUPPORTED`; JSON parse/shape failure is `AGENT_INVENTORY_INVALID`. New versions such as 2.1.319 / 0.167.0 must pass exactly the existing inventory validation.
|
|
85
86
|
|
|
86
|
-
The Codex flag was checked through this exact CLI's public `features list`; it excludes remote directory reads. Passing the version floor is necessary but not sufficient: both JSON shapes must validate. Claude uses the official installed array's `id`, user `scope`, enabled/version and load diagnostics. Other project-only target rows are unknown for this reader. Codex uses the official `installed` array and its qualified identity, installed/enabled/version. The exact target marketplace must match the registered HTTPS source in marketplace list; Codex's per-install `marketplaceSource` must also be present, supported and consistent. A local/different/unreadable source, duplicate/conflicting target or malformed/truncated/empty raw output is unknown. Only a complete valid clean inventory with no target in the observed user scope is not-installed. A known install may have version null; consumers must show version unknown. Official load/payload issues produce incomplete, not successful readiness. No plugin MCP/hook definitions, unrelated inventory or CLI raw diagnostics are returned.
|
|
87
|
+
The Codex flag was checked through this exact CLI's public `features list`; it excludes remote directory reads. Passing the version floor is necessary but not sufficient: both JSON shapes must validate. Claude uses the official installed array's `id`, user `scope`, enabled/version and load diagnostics. Other project-only target rows are unknown for this reader. Codex uses the official `installed` array and its qualified identity, installed/enabled/version. The exact target marketplace must match the registered HTTPS source in marketplace list; Codex's per-install `marketplaceSource` must also be present, supported and consistent. For official GitHub `sourceType:'git'` records, the exact credential/query/fragment-free `https://github.com/owner/repository.git` spelling identifies the same repository as the registered suffix-free URL. Normalize both the marketplace mapping and the per-install source independently; no other URL rewrite, repository substitution or source fallback is accepted. A local/different/unreadable source, duplicate/conflicting target or malformed/truncated/empty raw output is unknown. Only a complete valid clean inventory with no target in the observed user scope is not-installed. A known install may have version null; consumers must show version unknown. Official load/payload issues produce incomplete, not successful readiness. No plugin MCP/hook definitions, unrelated inventory or CLI raw diagnostics are returned.
|
|
87
88
|
|
|
88
89
|
Commands run with an empty stdin in a disposable neutral cwd, only named HOME/config/platform paths, only the selected inherited/recovered absolute PATH plus allowlisted manager and standard XDG directory variables, no inherited model/API keys, proxy/auth/askpass/Node preload, and disabled Git credential helpers. The verified Node and selected executable directories prefix the selected PATH; retain its manager binary directories, so an official asdf shim can resolve `asdf`. Preserve existing XDG_CONFIG_HOME/CACHE_HOME/DATA_HOME/STATE_HOME/RUNTIME_DIR and CONFIG_DIRS/DATA_DIRS; absent variables stay absent and retain manager OS defaults. The supervisor never overwrites XDG paths with empty temporary directories. Only TMPDIR/TMP/TEMP and cwd use the owned disposable directory. Manager paths additionally include ASDF_CONFIG_FILE, MISE_CONFIG_DIR/GLOBAL_CONFIG_FILE/CACHE_DIR/STATE_DIR alongside the earlier manager-home keys; credentials, proxy, Node preloads and askpass remain excluded. Recovered shell manager/XDG paths follow the same allowlist; there is no fallback to a different installation. No model/install/upgrade/marketplace refresh command runs. On POSIX, a detached IPC reaper owns the neutral cwd outside a separate live command-group anchor. The anchor remains alive until output closes; abort, budget exhaustion, overflow, provider disposal or parent IPC disconnect makes the surviving reaper terminate only that live anchor's group. The reaper waits for group close before removing the cwd, so active writers cannot race deletion, including after caller death. No released PID/PGID or arbitrary process name is used. The caller also performs exact-path cleanup when alive. On Windows the same live anchor/reaper ownership uses the OS taskkill tree operation for its precise owned anchor; no process-name match or command-shell execution. Windows/Linux runtime evidence is separate from macOS fixture execution. There is no retry or stale-result cache. Automatic recovery after an internal reaper/anchor itself is externally SIGKILLed or suffers a native crash is outside this lifecycle guarantee; an external intervention can leave command processes or temporary files and is never reported as an installed/absent inventory.
|
|
89
90
|
|
|
@@ -94,7 +95,11 @@ HTTP refusals: `HOST_DENIED`, `ORIGIN_DENIED`, `FRAME_CONTROL_DENIED`, `CREDENTI
|
|
|
94
95
|
Per-scope unknown reasons always include a safe code/message/action:
|
|
95
96
|
|
|
96
97
|
- `CORE_SUITE_DECLARATION_MISSING`, `CORE_SUITE_DECLARATION_INVALID`, `CORE_SUITE_READ_FAILED`, `CORE_SUITE_DECLARATION_UNKNOWN`; incomplete: `CORE_SUITE_CURRENT_INCOMPLETE`, plus Core blocked codes.
|
|
97
|
-
- `OTHER_RUNTIME_NOT_OBSERVED`, `CODEX_DESKTOP_SCOPE_UNOBSERVABLE`.
|
|
98
|
+
- `OTHER_RUNTIME_NOT_OBSERVED`, `CODEX_DESKTOP_SCOPE_UNOBSERVABLE`, `DSH_PROFILE_UNREADABLE`; incomplete: `DSH_PROFILE_SUITE_INCOMPLETE`.
|
|
99
|
+
|
|
100
|
+
Authoritative absence (read-only, no CLI execution): an official user profile directory (`CLAUDE_CONFIG_DIR` / `CODEX_HOME`, else `~/.claude` / `~/.codex`) that does not exist holds no user-scope plugin, so that channel is `not-installed` with the registered identity, whether or not the CLI is installed. An existing but inaccessible directory stays `AGENT_PROFILE_UNAVAILABLE`; an existing profile without a readable CLI stays `AGENT_READER_UNAVAILABLE`.
|
|
101
|
+
|
|
102
|
+
The HanaMesh client's `other-runtime` row (`kind:'other-client'`) reads the official DSH desktop profile on disk at the upstream default home (`~/.dsh/profiles/desktop`, `@deepseek-ai/dsh-home-paths` `.dsh`): after distinct physical profile identities have been established, a missing profile manifest or no `@hanamesh/hanamesh` dependency is `not-installed`; the bundle and all three members resolved at the bundle's exact versions and selected in `dsh.profile.bundles` is `installed` with the bundle version (a physical inventory, not that runtime's Core completeness); otherwise `incomplete`; unreadable JSON is unknown. Before every explicit read, resolve both the current profile and the target with `realpath`; the reused reader retains a selector, never a cached identity decision. The same physical directory (including symlink aliases), a missing current profile identity, or any identity-resolution failure keeps the row `unknown / OTHER_RUNTIME_NOT_OBSERVED`; no target inventory is read. A missing target directory also fails this identity check conservatively. A DSH listener never reports the HanaMesh client: that row is always `OTHER_RUNTIME_NOT_OBSERVED`.
|
|
98
103
|
- `AGENT_READER_VERSION_INVALID`, `AGENT_READER_VERSION_UNSUPPORTED`, `AGENT_READER_NODE_REQUIRED`, `AGENT_SHELL_ENV_UNAVAILABLE`, `AGENT_READER_ENTRYPOINT_UNAVAILABLE`, `AGENT_READER_DISCOVERY_INVALID`.
|
|
99
104
|
- `AGENT_PROFILE_UNAVAILABLE`, `AGENT_READER_UNAVAILABLE`, `AGENT_READER_PERMISSION_DENIED`, `AGENT_READER_TIMEOUT`, `AGENT_READER_OUTPUT_LIMIT`, `AGENT_READER_DIAGNOSTIC`, `AGENT_READER_CLEANUP_FAILED`, `AGENT_READER_FAILED`.
|
|
100
105
|
- `AGENT_INVENTORY_INVALID`, `AGENT_INVENTORY_CONFLICT`, `AGENT_SCOPE_UNOBSERVABLE`, `AGENT_MARKETPLACE_SOURCE_UNAVAILABLE`, `AGENT_MARKETPLACE_SOURCE_MISMATCH`.
|
package/docs/LIBRARY.md
CHANGED
|
@@ -85,3 +85,33 @@ D1: DSH suite installation and upgrades use only @hanamesh/hanamesh. Core owns t
|
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
A2 0.6.22:普通插件必须有 Indexer 已审核的 installMethods 和 oneClickInstall.allowed:true;仅执行其 npm 精确版本或完整 GitHub SHA。缺目标具名拒绝,不根据包名查可变版本标签。CLI 使用显式 library.registry(若有),否则保留宿主原生 registry 配置。删除普通插件按包名内部直装方法、元数据查询及其专用设置/规则加载。套件仅透传 Core installCommand。安装失败保留脱敏 CLI/读回诊断及补偿结果;精确版本发布后即可原生验收。
|
|
88
|
+
|
|
89
|
+
## VR1 市场版本管理
|
|
90
|
+
|
|
91
|
+
应用库/应用详情显示已装版本与最新兼容版本,一键升级默认选择该精确版本;“选择历史版本”列出发布版本,不兼容版本禁用。npm 发布元数据只用于发现,版本选择再次验证 Node、可观察 peer 和公开 DSH `evaluatePluginCompatibility`,缺失事实不会启用安装。总包继续由 Core 的 `getSuiteDeclaration({registry,profile,bundleVersion})` 决定三个业务组件的兼容组合。Theme 为总包分发依赖,不增加业务成员。
|
|
92
|
+
|
|
93
|
+
新增插件自有的认证同源路由:GET `/hanamesh/library/versions?itemId=...`、GET `/hanamesh/library/version-target?itemId=...&version=...`、POST `/hanamesh/library/version/install`。后者只接受当前目录身份、精确版本及复读的安装目标,不接受 URL、命令或浏览器提供的 registry。已确认的总包仍 POST `/hanamesh/library/suite/upgrade`,GET `/hanamesh/library/suite/status` 返回从总包解析的物理组件读回。原 install-target v1 的 `/target` 与 `/install` 继续拒绝任意 version 字段;网站交接不改变。
|
|
94
|
+
|
|
95
|
+
普通插件安装/升级/历史切换均复用 `dsh plugin --profile <实际profile> add --save-exact <name>@<version>`。总包执行 Core 提供的精确 argv;执行前按相同的显式版本请求复读来源、声明与物理 baseline。命令运行、精确读回或组件验证失败,先通过同一正式 CLI 补偿,再逐项比较依赖、包、选择与锁;原失败和恢复失败都可见,未核实不报告恢复成功。不会自动重试,不直接写 DSH 的 profile/lock,不全局放宽成熟期。安装后提示完全退出并重开;读回使用总包内部解析的真实子包,而非要求子包成为 profile 直接依赖。
|
|
96
|
+
|
|
97
|
+
版本查询使用与安装一致的配置 registry;总包选择 Core 来源时从其公开 source state 读取并拒绝未就绪状态。`library.versionLookupTimeoutMs`(默认 30000)与 `library.versionLookupMaxBytes`(默认 16777216)限制每次匿名、拒绝重定向的读取。查询仅发生于页面上已装 npm 项和主动展开版本,不遍历整个市场;失败显示错误。
|
|
98
|
+
|
|
99
|
+
参考:dsh-market `f76b5f58ec553aba91b964f009a8561eea175011` 的 `src/updates.ts`、`src/routes.ts`、`src/official-desktop.ts`;DSH 0.2.2-rc.1 `dsh-plugin-manager` README 的 `listVersions`/`updateBundle`、`dsh-client-ui-plugin-manager` 版本确认 UI。DSH 的版本接口只列新版,`updateBundle` 拒绝旧版;本市场的完整历史切换沿用已有受监测正式 CLI,不移除旧包后再装,也不修改 DSH 本体。
|
|
100
|
+
|
|
101
|
+
原生验收补充:总包确认页单独显示精确总包的已装→目标版本,即使三个业务成员未变。正式安装 CLI 保留宿主经绝对路径校验的 XDG_DATA_HOME,使 pnpm 沿用该 profile 首次安装的数据根;不转发任意环境、API token 或 proxy,不改 store、不重装依赖以掩盖 store 冲突。
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## R162 首屏与保护边界
|
|
105
|
+
|
|
106
|
+
当前预装Brand包名为 `@hanamesh/brand`,与总包、Core、Usage、AppHost同属普通市场保护名单;已装事实可以读到它,但不提供卸载入口,服务与安装器都在执行正式CLI之前拒绝装卸。退休Brand包名不保留保护别名;普通第三方插件仍可卸载。
|
|
107
|
+
|
|
108
|
+
目录与磁盘已装事实先返回,首屏不等待npm版本查询。客户端随后逐项调用现有 `/hanamesh/library/versions`;同一页面以 `library.versionLookupConcurrency`(默认4,整数1–16)限制在途查询,按项显示最新兼容版本或具名错误。`library.versionLookupCacheMs`(默认30000ms)仅复用同来源、同已装版本的展示结果和在途请求;过期后下一次明确加载才查询,不定时自动重试。历史下拉复用当前新鲜结果;来源或已装版本变化会失效,离开页面/卸载组件取消在途及排队项,旧响应不回写新来源。
|
|
109
|
+
|
|
110
|
+
浏览器取消经既有HTTP断连生命周期传给registry fetch;原单次timeout、响应预算、匿名和拒绝重定向规则仍生效。展示缓存不进入安装授权:确认和安装仍重新读取精确目标、Core组合与物理baseline。借用dsh-market同一调研SHA的 `MarketSection.tsx` 分离installed/updates读取与展示缓存思路;HTTP取消复用本仓 `installation-status-http.js` 的res close/req aborted模式,不引入上游没有的DSH API。
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## R168 后台套件查询与确认计划
|
|
114
|
+
|
|
115
|
+
后台 `/hanamesh/library/versions` 通过安装器内部只读组合入口读取同一 Core 兼容声明,不登记 planId,不写入或替换用户确认计划。只有用户显式打开默认或历史目标确认时才登记新的执行计划;展示查询完成、失败或在 Core 读取期间取消,都不得使已打开的确认失效。Core 公开协议没有传入取消信号的接口;取消后丢弃展示结果,不虚构该接口,也不依赖撤销已登记计划。
|
|
116
|
+
|
|
117
|
+
执行仍验证本人 planId,并重新核对来源、Core声明/精确tuple与物理baseline;确认只可使用一次。该分离不缓存执行权限,也不删除 `SUITE_PLAN_CHANGED` 保护。回归 `tests/suite-query-race.test.mjs` 将真实 service、installer 与公开Core适配器连接起来,挂起后台metadata/Core读取后分别确认默认/历史目标,并验证取消后原确认仍执行原精确版本。
|
package/docs/LICENSES.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": {
|
|
3
3
|
"name": "@hanamesh/dsh-app-host",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.27",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"source": "git+https://github.com/yzsnstotz/hanamesh-plugin-app-host.git",
|
|
7
7
|
"purpose": "Application ownership, view leases, Router forwarding, and application library for official DSH"
|
|
@@ -352,12 +352,13 @@
|
|
|
352
352
|
"version": "0.2.2-rc.1",
|
|
353
353
|
"license": "MIT",
|
|
354
354
|
"source": "https://registry.npmjs.org/@deepseek-ai/dsh-app-boot/-/dsh-app-boot-0.2.2-rc.1.tgz",
|
|
355
|
-
"purpose": "
|
|
355
|
+
"purpose": "Public host SDK for DSH compatibility evaluation; exact development input.",
|
|
356
356
|
"usedAs": [
|
|
357
357
|
"devDependencies"
|
|
358
358
|
],
|
|
359
359
|
"specs": {
|
|
360
|
-
"devDependencies": "0.2.2-rc.1"
|
|
360
|
+
"devDependencies": "0.2.2-rc.1",
|
|
361
|
+
"peerDependencies": ">=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0 || >=0.2.2-0 <0.3.0-0"
|
|
361
362
|
}
|
|
362
363
|
},
|
|
363
364
|
"@deepseek-ai/dsh-client-ui-primitives": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanamesh/dsh-app-host",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.27",
|
|
4
4
|
"description": "HanaMesh application ownership, durable view leases, and application library for official DSH 0.2.x",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -120,7 +120,8 @@
|
|
|
120
120
|
"@deepseek-ai/dsh-client-ui-renderer": ">=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0 || >=0.2.2-0 <0.3.0-0",
|
|
121
121
|
"@deepseek-ai/dsh-client-ui-settings": ">=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0 || >=0.2.2-0 <0.3.0-0",
|
|
122
122
|
"@deepseek-ai/dsh-client-locale": ">=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0 || >=0.2.2-0 <0.3.0-0",
|
|
123
|
-
"react": "^18.3.1"
|
|
123
|
+
"react": "^18.3.1",
|
|
124
|
+
"@deepseek-ai/dsh-app-boot": ">=0.2.0-rc.2 <0.3.0-0 || >=0.2.1-0 <0.3.0-0 || >=0.2.2-0 <0.3.0-0"
|
|
124
125
|
},
|
|
125
126
|
"hanamesh": {
|
|
126
127
|
"module": "app-host",
|