publishport-opencli 1.0.15 → 1.0.16
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/clis/oschina/article.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CliError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{readFile as q,stat as T}from"node:fs/promises";import{publishArticle as
|
|
1
|
+
import{CliError as W,CommandExecutionError as F}from"@jackwener/opencli/errors";import{cli as O,Strategy as _}from"@jackwener/opencli/registry";import{readFile as q,stat as T}from"node:fs/promises";import{publishArticle as j}from"../_shared/article/publish.js";export const oschinaProfile={home:"https://my.oschina.net",outputFormat:"markdown",image:{spec:{url:"https://apiv1.oschina.net/oschinapi/ai/creation/project/uploadDetail",method:"POST",bodyType:"binary-multipart",fileField:"file",fileName:"image",body:{},responsePath:"result"},skip:["apiv1.oschina.net","oscimg.oschina.net","static.oschina.net"]},checkAuth:async()=>{const G=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!G.ok)return{isAuthenticated:!1,error:"HTTP "+G.status};let z=null;try{z=await G.json()}catch(V){return{isAuthenticated:!1,error:"解析响应失败"}}if(!z||!z.success||!z.result||!z.result.userId)return{isAuthenticated:!1,error:z&&z.message||"未登录"};const B=String(z.result.userId),K=z.result.userVo&&z.result.userVo.name||B,M=z.result.userVo&&z.result.userVo.portraitUrl||"";return{isAuthenticated:!0,userId:B,username:K,avatar:M}},publish:async(G)=>{const z=G.params||{};let B="",K="";try{const H=await fetch("https://apiv1.oschina.net/oschinapi/user/myDetails",{credentials:"include"});if(!H.ok)return{ok:!1,stage:"auth",status:H.status,message:"获取开源中国用户信息失败:HTTP "+H.status+"(登录态可能已失效,请在客户端重新登录)"};const L=await H.json();if(L&&L.success&&L.result&&L.result.userId)B=String(L.result.userId);else K=L&&L.message||"myDetails 未返回 userId(登录态可能已失效或为匿名)"}catch(H){return{ok:!1,stage:"auth",message:"校验开源中国登录态时请求异常:"+String(H&&H.message||H)}}if(!B)return{ok:!1,stage:"auth",message:"未登录开源中国,无法获取用户 ID"+(K?":"+K:"")};let M=0;if(z.category){let H=[];try{const Y=await(await fetch("https://apiv1.oschina.net/oschinapi/blog_catalog/list_by_user",{credentials:"include"})).json();H=Y&&Y.result||[]}catch(J){return{ok:!1,stage:"category",message:"获取开源中国博客分类失败:"+String(J&&J.message||J)}}const L=H.find((J)=>String(J.name)===String(z.category));if(!L)return{ok:!1,stage:"category",message:"未找到博客分类「"+z.category+"」,可选:"+(H.map((J)=>J.name).join(" / ")||"(你还没有任何分类)")};M=L.id}if(G.draftOnly){const H=await fetch("https://apiv1.oschina.net/oschinapi/api/draft/save_draft",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,user:Number(B),content:G.content,contentType:1,catalog:M,originUrl:"",privacy:!0,disableComment:!1})}),L=await H.text();let J=null;try{J=JSON.parse(L)}catch(S){}if(!H.ok||!J||!J.success||!J.result||!J.result.id)return{ok:!1,stage:"save_draft",status:H.status,message:J&&J.message||L.slice(0,300)};const Y=String(J.result.id);return{ok:!0,draft:!0,id:Y,url:"https://my.oschina.net/u/"+B+"/blog/ai-write/draft/"+Y}}const V=await fetch("https://apiv1.oschina.net/oschinapi/blog/web/add",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({title:G.title,content:G.content,contentType:1,type:"1",originUrl:"",catalog:M,privacy:!1,disableComment:!1,isAiRelated:!1,user:Number(B)})}),Q=await V.text();let U=null;try{U=JSON.parse(Q)}catch(H){}if(!V.ok||!U||U.code!==200||U.result==null)return{ok:!1,stage:"publish",status:V.status,message:U&&(U.message||U.msg)||Q.slice(0,300)};const X=String(U.result);return{ok:!0,draft:!1,id:X,url:"https://my.oschina.net/u/"+B+"/blog/"+X}}};function Z(G){if(!G.execute)throw new W("INVALID_INPUT","发布开源中国文章需要 --execute 参数。注意:本报错仅表示缺少 --execute,并未校验其余参数,不能当 dry-run 预览用")}function $(G,z,B,K,M={}){return[{status:"success",outcome:K,message:G,target_type:z,target:B,...M}]}async function A(G){const z=typeof G.text==="string"?G.text:void 0,B=typeof G.file==="string"?G.file:void 0;if(z&&B)throw new W("INVALID_INPUT","正文和文件路径只能二选一,不能同时指定");let K=z??"";if(B){let M;try{M=await T(B)}catch{throw new W("INVALID_INPUT","文件不存在:"+B)}if(!M.isFile())throw new W("INVALID_INPUT","必须指定可读的文本文件:"+B);let V;try{V=await q(B)}catch{throw new W("INVALID_INPUT","文件读取失败:"+B)}try{K=new TextDecoder("utf-8",{fatal:!0}).decode(V)}catch{throw new W("INVALID_INPUT","文件编码不是 UTF-8:"+B)}}if(!K.trim())throw new W("INVALID_INPUT","文章正文不能为空");return K}O({site:"oschina",name:"article",access:"write",description:"发布文章到开源中国博客。默认正式发布,加 --draft 仅存草稿。正文默认 Markdown,外链图片自动转存到开源中国图床。",domain:"my.oschina.net",strategy:_.COOKIE,browser:!0,args:[{name:"title",positional:!0,required:!0,help:"文章标题"},{name:"text",positional:!0,help:"文章正文(默认 Markdown;--html 表示原始 HTML)"},{name:"file",help:"从文件读取正文(UTF-8,默认 Markdown)"},{name:"html",type:"boolean",help:"将正文视为原始 HTML 而非 Markdown"},{name:"category",help:"个人博客分类名(精确匹配),合法值用 `oschina catalogs` 列举;可空=不分类"},{name:"draft",type:"boolean",help:"仅存草稿,不发布"},{name:"execute",type:"boolean",help:"真正执行写操作;不加此参数命令拒绝写入"}],columns:["status","outcome","message","target_type","target","created_target","created_url"],func:async(G,z)=>{if(!G)throw new F("开源中国文章发布需要浏览器会话");Z(z);const B=String(z.title??"").trim();if(!B)throw new W("INVALID_INPUT","文章标题不能为空");const K=await A(z),M=Boolean(z.draft),V={category:typeof z.category==="string"?z.category.trim():""},Q=await j(G,{title:B,body:K,format:z.html?"html":"markdown",draftOnly:M,profile:oschinaProfile,publishParams:V}),U=Q.images.uploaded.length|0,X=Q.images.failed.length|0;let H=Q.draft?"已保存到开源中国草稿箱":"已正式发布到开源中国博客";if(U||X)H+=`·图片:${U} 张已转存${X?`,${X} 张失败`:""}`;if(!Q.draft)H+="。已按公开发布提交;开源中国对新账号/新文章有站方审核,过审前它不会出现在公开列表,"+"此时 stats 的 is_private 会显示「是」——这是审核中,不是发布失败,请勿重发,稍后回查即可";return $(H,"article","",Q.draft?"draft":"created",{created_target:(Q.draft?"draft:":"article:")+Q.id,created_url:Q.url})}});export const __test__={oschinaProfile,requireExecute:Z,buildResultRow:$,resolvePayload:A};
|
package/clis/oschina/stats.js
CHANGED
|
@@ -51,4 +51,4 @@ import{cli as Y,Strategy as $}from"@jackwener/opencli/registry";import{CliError
|
|
|
51
51
|
}
|
|
52
52
|
return out;
|
|
53
53
|
}
|
|
54
|
-
`}function
|
|
54
|
+
`}function j(h){if(h&&typeof h==="object"&&!Array.isArray(h)&&"session"in h&&"data"in h)return h.data;return h}function U(h){return typeof h==="number"&&Number.isFinite(h)?h:""}function W(h,H,Q,T,q){const F=h.id!=null?String(h.id):"",N=T&&F?T.get(F):null,B=(z)=>h[z]!=null?h[z]:N?N[z]:void 0,K=Number(B("type")),O={};for(const[z,D]of Object.entries({reply_count:U(B("replyCount")),state:B("state"),as_recomm:h.asRecomm,detritus:B("detritus"),scheduled_time:h.scheduledTime,scheduled_state:h.scheduledState,summary:typeof B("detail")==="string"?String(B("detail")).slice(0,80):void 0}))if(D!==void 0&&D!==null&&D!=="")O[z]=D;if(Q==="已发布"&&!(q||N))O.visibility_note="未出现在公开列表:可能是站方审核中(新账号/新文章常见),也可能是该文设为私密;请稍后回查再判定";return{id:F,type:"博客",title:h.title!=null?String(h.title):"",url:F&&H?`https://my.oschina.net/u/${H}/blog/${F}`:"",published_at:B("createTime")||"",views:U(B("viewCount")),likes:U(B("voteCount")),comments:U(B("commentCount")),collects:"",shares:"",origin:K===2?"转载":K===1?"原创":"",is_private:Q!=="已发布"?"":q||N?"否":"是",recommended:Number(B("recomm"))===1?"推荐":"",top:Number(B("asTop"))===1?"置顶":"",status:Q,extra:Object.keys(O).length?JSON.stringify(O):""}}Y({site:"oschina",name:"stats",access:"read",description:"开源中国博客数据(每篇阅读/点赞/评论等全量运营指标)",domain:"my.oschina.net",strategy:$.COOKIE,browser:!0,args:[{name:"limit",type:"number",default:20,help:"返回条数"},{name:"user",help:"查看指定用户 ID 的公开博客数据(默认当前登录账号,含私密与定时博客)"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","origin","is_private","recommended","top","status","extra"],func:async(h,H)=>{const Q=Number(H.limit)||20,T=H.user!=null&&String(H.user).trim()!==""?String(H.user).trim():"";await h.goto("https://my.oschina.net/");await h.wait(1);const q=j(await h.evaluate(Z(Q,T)));if(!q||typeof q!=="object")throw Error("开源中国页面取数失败:未拿到任何响应,请确认 Chrome 已打开且能访问 my.oschina.net。");if(q.notLoggedIn)throw Error("未登录开源中国:user/myDetails 未返回用户信息。请先在 Chrome 中登录 my.oschina.net(可用 ppcli oschina login)。");const F=String(q.targetUser||q.me||""),N=q.pub&&q.pub.rows||[];if(q.pub&&q.pub.error)throw Error("获取开源中国博客公开列表失败:"+q.pub.error);const B=new Map(N.filter((z)=>z&&z.id!=null).map((z)=>[String(z.id),z])),K=[],O=new Set;if(q.isSelf){if(q.mine&&q.mine.error)throw Error("获取开源中国「我的创作」博客列表失败:"+q.mine.error);for(const z of q.mine&&q.mine.rows||[]){const D=W(z,F,"已发布",B);if(D.id)O.add(D.id);K.push(D)}for(const z of N){const D=z&&z.id!=null?String(z.id):"";if(!D||O.has(D))continue;O.add(D);K.push(W(z,F,"已发布",null,!0))}for(const z of q.scheduled&&q.scheduled.rows||[])K.push(W(z,F,"已定时未发布",null,!1))}else for(const z of N)K.push(W(z,F,"已发布",null,!0));if(K.length===0)throw new I("EMPTY_RESULT",q.isSelf?"当前开源中国账号没有任何博客(已发布/定时均为空)。":`用户 ${F} 没有任何公开博客。`,q.isSelf?"发布过内容后再来查看数据(ppcli oschina article)。":"请确认用户 ID 是否正确。");return K.slice(0,Q)}});
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import{spawn as
|
|
2
|
-
`);let
|
|
3
|
-
Run manually: ppcli daemon stop`,"daemon-not-running");q=!0}if(!q&&X.state==="ready")return{health:X,spawnedProcess:k};if(!q&&X.state==="profile-required")throw
|
|
1
|
+
import{spawn as V}from"node:child_process";import{fileURLToPath as b}from"node:url";import*as x from"node:fs";import*as K from"node:path";import{DEFAULT_DAEMON_PORT}from"../constants.js";import{BrowserConnectError as J}from"../errors.js";import{PKG_VERSION as j}from"../version.js";import{waitForBridgeReady as F}from"./bridge-readiness.js";import{fetchDaemonStatus as B,getDaemonHealth as y,requestDaemonShutdown as C}from"./daemon-transport.js";export function resolveDaemonLaunchSpec(){const z=K.dirname(b(import.meta.url)),Q=K.resolve(z,".."),W=K.join(Q,"daemon.ts"),Z=K.join(Q,"daemon.js"),Y=x.existsSync(W),X=Y?W:Z,$=!!process.versions?.bun,U=Y&&!$;return{binary:process.execPath,args:U?["--import","tsx/esm",X]:[X],scriptPath:X}}export function spawnDaemonProcess(){const z=resolveDaemonLaunchSpec(),Q=V(z.binary,z.args,{detached:!0,stdio:"ignore",env:{...process.env}});Q.unref();return Q}export async function waitForDaemonStop(z){const Q=Date.now()+z;while(Date.now()<Q){await O(200);if((await y()).state==="stopped")return!0}return!1}export async function waitForDaemonStatus(z){const Q=Date.now()+z;while(Date.now()<Q){const W=await B({timeout:Math.min(1000,Math.max(100,Q-Date.now()))});if(W)return W;await O(200)}return null}export const daemonLifecycleHooks={requestDaemonShutdown:C,spawnDaemonProcess,waitForDaemonStop};export async function restartDaemon(z={}){const Q=await B();let W=Q===null;if(Q){W=await C()&&await waitForDaemonStop(z.stopTimeoutMs??3000);if(!W)return{previousStatus:Q,status:Q,stopped:!1,spawned:!1}}spawnDaemonProcess();const Z=await waitForDaemonStatus(z.startTimeoutMs??5000);return{previousStatus:Q,status:Z,stopped:W,spawned:!0}}export async function ensureBrowserBridgeReady(z={}){const W=(z.timeoutSeconds&&z.timeoutSeconds>0?z.timeoutSeconds:10)*1000,Z=z.verbose??!0,Y=z.contextId,X=await y({contextId:Y}),$=X.status?.daemonVersion,U=!!X.status&&(!$||$!==j);let q=!1,k=null;if(U){const N=$?`v${$} ≠ v${j}`:`pre-version daemon, CLI is v${j}`;if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⚠️ Stale daemon detected (${N}). Restarting...
|
|
2
|
+
`);let g=await daemonLifecycleHooks.requestDaemonShutdown()&&await daemonLifecycleHooks.waitForDaemonStop(3000);if(!g){const G=X.status?.pid;if(typeof G==="number"&&Number.isInteger(G)&&G>0){try{process.kill(G,"SIGKILL")}catch{}g=await daemonLifecycleHooks.waitForDaemonStop(2000)}}if(!g)throw new J("Stale daemon could not be replaced",`A stale daemon (${N}) is running but did not shut down (graceful + SIGKILL both failed).
|
|
3
|
+
Run manually: ppcli daemon stop`,"daemon-not-running");q=!0}if(!q&&X.state==="ready")return{health:X,spawnedProcess:k};if(!q&&X.state==="profile-required")throw M(X,Y);if(q||X.state==="stopped"){if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY))process.stderr.write(`⏳ Starting daemon...
|
|
4
4
|
`);k=daemonLifecycleHooks.spawnDaemonProcess()}else if(Z&&(process.env.OPENCLI_VERBOSE||process.stderr.isTTY)){process.stderr.write(`⏳ Waiting for Chrome/Chromium extension to connect...
|
|
5
5
|
`);process.stderr.write(` Make sure Chrome or Chromium is open and the ppcli extension is enabled.
|
|
6
|
-
`)}const A=await
|
|
7
|
-
Run ppcli profile list to see connected profiles.`,"profile-required");if(z.state==="profile-disconnected"){const W=Q??z.status.contextId??"unknown";return new J(
|
|
8
|
-
If not installed:
|
|
9
|
-
1. Download: https://github.com/jackwener/OpenCLI/releases
|
|
10
|
-
`+" 2. Open chrome://extensions → Developer Mode → Load unpacked","extension-not-connected");return new J("Failed to start ppcli daemon",`Try running manually:
|
|
6
|
+
`)}const A=await F(y,{timeoutMs:W,contextId:Y});if(A.state==="ready")return{health:A,spawnedProcess:k};throw M(A,Y)}function M(z,Q){if(z.state==="profile-required")return new J("Multiple Browser Bridge profiles are connected",`Select one with --profile <name>, OPENCLI_PROFILE=<name>, or ppcli profile use <name>.
|
|
7
|
+
Run ppcli profile list to see connected profiles.`,"profile-required");if(z.state==="profile-disconnected"){const W=Q??z.status.contextId??"unknown";return new J(`浏览器环境 "${W}" 未连接`,`这台机器上名为 "${W}" 的浏览器环境当前没连上。请用户打开 PublishPort 客户端,`+"在「高级设置 → 环境」里确认该环境已启动;或改用其它环境(--profile <名字>),"+"不带 --profile 则用默认环境。这是设备侧环境问题,重试同一条命令不会好转,请先告知用户。","profile-disconnected")}if(z.state==="no-extension")return new J("浏览器桥未连接","这台机器上的 PublishPort 托管浏览器当前没在运行(或浏览器插件被禁用了)。"+"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」,客户端会把托管浏览器拉起来;"+"若仍不行,重启一次 PublishPort 客户端。这是设备侧环境问题,重试同一条命令不会好转,请先告知用户。","extension-not-connected");return new J("Failed to start ppcli daemon",`Try running manually:
|
|
11
8
|
node ${resolveDaemonLaunchSpec().scriptPath}
|
|
12
|
-
Make sure port ${DEFAULT_DAEMON_PORT} is available.`,"daemon-not-running")}function
|
|
9
|
+
Make sure port ${DEFAULT_DAEMON_PORT} is available.`,"daemon-not-running")}function O(z){return new Promise((Q)=>setTimeout(Q,z))}export{DEFAULT_DAEMON_PORT};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{BrowserConnectError as
|
|
1
|
+
import{BrowserConnectError as x}from"../errors.js";import{DEFAULT_DAEMON_PORT as K}from"../constants.js";const Q=["Extension disconnected","Extension not connected","attach failed","Detached while handling command","Debugger is not attached to the tab","no longer exists","No tab with id","CDP connection","Daemon command failed","No window with id"],W=["Inspected target navigated or closed"];function Y(h){return h instanceof Error?h.message:String(h)}const J={attach_failed:{kind:"extension-transient",retryable:!0,delayMs:1500},tab_gone:{kind:"extension-transient",retryable:!0,delayMs:1500},target_navigated:{kind:"target-navigation",retryable:!0,delayMs:200},detached_mid_command:{kind:"non-retryable",retryable:!1,delayMs:0},cdp_timeout:{kind:"non-retryable",retryable:!1,delayMs:0}};export function classifyBrowserError(h){const q=h&&typeof h==="object"?h.code:void 0;if(typeof q==="string"&&J[q])return J[q];const v=Y(h);if(Q.some((z)=>v.includes(z)))return{kind:"extension-transient",retryable:!0,delayMs:1500};if(W.some((z)=>v.includes(z)))return{kind:"target-navigation",retryable:!0,delayMs:200};if(v.includes("-32000")&&/target|context/i.test(v))return{kind:"target-navigation",retryable:!0,delayMs:200};return{kind:"non-retryable",retryable:!1,delayMs:0}}export function isTransientBrowserError(h){return classifyBrowserError(h).retryable}export function formatBrowserConnectError(h,q){switch(h){case"daemon-not-running":return new x("Cannot connect to ppcli daemon."+(q?`
|
|
2
2
|
|
|
3
|
-
${
|
|
3
|
+
${q}`:""),`Run \`ppcli doctor\` to diagnose, or \`ppcli daemon restart\` to force a fresh daemon. Default port is ${K}.`,h);case"extension-not-connected":return new x("浏览器桥未连接。"+(q?`
|
|
4
4
|
|
|
5
|
-
${
|
|
5
|
+
${q}`:""),"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。",h);case"command-failed":return new x(`Browser command failed: ${q??"unknown error"}`,void 0,h);default:return new x(q??"Failed to connect to browser",void 0,h)}}
|
package/dist/src/daemon.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createServer as p}from"node:http";import{WebSocketServer as a,WebSocket as P}from"ws";import{DEFAULT_DAEMON_PORT as t,PUBLISHPORT_DAEMON_PORT as n,unsupportedDaemonPortEnvMessage as s}from"./constants.js";import{EXIT_CODES as E}from"./errors.js";import{log as Y}from"./logger.js";import{PKG_VERSION as o}from"./version.js";import{DEFAULT_CONTEXT_ID as r}from"./browser/profile.js";import{buildCommandDispatchFailure as g,buildCommandTimeoutFailure as e,buildExtensionDisconnectFailure as JJ,getResponseCorsHeaders as QJ}from"./daemon-utils.js";import*as F from"./browser/trace.js";import{SessionLeaseRegistry as $J,buildSessionBusyFailure as VJ,getSessionLeaseKey as ZJ,isSessionLeaseCommand as zJ}from"./session-lease.js";const T=t,I=n;if(process.env.OPENCLI_DAEMON_PORT){Y.error(s(process.env.OPENCLI_DAEMON_PORT));process.exit(E.USAGE_ERROR)}const U=new Map,A=new Map,C=new $J;function m(Q){for(const $ of A.values())if($.runId===Q)return!0;return!1}function j(Q,$,Z){clearTimeout($.timer);A.delete(Q);if($.leaseKey&&$.runId)C.heartbeat($.leaseKey,$.runId,Date.now());for(const z of $.settlers)if(Z.error)z.reject(Z.error);else z.resolve(Z.data)}let q=0;const GJ=200,D=[];class S extends Error{errorCode;errorHint;status;constructor(Q,$,Z,z=400){super(Q);this.errorCode=$;this.errorHint=Z;this.status=z;this.name="DaemonCommandFailure"}}function YJ(Q){D.push(Q);if(D.length>GJ)D.shift()}function x(){return[...U.values()].filter((Q)=>Q.ws.readyState===P.OPEN)}function c(Q){const $=typeof Q==="string"&&Q.trim()?Q.trim():void 0;if($){const z=U.get($);if(z?.ws.readyState===P.OPEN)return{connection:z};const V=x();if(V.length===1){Y.warn(`[daemon] Requested profile "${$}" not connected; falling back to the only connected profile "${V[0].contextId}"`);return{connection:V[0]}}return{errorCode:"profile_disconnected",error:`Browser profile "${$}" is not connected.`,errorHint:"Open that Chrome profile and make sure the ppcli extension is enabled, or choose another profile with ppcli profile use <name>."}}const Z=x();if(Z.length===1)return{connection:Z[0]};if(Z.length>1)return{errorCode:"profile_required",error:"Multiple Browser Bridge profiles are connected; choose one with --profile.",errorHint:"Run ppcli profile list, then use ppcli --profile <name> ... or ppcli profile use <name>."};return{errorCode:"extension_not_connected",error:"Extension not connected. Please install the ppcli Browser Bridge extension."}}function NJ(Q,$){const Z=typeof $==="string"&&$.trim()?$.trim():r,z=U.get(Z);if(z&&z.ws!==Q)z.ws.close();const V=[...U.entries()].find(([,H])=>H.ws===Q);if(V&&V[0]!==Z)U.delete(V[0]);const J=U.get(Z),G={contextId:Z,ws:Q,extensionVersion:J?.ws===Q?J.extensionVersion:null,extensionCompatRange:J?.ws===Q?J.extensionCompatRange:null,lastSeenAt:Date.now()};U.set(Z,G);return G}function u(Q){for(const[$,Z]of U.entries()){if(Z.ws!==Q)continue;U.delete($);F.traceExtDisconnect($);for(const[z,V]of A){if(V.contextId!==$)continue;const J=JJ({contextId:$,action:V.action,dispatched:V.dispatched});if(J.countAsCommandResultUnknown){q++;Y.warn(`[daemon] Command result unknown after extension disconnect (id=${z}, action=${V.action}, context=${$})`)}F.traceResult(z,!1,Date.now()-V.dispatchTs,J.errorCode);j(z,V,{error:new S(J.message,J.errorCode,J.errorHint,J.status)})}}}const l=33554432;class f extends Error{status=413;constructor(){super(`请求体超过 ${l/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function KJ(Q){return new Promise(($,Z)=>{const z=[];let V=0,J=!1;Q.on("data",(G)=>{if(J)return;V+=G.length;if(V>l){J=!0;z.length=0;Z(new f);return}z.push(G)});Q.on("end",()=>{if(!J)$(Buffer.concat(z).toString("utf-8"))});Q.on("error",(G)=>{if(!J)Z(G)})})}function N(Q,$,Z,z){Q.writeHead($,{"Content-Type":"application/json",...z});Q.end(JSON.stringify(Z))}async function d(Q,$){const Z=Q.headers.origin;if(Z&&!Z.startsWith("chrome-extension://")){N($,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(Q.method==="OPTIONS"){$.writeHead(204);$.end();return}const z=Q.url??"/",V=z.split("?")[0];if(Q.method==="GET"&&V==="/ping"){N($,200,{ok:!0},QJ(V,Z));return}if(!Q.headers["x-opencli"]){N($,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(Q.method==="GET"&&V==="/status"){const J=process.uptime(),G=process.memoryUsage(),L=new URL(z,`http://localhost:${T}`).searchParams.get("contextId")?.trim()||void 0,W=c(L),R=x().map((O)=>({contextId:O.contextId,extensionConnected:!0,extensionVersion:O.extensionVersion??void 0,extensionCompatRange:O.extensionCompatRange??void 0,pending:[...A.values()].filter((B)=>B.contextId===O.contextId).length,lastSeenAt:O.lastSeenAt}));N($,200,{ok:!0,pid:process.pid,uptime:J,daemonVersion:o,extensionConnected:!!W.connection,extensionVersion:W.connection?.extensionVersion??void 0,extensionCompatRange:W.connection?.extensionCompatRange??void 0,contextId:W.connection?.contextId??L,profileRequired:W.errorCode==="profile_required",profileDisconnected:W.errorCode==="profile_disconnected",profiles:R,pending:A.size,sessionLeases:C.list(Date.now(),m),commandResultUnknown:q,memoryMB:Math.round(G.rss/1024/1024*10)/10,port:T});return}if(Q.method==="GET"&&V==="/logs"){const G=new URL(z,`http://localhost:${T}`).searchParams.get("level"),H=G?D.filter((L)=>L.level===G):D;N($,200,{ok:!0,logs:H});return}if(Q.method==="DELETE"&&V==="/logs"){D.length=0;N($,200,{ok:!0});return}if(Q.method==="POST"&&V==="/shutdown"){N($,200,{ok:!0,message:"Shutting down"});setTimeout(()=>b(),100);return}if(Q.method==="POST"&&z==="/command"){try{const J=JSON.parse(await KJ(Q));if(!J.id){N($,400,{ok:!1,error:"Missing command id"});return}if(J.action==="lease-release"){if(typeof J.runId==="string")C.releaseByRunId(J.runId);N($,200,{id:J.id,ok:!0});return}const G=c(typeof J.contextId==="string"?J.contextId:void 0);if(!G.connection){N($,G.errorCode==="profile_required"?409:503,{id:J.id,ok:!1,errorCode:G.errorCode,error:G.error,...G.errorHint?{errorHint:G.errorHint}:{}});return}let H,L;if(zJ(J)){const B=Date.now(),k=ZJ(G.connection.contextId,J.surface,J.session),_=C.touch(k,{runId:J.runId,command:typeof J.command==="string"&&J.command?J.command:J.action,now:B,hasPendingWork:m});if(!_.granted){const X=VJ(J.session,_.holder,B);Y.warn(`[daemon] Session ${k} busy — rejected ${J.command??J.action} (runId=${J.runId}); held by ${_.holder.command} (runId=${_.holder.runId})`);N($,X.status,{id:J.id,ok:!1,errorCode:X.errorCode,error:X.message,errorHint:X.errorHint});return}H=k;L=J.runId}const W=typeof J.deadlineAt==="number"&&J.deadlineAt>0?Math.max(1000,J.deadlineAt-Date.now()):typeof J.timeout==="number"&&J.timeout>0?J.timeout*1000:120000,R=A.get(J.id);if(R){const B=await new Promise((k,_)=>{R.settlers.push({resolve:k,reject:_})});N($,200,B);return}const O=await new Promise((B,k)=>{const _=setTimeout(()=>{const K=A.get(J.id);if(!K)return;F.traceTimeout(J.id,W,typeof J.action==="string"?J.action:void 0);const M=e(K.action,W);if(M.countAsCommandResultUnknown&&K.dispatched){q++;Y.warn(`[daemon] Command timed out after dispatch (id=${J.id}, action=${K.action}, timeout=${W}ms)`)}j(J.id,K,{error:new S(M.message,M.errorCode,M.errorHint,M.status)})},W),X={contextId:G.connection.contextId,action:typeof J.action==="string"?J.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:B,reject:k}],timer:_,...H&&L?{leaseKey:H,runId:L}:{}};A.set(J.id,X);const y=(K)=>{if(A.get(J.id)!==X)return;const M=g(X.contextId);j(J.id,X,{error:new S(M.message,M.errorCode,M.errorHint,M.status)});Y.warn(`[daemon] Failed to dispatch command ${J.id}: ${K instanceof Error?K.message:String(K)}`)};try{G.connection.ws.send(JSON.stringify(J),(K)=>{if(K&&!X.dispatched)y(K)});X.dispatched=!0;F.traceDispatch(J)}catch(K){y(K)}});N($,200,O)}catch(J){const G=J instanceof S?J:null;if(J instanceof f){N($,413,{ok:!1,error:J.message,errorCode:"body_too_large"});return}N($,G?.status??(J instanceof Error&&J.message.includes("timeout")?408:400),{ok:!1,error:J instanceof Error?J.message:"Invalid request",...G?.errorCode?{errorCode:G.errorCode}:{},...G?.errorHint?{errorHint:G.errorHint}:{}})}return}N($,404,{error:"Not found"})}const v=p((Q,$)=>{d(Q,$).catch(()=>{$.writeHead(500);$.end()})}),h=p((Q,$)=>{d(Q,$).catch(()=>{$.writeHead(500);$.end()})}),w=new a({noServer:!0});function i(Q,$,Z){if((Q.url??"/").split("?")[0]!=="/ext"){$.destroy();return}const V=Q.headers.origin;if(V&&!V.startsWith("chrome-extension://")){$.destroy();return}w.handleUpgrade(Q,$,Z,(J)=>{w.emit("connection",J,Q)})}v.on("upgrade",i);h.on("upgrade",i);w.on("connection",(Q)=>{Y.info("[daemon] Extension connected");let $=0;const Z=setInterval(()=>{if(Q.readyState!==P.OPEN){clearInterval(Z);return}if($>=2){Y.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(Z);Q.terminate();return}$++;Q.ping()},15000);Q.on("pong",()=>{$=0});Q.on("message",(z)=>{try{const V=JSON.parse(z.toString());if(V.type==="hello"){if(V.brand!=="publishport"){Y.warn(`[daemon] 拒绝非 PublishPort 官方扩展连接(brand=${JSON.stringify(V.brand)})`);Q.close(4003,"Only the official PublishPort extension is supported");return}const G=NJ(Q,V.contextId);G.extensionVersion=typeof V.version==="string"?V.version:null;G.extensionCompatRange=typeof V.compatRange==="string"?V.compatRange:null;G.lastSeenAt=Date.now();Y.info(`[daemon] Extension profile connected: ${G.contextId}`);F.traceExtConnect(G.contextId,G.extensionVersion);return}if(V.type==="log"){if(V.level==="error")Y.error(`[ext] ${V.msg}`);else if(V.level==="warn")Y.warn(`[ext] ${V.msg}`);else Y.info(`[ext] ${V.msg}`);YJ({level:V.level,msg:V.msg,ts:V.ts??Date.now()});F.traceExtLog(V.level,V.msg);return}if(V.type==="ping")return;const J=A.get(V.id);if(J){F.traceResult(V.id,!!V.ok,Date.now()-J.dispatchTs,typeof V.errorCode==="string"?V.errorCode:void 0,typeof V.page==="string"?V.page:void 0);j(V.id,J,{data:V})}}catch(V){const J=z.toString().slice(0,200);Y.warn(`[daemon] Ignoring malformed WS message from extension: ${V instanceof Error?V.message:String(V)} (first 200 chars: ${JSON.stringify(J)})`)}});Q.on("close",()=>{Y.info("[daemon] Extension disconnected");clearInterval(Z);u(Q)});Q.on("error",()=>{clearInterval(Z);u(Q)})});v.listen(T,"127.0.0.1",()=>{Y.info(`[daemon] Listening on http://127.0.0.1:${T}`)});v.on("error",(Q)=>{if(Q.code==="EADDRINUSE"){Y.error(`[daemon] Port ${T} already in use — another daemon is likely running. Exiting.`);process.exit(E.SERVICE_UNAVAIL)}Y.error(`[daemon] Server error: ${Q.message}`);process.exit(E.GENERIC_ERROR)});h.listen(I,"127.0.0.1",()=>{Y.info(`[daemon] Listening on http://127.0.0.1:${I} (PublishPort extension)`)});h.on("error",(Q)=>{Y.warn(`[daemon] PublishPort port ${I} unavailable (${Q.message}) — extension will fall back to ${T}`)});function b(){for(const[Q,$]of A){const Z=$.dispatched?new S("Daemon shutting down before the command completed.","daemon_shutting_down","The daemon is being replaced; a journaling extension replays the command result on retry.",503):(()=>{const z=g($.contextId);return new S(z.message,z.errorCode,z.errorHint,z.status)})();j(Q,$,{error:Z})}A.clear();for(const Q of U.values())Q.ws.close();h.close();v.close(()=>process.exit(E.SUCCESS));setTimeout(()=>{v.closeIdleConnections?.();h.closeIdleConnections?.();setTimeout(()=>process.exit(E.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",b);process.on("SIGINT",b);
|
|
1
|
+
import{createServer as p}from"node:http";import{WebSocketServer as a,WebSocket as P}from"ws";import{DEFAULT_DAEMON_PORT as t,PUBLISHPORT_DAEMON_PORT as n,unsupportedDaemonPortEnvMessage as s}from"./constants.js";import{EXIT_CODES as E}from"./errors.js";import{log as Y}from"./logger.js";import{PKG_VERSION as o}from"./version.js";import{DEFAULT_CONTEXT_ID as r}from"./browser/profile.js";import{buildCommandDispatchFailure as g,buildCommandTimeoutFailure as e,buildExtensionDisconnectFailure as JJ,getResponseCorsHeaders as QJ}from"./daemon-utils.js";import*as F from"./browser/trace.js";import{SessionLeaseRegistry as $J,buildSessionBusyFailure as VJ,getSessionLeaseKey as ZJ,isSessionLeaseCommand as zJ}from"./session-lease.js";const T=t,I=n;if(process.env.OPENCLI_DAEMON_PORT){Y.error(s(process.env.OPENCLI_DAEMON_PORT));process.exit(E.USAGE_ERROR)}const U=new Map,A=new Map,C=new $J;function m(Q){for(const $ of A.values())if($.runId===Q)return!0;return!1}function j(Q,$,Z){clearTimeout($.timer);A.delete(Q);if($.leaseKey&&$.runId)C.heartbeat($.leaseKey,$.runId,Date.now());for(const z of $.settlers)if(Z.error)z.reject(Z.error);else z.resolve(Z.data)}let q=0;const GJ=200,D=[];class S extends Error{errorCode;errorHint;status;constructor(Q,$,Z,z=400){super(Q);this.errorCode=$;this.errorHint=Z;this.status=z;this.name="DaemonCommandFailure"}}function YJ(Q){D.push(Q);if(D.length>GJ)D.shift()}function x(){return[...U.values()].filter((Q)=>Q.ws.readyState===P.OPEN)}function c(Q){const $=typeof Q==="string"&&Q.trim()?Q.trim():void 0;if($){const z=U.get($);if(z?.ws.readyState===P.OPEN)return{connection:z};const V=x();if(V.length===1){Y.warn(`[daemon] Requested profile "${$}" not connected; falling back to the only connected profile "${V[0].contextId}"`);return{connection:V[0]}}return{errorCode:"profile_disconnected",error:`浏览器环境 "${$}" 未连接。`,errorHint:"请用户在 PublishPort 客户端的「高级设置 → 环境」里确认该环境已启动,或改用其它环境(--profile <名字>)。"}}const Z=x();if(Z.length===1)return{connection:Z[0]};if(Z.length>1)return{errorCode:"profile_required",error:"Multiple Browser Bridge profiles are connected; choose one with --profile.",errorHint:"Run ppcli profile list, then use ppcli --profile <name> ... or ppcli profile use <name>."};return{errorCode:"extension_not_connected",error:"浏览器桥未连接:这台机器上的 PublishPort 托管浏览器没在运行。",errorHint:"请用户打开 PublishPort 客户端,在平台列表里点一次「检测登录」把托管浏览器拉起来;仍不行就重启客户端。"}}function NJ(Q,$){const Z=typeof $==="string"&&$.trim()?$.trim():r,z=U.get(Z);if(z&&z.ws!==Q)z.ws.close();const V=[...U.entries()].find(([,H])=>H.ws===Q);if(V&&V[0]!==Z)U.delete(V[0]);const J=U.get(Z),G={contextId:Z,ws:Q,extensionVersion:J?.ws===Q?J.extensionVersion:null,extensionCompatRange:J?.ws===Q?J.extensionCompatRange:null,lastSeenAt:Date.now()};U.set(Z,G);return G}function u(Q){for(const[$,Z]of U.entries()){if(Z.ws!==Q)continue;U.delete($);F.traceExtDisconnect($);for(const[z,V]of A){if(V.contextId!==$)continue;const J=JJ({contextId:$,action:V.action,dispatched:V.dispatched});if(J.countAsCommandResultUnknown){q++;Y.warn(`[daemon] Command result unknown after extension disconnect (id=${z}, action=${V.action}, context=${$})`)}F.traceResult(z,!1,Date.now()-V.dispatchTs,J.errorCode);j(z,V,{error:new S(J.message,J.errorCode,J.errorHint,J.status)})}}}const l=33554432;class f extends Error{status=413;constructor(){super(`请求体超过 ${l/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function KJ(Q){return new Promise(($,Z)=>{const z=[];let V=0,J=!1;Q.on("data",(G)=>{if(J)return;V+=G.length;if(V>l){J=!0;z.length=0;Z(new f);return}z.push(G)});Q.on("end",()=>{if(!J)$(Buffer.concat(z).toString("utf-8"))});Q.on("error",(G)=>{if(!J)Z(G)})})}function N(Q,$,Z,z){Q.writeHead($,{"Content-Type":"application/json",...z});Q.end(JSON.stringify(Z))}async function d(Q,$){const Z=Q.headers.origin;if(Z&&!Z.startsWith("chrome-extension://")){N($,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(Q.method==="OPTIONS"){$.writeHead(204);$.end();return}const z=Q.url??"/",V=z.split("?")[0];if(Q.method==="GET"&&V==="/ping"){N($,200,{ok:!0},QJ(V,Z));return}if(!Q.headers["x-opencli"]){N($,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(Q.method==="GET"&&V==="/status"){const J=process.uptime(),G=process.memoryUsage(),L=new URL(z,`http://localhost:${T}`).searchParams.get("contextId")?.trim()||void 0,W=c(L),R=x().map((O)=>({contextId:O.contextId,extensionConnected:!0,extensionVersion:O.extensionVersion??void 0,extensionCompatRange:O.extensionCompatRange??void 0,pending:[...A.values()].filter((B)=>B.contextId===O.contextId).length,lastSeenAt:O.lastSeenAt}));N($,200,{ok:!0,pid:process.pid,uptime:J,daemonVersion:o,extensionConnected:!!W.connection,extensionVersion:W.connection?.extensionVersion??void 0,extensionCompatRange:W.connection?.extensionCompatRange??void 0,contextId:W.connection?.contextId??L,profileRequired:W.errorCode==="profile_required",profileDisconnected:W.errorCode==="profile_disconnected",profiles:R,pending:A.size,sessionLeases:C.list(Date.now(),m),commandResultUnknown:q,memoryMB:Math.round(G.rss/1024/1024*10)/10,port:T});return}if(Q.method==="GET"&&V==="/logs"){const G=new URL(z,`http://localhost:${T}`).searchParams.get("level"),H=G?D.filter((L)=>L.level===G):D;N($,200,{ok:!0,logs:H});return}if(Q.method==="DELETE"&&V==="/logs"){D.length=0;N($,200,{ok:!0});return}if(Q.method==="POST"&&V==="/shutdown"){N($,200,{ok:!0,message:"Shutting down"});setTimeout(()=>b(),100);return}if(Q.method==="POST"&&z==="/command"){try{const J=JSON.parse(await KJ(Q));if(!J.id){N($,400,{ok:!1,error:"Missing command id"});return}if(J.action==="lease-release"){if(typeof J.runId==="string")C.releaseByRunId(J.runId);N($,200,{id:J.id,ok:!0});return}const G=c(typeof J.contextId==="string"?J.contextId:void 0);if(!G.connection){N($,G.errorCode==="profile_required"?409:503,{id:J.id,ok:!1,errorCode:G.errorCode,error:G.error,...G.errorHint?{errorHint:G.errorHint}:{}});return}let H,L;if(zJ(J)){const B=Date.now(),k=ZJ(G.connection.contextId,J.surface,J.session),_=C.touch(k,{runId:J.runId,command:typeof J.command==="string"&&J.command?J.command:J.action,now:B,hasPendingWork:m});if(!_.granted){const X=VJ(J.session,_.holder,B);Y.warn(`[daemon] Session ${k} busy — rejected ${J.command??J.action} (runId=${J.runId}); held by ${_.holder.command} (runId=${_.holder.runId})`);N($,X.status,{id:J.id,ok:!1,errorCode:X.errorCode,error:X.message,errorHint:X.errorHint});return}H=k;L=J.runId}const W=typeof J.deadlineAt==="number"&&J.deadlineAt>0?Math.max(1000,J.deadlineAt-Date.now()):typeof J.timeout==="number"&&J.timeout>0?J.timeout*1000:120000,R=A.get(J.id);if(R){const B=await new Promise((k,_)=>{R.settlers.push({resolve:k,reject:_})});N($,200,B);return}const O=await new Promise((B,k)=>{const _=setTimeout(()=>{const K=A.get(J.id);if(!K)return;F.traceTimeout(J.id,W,typeof J.action==="string"?J.action:void 0);const M=e(K.action,W);if(M.countAsCommandResultUnknown&&K.dispatched){q++;Y.warn(`[daemon] Command timed out after dispatch (id=${J.id}, action=${K.action}, timeout=${W}ms)`)}j(J.id,K,{error:new S(M.message,M.errorCode,M.errorHint,M.status)})},W),X={contextId:G.connection.contextId,action:typeof J.action==="string"?J.action:"unknown",dispatched:!1,dispatchTs:Date.now(),settlers:[{resolve:B,reject:k}],timer:_,...H&&L?{leaseKey:H,runId:L}:{}};A.set(J.id,X);const y=(K)=>{if(A.get(J.id)!==X)return;const M=g(X.contextId);j(J.id,X,{error:new S(M.message,M.errorCode,M.errorHint,M.status)});Y.warn(`[daemon] Failed to dispatch command ${J.id}: ${K instanceof Error?K.message:String(K)}`)};try{G.connection.ws.send(JSON.stringify(J),(K)=>{if(K&&!X.dispatched)y(K)});X.dispatched=!0;F.traceDispatch(J)}catch(K){y(K)}});N($,200,O)}catch(J){const G=J instanceof S?J:null;if(J instanceof f){N($,413,{ok:!1,error:J.message,errorCode:"body_too_large"});return}N($,G?.status??(J instanceof Error&&J.message.includes("timeout")?408:400),{ok:!1,error:J instanceof Error?J.message:"Invalid request",...G?.errorCode?{errorCode:G.errorCode}:{},...G?.errorHint?{errorHint:G.errorHint}:{}})}return}N($,404,{error:"Not found"})}const v=p((Q,$)=>{d(Q,$).catch(()=>{$.writeHead(500);$.end()})}),h=p((Q,$)=>{d(Q,$).catch(()=>{$.writeHead(500);$.end()})}),w=new a({noServer:!0});function i(Q,$,Z){if((Q.url??"/").split("?")[0]!=="/ext"){$.destroy();return}const V=Q.headers.origin;if(V&&!V.startsWith("chrome-extension://")){$.destroy();return}w.handleUpgrade(Q,$,Z,(J)=>{w.emit("connection",J,Q)})}v.on("upgrade",i);h.on("upgrade",i);w.on("connection",(Q)=>{Y.info("[daemon] Extension connected");let $=0;const Z=setInterval(()=>{if(Q.readyState!==P.OPEN){clearInterval(Z);return}if($>=2){Y.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(Z);Q.terminate();return}$++;Q.ping()},15000);Q.on("pong",()=>{$=0});Q.on("message",(z)=>{try{const V=JSON.parse(z.toString());if(V.type==="hello"){if(V.brand!=="publishport"){Y.warn(`[daemon] 拒绝非 PublishPort 官方扩展连接(brand=${JSON.stringify(V.brand)})`);Q.close(4003,"Only the official PublishPort extension is supported");return}const G=NJ(Q,V.contextId);G.extensionVersion=typeof V.version==="string"?V.version:null;G.extensionCompatRange=typeof V.compatRange==="string"?V.compatRange:null;G.lastSeenAt=Date.now();Y.info(`[daemon] Extension profile connected: ${G.contextId}`);F.traceExtConnect(G.contextId,G.extensionVersion);return}if(V.type==="log"){if(V.level==="error")Y.error(`[ext] ${V.msg}`);else if(V.level==="warn")Y.warn(`[ext] ${V.msg}`);else Y.info(`[ext] ${V.msg}`);YJ({level:V.level,msg:V.msg,ts:V.ts??Date.now()});F.traceExtLog(V.level,V.msg);return}if(V.type==="ping")return;const J=A.get(V.id);if(J){F.traceResult(V.id,!!V.ok,Date.now()-J.dispatchTs,typeof V.errorCode==="string"?V.errorCode:void 0,typeof V.page==="string"?V.page:void 0);j(V.id,J,{data:V})}}catch(V){const J=z.toString().slice(0,200);Y.warn(`[daemon] Ignoring malformed WS message from extension: ${V instanceof Error?V.message:String(V)} (first 200 chars: ${JSON.stringify(J)})`)}});Q.on("close",()=>{Y.info("[daemon] Extension disconnected");clearInterval(Z);u(Q)});Q.on("error",()=>{clearInterval(Z);u(Q)})});v.listen(T,"127.0.0.1",()=>{Y.info(`[daemon] Listening on http://127.0.0.1:${T}`)});v.on("error",(Q)=>{if(Q.code==="EADDRINUSE"){Y.error(`[daemon] Port ${T} already in use — another daemon is likely running. Exiting.`);process.exit(E.SERVICE_UNAVAIL)}Y.error(`[daemon] Server error: ${Q.message}`);process.exit(E.GENERIC_ERROR)});h.listen(I,"127.0.0.1",()=>{Y.info(`[daemon] Listening on http://127.0.0.1:${I} (PublishPort extension)`)});h.on("error",(Q)=>{Y.warn(`[daemon] PublishPort port ${I} unavailable (${Q.message}) — extension will fall back to ${T}`)});function b(){for(const[Q,$]of A){const Z=$.dispatched?new S("Daemon shutting down before the command completed.","daemon_shutting_down","The daemon is being replaced; a journaling extension replays the command result on retry.",503):(()=>{const z=g($.contextId);return new S(z.message,z.errorCode,z.errorHint,z.status)})();j(Q,$,{error:Z})}A.clear();for(const Q of U.values())Q.ws.close();h.close();v.close(()=>process.exit(E.SUCCESS));setTimeout(()=>{v.closeIdleConnections?.();h.closeIdleConnections?.();setTimeout(()=>process.exit(E.SUCCESS),500).unref()},100).unref()}process.on("SIGTERM",b);process.on("SIGINT",b);
|