publishport-opencli 1.0.5 → 1.0.6
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/dist/src/browser/cdp.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(L)=>{if(L?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.proxyAuth){this.on("Fetch.requestPaused",(L)=>{const Y=L;if(Y.requestId)this.send("Fetch.continueRequest",{requestId:Y.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(L)=>{const Y=L;if(!Y.requestId)return;this.send("Fetch.continueWithAuth",{requestId:Y.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(L){Z.close();X(L instanceof Error?L:Error(String(L)));return}K(new O(this))});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
1
|
+
import{WebSocket as I}from"ws";import{request as W}from"node:http";import{request as F}from"node:https";import{buildEvaluateExpression as _}from"./utils.js";import{generateStealthJs as z}from"./stealth.js";import{waitForDomStableJs as S}from"./dom-helpers.js";import{isRecord as H,saveBase64ToFile as M}from"../utils.js";import{getAllElectronApps as w}from"../electron-apps.js";import{BasePage as E}from"./base-page.js";const R=30000;export const CDP_RESPONSE_BODY_CAPTURE_LIMIT=8388608;export class CDPBridge{_ws=null;_idCounter=0;_pending=new Map;_eventListeners=new Map;async connect(G){if(this._ws)throw Error("CDPBridge is already connected. Call close() before reconnecting.");const V=G?.cdpEndpoint??process.env.OPENCLI_CDP_ENDPOINT;if(!V)throw Error("CDP endpoint not provided (pass cdpEndpoint or set OPENCLI_CDP_ENDPOINT)");let Q=V;if(V.startsWith("http")){const K=await j(`${V.replace(/\/$/,"")}/json`),X=U(K);if(!X||!X.webSocketDebuggerUrl)throw Error("No inspectable targets found at CDP endpoint");Q=X.webSocketDebuggerUrl}return new Promise((K,X)=>{const Z=new I(Q),$=(G?.timeout??10)*1000,J=setTimeout(()=>{this._ws=null;Z.close();X(Error("CDP connect timeout"))},$);Z.on("open",async()=>{clearTimeout(J);this._ws=Z;try{await this.send("Page.enable");this.on("Page.javascriptDialogOpening",(Y)=>{if(Y?.type==="beforeunload")this.send("Page.handleJavaScriptDialog",{accept:!0}).catch(()=>{})});await this.send("Page.addScriptToEvaluateOnNewDocument",{source:z()});if(G?.initScript)await this.send("Page.addScriptToEvaluateOnNewDocument",{source:G.initScript});if(G?.windowMode==="background")await this.send("Emulation.setFocusEmulationEnabled",{enabled:!0}).catch(()=>{});else if(G?.windowMode==="foreground")await this.send("Page.bringToFront").catch(()=>{});if(G?.proxyAuth){this.on("Fetch.requestPaused",(Y)=>{const A=Y;if(A.requestId)this.send("Fetch.continueRequest",{requestId:A.requestId}).catch(()=>{})});this.on("Fetch.authRequired",(Y)=>{const A=Y;if(!A.requestId)return;this.send("Fetch.continueWithAuth",{requestId:A.requestId,authChallengeResponse:{response:"ProvideCredentials",username:G.proxyAuth.username,password:G.proxyAuth.password}}).catch(()=>{})});await this.send("Fetch.enable",{handleAuthRequests:!0})}}catch(Y){Z.close();X(Y instanceof Error?Y:Error(String(Y)));return}const L=new O(this);if(G?.session)L.session=G.session;if(G?.contextId)L.contextId=G.contextId;K(L)});Z.on("error",(L)=>{clearTimeout(J);X(L)});Z.on("message",(L)=>{try{const Y=JSON.parse(L.toString());if(Y.id&&this._pending.has(Y.id)){const A=this._pending.get(Y.id);clearTimeout(A.timer);this._pending.delete(Y.id);if(Y.error)A.reject(Error(Y.error.message));else A.resolve(Y.result)}if(Y.method){const A=this._eventListeners.get(Y.method);if(A)for(const B of A)B(Y.params)}}catch(Y){if(process.env.OPENCLI_VERBOSE)console.error("[cdp] Failed to parse WebSocket message:",Y instanceof Error?Y.message:Y)}})})}async close(){if(this._ws){this._ws.close();this._ws=null}for(const G of this._pending.values()){clearTimeout(G.timer);G.reject(Error("CDP connection closed"))}this._pending.clear();this._eventListeners.clear()}async send(G,V={},Q=R){if(!this._ws||this._ws.readyState!==I.OPEN)throw Error("CDP connection is not open");const K=++this._idCounter;return new Promise((X,Z)=>{const $=setTimeout(()=>{this._pending.delete(K);const J=G.startsWith("Page.")?"(页面可能被 JS 弹窗挡住:试 `ppcli browser dialog accept`;托管 profile 可 `ppcli profile stop <name>` 关闭后重跑命令自动重启会话)":"";Z(Error(`CDP command '${G}' timed out after ${Q/1000}s${J}`))},Q);this._pending.set(K,{resolve:X,reject:Z,timer:$});this._ws.send(JSON.stringify({id:K,method:G,params:V}))})}on(G,V){let Q=this._eventListeners.get(G);if(!Q){Q=new Set;this._eventListeners.set(G,Q)}Q.add(V)}off(G,V){this._eventListeners.get(G)?.delete(V)}waitForEvent(G,V=15000){return new Promise((Q,K)=>{const X=setTimeout(()=>{this.off(G,Z);K(Error(`Timed out waiting for CDP event '${G}'`))},V),Z=($)=>{clearTimeout(X);this.off(G,Z);Q($)};this.on(G,Z)})}}class O extends E{bridge;session;contextId;_pageEnabled=!1;_networkCapturing=!1;_networkCapturePattern="";_networkEntries=[];_pendingRequests=new Map;_pendingBodyFetches=new Set;_consoleMessages=[];_consoleCapturing=!1;constructor(G){super();this.bridge=G}async goto(G,V){if(!this._pageEnabled){await this.bridge.send("Page.enable");this._pageEnabled=!0}const Q=this.bridge.waitForEvent("Page.loadEventFired",30000).catch(()=>{});await this.bridge.send("Page.navigate",{url:G});await Q;this._lastUrl=G;if(V?.waitUntil!=="none"){const K=V?.settleMs??1000;await this.evaluate(S(K,Math.min(500,K)))}}async evaluate(G,...V){const Q=_(G,V),K=await this.bridge.send("Runtime.evaluate",{expression:Q,returnByValue:!0,awaitPromise:!0});if(K.exceptionDetails)throw Error("Evaluate error: "+(K.exceptionDetails.exception?.description||"Unknown exception"));return K.result?.value}async getCookies(G={}){let V;try{const K=await this.bridge.send("Storage.getCookies");V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}catch{const K=await this.bridge.send("Network.getCookies",G.url?{urls:[G.url]}:{});V=H(K)&&Array.isArray(K.cookies)?K.cookies:[]}const Q=G.domain??(G.url?T(G.url):void 0);return Q?V.filter((K)=>N(K)&&C(K.domain,Q)):V.filter(N)}async screenshot(G={}){const V=G.fullPage===!0,Q=G.width&&G.width>0?Math.ceil(G.width):void 0,K=!V&&G.height&&G.height>0?Math.ceil(G.height):void 0,X=Q!==void 0||K!==void 0;if(X){if(Q!==void 0&&V)await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Q,height:0,deviceScaleFactor:1});let Z=Q??0,$=K??0;if(V){const J=await this.bridge.send("Page.getLayoutMetrics"),L=H(J)?J:{},Y=H(L.cssContentSize)?L.cssContentSize:void 0,A=H(L.contentSize)?L.contentSize:void 0,B=Y??A;if(B&&typeof B.width==="number"&&typeof B.height==="number"){if(Z===0)Z=Math.ceil(B.width);$=Math.ceil(B.height)}}await this.bridge.send("Emulation.setDeviceMetricsOverride",{mobile:!1,width:Z,height:$,deviceScaleFactor:1})}try{const Z=await this.bridge.send("Page.captureScreenshot",{format:G.format??"png",quality:G.format==="jpeg"?G.quality??80:void 0,captureBeyondViewport:!X&&V}),$=H(Z)&&typeof Z.data==="string"?Z.data:"";if(G.path)await M($,G.path);return $}finally{if(X)await this.bridge.send("Emulation.clearDeviceMetricsOverride").catch(()=>{})}}async startNetworkCapture(G=""){this._networkCapturePattern=G;if(!this._networkCapturing){this._networkEntries=[];this._pendingRequests.clear();this._pendingBodyFetches.clear();await this.bridge.send("Network.enable");this.bridge.on("Network.requestWillBeSent",(V)=>{const Q=V;if(!this._networkCapturePattern||Q.request.url.includes(this._networkCapturePattern)){const K=this._networkEntries.push({url:Q.request.url,method:Q.request.method,timestamp:Date.now()})-1;this._pendingRequests.set(Q.requestId,K)}});this.bridge.on("Network.responseReceived",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){this._networkEntries[K].responseStatus=Q.response.status;this._networkEntries[K].responseContentType=Q.response.mimeType||""}});this.bridge.on("Network.loadingFinished",(V)=>{const Q=V,K=this._pendingRequests.get(Q.requestId);if(K!==void 0){const X=this.bridge.send("Network.getResponseBody",{requestId:Q.requestId}).then((Z)=>{const $=Z;if(typeof $?.body==="string"){const J=$.body.length,L=J>CDP_RESPONSE_BODY_CAPTURE_LIMIT,Y=L?$.body.slice(0,CDP_RESPONSE_BODY_CAPTURE_LIMIT):$.body;this._networkEntries[K].responsePreview=$.base64Encoded?`base64:${Y}`:Y;this._networkEntries[K].responseBodyFullSize=J;this._networkEntries[K].responseBodyTruncated=L}}).catch((Z)=>{if(process.env.OPENCLI_VERBOSE)console.error(`[cdp] getResponseBody failed for ${Q.requestId}:`,Z instanceof Error?Z.message:Z)}).finally(()=>{this._pendingBodyFetches.delete(X)});this._pendingBodyFetches.add(X);this._pendingRequests.delete(Q.requestId)}});this._networkCapturing=!0}return!0}async readNetworkCapture(){if(this._pendingBodyFetches.size>0)await Promise.all([...this._pendingBodyFetches]);const G=[...this._networkEntries];this._networkEntries=[];return G}async consoleMessages(G="all"){if(!this._consoleCapturing){await this.bridge.send("Runtime.enable");this.bridge.on("Runtime.consoleAPICalled",(V)=>{const Q=V,K=(Q.args||[]).map((X)=>X.value!==void 0?String(X.value):X.description||"").join(" ");this._consoleMessages.push({type:Q.type,text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this.bridge.on("Runtime.exceptionThrown",(V)=>{const Q=V,K=Q.exceptionDetails?.exception?.description||Q.exceptionDetails?.text||"Unknown exception";this._consoleMessages.push({type:"error",text:K,timestamp:Date.now()});if(this._consoleMessages.length>500)this._consoleMessages.shift()});this._consoleCapturing=!0}if(G==="all")return[...this._consoleMessages];if(G==="error")return this._consoleMessages.filter((V)=>V.type==="error"||V.type==="warning");return this._consoleMessages.filter((V)=>V.type===G)}async tabs(){return[]}async selectTab(G){}async cdp(G,V={}){return this.bridge.send(G,V)}async handleJavaScriptDialog(G,V){await this.cdp("Page.handleJavaScriptDialog",{accept:G,...V!==void 0&&{promptText:V}})}async nativeClick(G,V){await this.cdp("Input.dispatchMouseEvent",{type:"mouseMoved",x:G,y:V});await this.cdp("Input.dispatchMouseEvent",{type:"mousePressed",x:G,y:V,button:"left",clickCount:1});await this.cdp("Input.dispatchMouseEvent",{type:"mouseReleased",x:G,y:V,button:"left",clickCount:1})}async nativeType(G){await this.cdp("Input.insertText",{text:G})}async insertText(G){await this.nativeType(G)}async nativeKeyPress(G,V=[]){let Q=0;for(const K of V){if(K==="Alt")Q|=1;if(K==="Ctrl"||K==="Control")Q|=2;if(K==="Meta")Q|=4;if(K==="Shift")Q|=8}await this.cdp("Input.dispatchKeyEvent",{type:"keyDown",key:G,modifiers:Q});await this.cdp("Input.dispatchKeyEvent",{type:"keyUp",key:G,modifiers:Q})}}function T(G){try{return new URL(G).hostname}catch{return}}function N(G){return H(G)&&typeof G.name==="string"&&typeof G.value==="string"&&typeof G.domain==="string"}function C(G,V){const Q=G.replace(/^\./,"").toLowerCase(),K=V.replace(/^\./,"").toLowerCase();return K===Q||K.endsWith(`.${Q}`)}function U(G){const V=b(process.env.OPENCLI_CDP_TARGET);return G.map((K,X)=>({target:K,index:X,score:q(K,V)})).filter(({score:K})=>Number.isFinite(K)).sort((K,X)=>{if(X.score!==K.score)return X.score-K.score;return K.index-X.index})[0]?.target}function q(G,V){if(!G.webSocketDebuggerUrl)return Number.NEGATIVE_INFINITY;const Q=(G.type??"").toLowerCase(),K=(G.url??"").toLowerCase(),X=(G.title??"").toLowerCase(),Z=`${X} ${K}`;if(!Z.trim()&&!Q)return Number.NEGATIVE_INFINITY;if(Z.includes("devtools"))return Number.NEGATIVE_INFINITY;if(Q==="background_page"||Q==="service_worker")return Number.NEGATIVE_INFINITY;let $=0;if(V&&V.test(Z))$+=1000;if(Q==="app")$+=120;else if(Q==="webview")$+=100;else if(Q==="page")$+=80;else if(Q==="iframe")$+=20;if(K.startsWith("http://localhost")||K.startsWith("https://localhost"))$+=90;if(K.startsWith("file://"))$+=60;if(K.startsWith("http://127.0.0.1")||K.startsWith("https://127.0.0.1"))$+=50;if(K.startsWith("about:blank"))$-=120;if(K===""||K==="about:blank")$-=40;if(X&&X!=="devtools")$+=25;const J=Object.values(w()).map((L)=>(L.displayName??L.processName).toLowerCase());for(const L of J)if(X.includes(L)){$+=120;break}for(const L of J)if(K.includes(L)){$+=100;break}return $}function b(G){const V=G?.trim();if(!V)return;return new RegExp(D(V.toLowerCase()))}function D(G){return G.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export const __test__={selectCDPTarget:U,scoreCDPTarget:q};function j(G){return new Promise((V,Q)=>{const K=new URL(G),X=(K.protocol==="https:"?F:W)(K,(Z)=>{const $=Z.statusCode??0;if($<200||$>=300){Z.resume();Q(Error(`Failed to fetch CDP targets: HTTP ${$}`));return}const J=[];Z.on("data",(L)=>J.push(Buffer.isBuffer(L)?L:Buffer.from(L)));Z.on("end",()=>{try{V(JSON.parse(Buffer.concat(J).toString("utf8")))}catch(L){Q(L instanceof Error?L:Error(String(L)))}})});X.on("error",Q);X.setTimeout(1e4,()=>X.destroy(Error("Timed out fetching CDP targets")));X.end()})}
|
|
@@ -19,8 +19,12 @@ export declare function isManagedChromeRunning(entry: ManagedProfileEntry): Prom
|
|
|
19
19
|
* 已在跑 → 直接复用;没在跑 → 拉起并等就绪。
|
|
20
20
|
* 「探测→拉起→等就绪」整段按 profile 互斥(进程内 Promise 去重 + 跨进程文件锁),
|
|
21
21
|
* 保证任一时刻同一 profile 只有一路真正执行启动。
|
|
22
|
+
* windowMode 决定窗口状态协调:background 新建窗口即最小化(真后台),
|
|
23
|
+
* foreground 把最小化窗口还原(登录等用户必须看见的流程)。
|
|
22
24
|
*/
|
|
23
|
-
export declare function ensureManagedChrome(name: string, entry: ManagedProfileEntry
|
|
25
|
+
export declare function ensureManagedChrome(name: string, entry: ManagedProfileEntry, opts?: {
|
|
26
|
+
windowMode?: 'foreground' | 'background';
|
|
27
|
+
}): Promise<string>;
|
|
24
28
|
/** 通过 CDP Browser.close 优雅关闭托管 Chrome。返回是否真的关了(本来没跑返回 false)。 */
|
|
25
29
|
export declare function stopManagedChrome(entry: ManagedProfileEntry): Promise<boolean>;
|
|
26
30
|
export type ManagedProfileOpts = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync as R}from"node:child_process";import*as B from"node:fs";import*as W from"node:path";import{request as x}from"node:http";import{WebSocket as T}from"ws";import{probeCDP as q,launchDetachedApp as v}from"../launcher.js";import{withBrowserSessionLock as C}from"../browser-session-lock.js";import{CommandExecutionError as O}from"../errors.js";import{log as J}from"../logger.js";import{addManagedProfile as M,getManagedProfile as N,listManagedProfiles as _,managedProfilesBaseDir as w,removeManagedProfileEntry as b,updateManagedProfile as A}from"./profile.js";import{proxyServerFlag as D}from"./proxy-test.js";import{resolveFingerprint as E,randomSeed as P}from"./fingerprint.js";const f=500,L=20000,j=9301,k=9399;function F(K){for(const H of K)try{B.accessSync(H,B.constants.X_OK);return H}catch{}return null}function g(K){for(const H of K)try{const Q=R("which",[H],{encoding:"utf-8",stdio:"pipe"}).trim();if(Q)return Q}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return F(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const H=process.env.ProgramFiles??"C:\\Program Files",Q=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Y=process.env.LOCALAPPDATA??"";return F([W.join(H,"Google","Chrome","Application","chrome.exe"),W.join(Q,"Google","Chrome","Application","chrome.exe"),...Y?[W.join(Y,"Google","Chrome","Application","chrome.exe")]:[],W.join(H,"Microsoft","Edge","Application","msedge.exe"),W.join(Q,"Microsoft","Edge","Application","msedge.exe")])}return g(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function G(K,H,Q=5000){return new Promise((Y,Z)=>{const V=new URL(H),$=x({hostname:V.hostname,port:V.port,path:V.pathname+V.search,method:K,timeout:Q},(z)=>{const U=[];z.on("data",(X)=>U.push(Buffer.isBuffer(X)?X:Buffer.from(X)));z.on("end",()=>{const X=Buffer.concat(U).toString("utf8");if((z.statusCode??0)<200||(z.statusCode??0)>=300){Z(Error(`HTTP ${z.statusCode}: ${X.slice(0,200)}`));return}try{Y(X?JSON.parse(X):null)}catch{Y(X)}})});$.on("error",Z);$.on("timeout",()=>$.destroy(Error(`请求 ${H} 超时`)));$.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return q(K.port)}const m=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function d(K){const H=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)H.push(...m);if(K.webrtc!==!1)H.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)H.push(`--proxy-server=${D(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)H.push(`--lang=${K.fingerprint.locale}`);H.push("about:blank");return H}function u(K){const H=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return H?{TZ:H}:void 0}async function y(K,H){const Q=Date.now()+L;while(Date.now()<Q){if(await q(K,1000))return;await new Promise((Y)=>setTimeout(Y,f))}throw new O(`托管 Chrome「${H}」已启动但 ${L/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}async function S(K){try{const H=await G("GET",`${K}/json`);if(!(Array.isArray(H)&&H.some((Y)=>Y.type==="page")))await G("PUT",`${K}/json/new?about:blank`)}catch(H){J.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${H instanceof Error?H.message:H}`)}}const I=new Map;export async function ensureManagedChrome(K,H){const Q=managedEndpoint(H),Y=I.get(K);if(Y)return Y;if(await q(H.port)){J.debug(`[managed-chrome] profile「${K}」已在端口 ${H.port} 运行`);await S(Q);return Q}const Z=I.get(K);if(Z)return Z;const V=C(async()=>{if(await q(H.port)){J.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await S(Q);return Q}const $=H.browserPath??discoverChromePath();if(!$)throw new O("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(H.userDataDir,{recursive:!0});J.debug(`[managed-chrome] 拉起 profile「${K}」:${$} 端口 ${H.port}${H.proxy?" (代理)":""}`);await v($,d(H),`托管 profile ${K}`,u(H));await y(H.port,K);await S(Q);return Q},`launch-${K}`).finally(()=>I.delete(K));I.set(K,V);return V}export async function stopManagedChrome(K){if(!await q(K.port))return!1;const Q=(await G("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Q)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise((Z,V)=>{const $=new T(Q),z=setTimeout(()=>{$.close();V(Error("CDP Browser.close 超时"))},1e4);$.on("open",()=>{$.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});$.on("close",()=>{clearTimeout(z);Z()});$.on("error",(U)=>{clearTimeout(z);V(U)})});const Y=Date.now()+5000;while(Date.now()<Y){if(!await q(K.port,500))break;await new Promise((Z)=>setTimeout(Z,200))}if(process.platform!=="win32"){const Z=W.join(K.userDataDir,"SingletonLock"),V=Date.now()+15000;while(Date.now()<V){try{B.lstatSync(Z)}catch{return!0}await new Promise(($)=>setTimeout($,200))}J.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${Z}`)}return!0}async function h(K){return!await q(K,500)}async function l(){const K=new Set(_().map(({entry:H})=>H.port));for(let H=j;H<=k;H++){if(K.has(H))continue;if(await h(H))return H}throw Error(`托管 profile 端口区间 ${j}-${k} 已耗尽`)}export async function resolveManagedOpts(K){const H={};if(K.label)H.label=K.label;if(K.proxy)H.proxy=K.proxy;if(K.webrtc===!1)H.webrtc=!1;if(K.clean===!1)H.clean=!1;if(K.fingerprint?.enabled){const Q=K.fingerprint,Y=Q.seed??P();H.fingerprint=E(Y,{timezone:Q.timezone,locale:Q.locale,userAgent:Q.userAgent,platform:Q.platform,spoof:Q.spoof,hardwareConcurrency:Q.hardwareConcurrency,deviceMemory:Q.deviceMemory,screen:Q.screen,webglVendor:Q.webglVendor,webglRenderer:Q.webglRenderer,canvasNoise:Q.canvasNoise})}return{opts:H}}export async function createManagedProfile(K,H={}){const Q=await l(),Y=W.join(w(),K.trim()),Z={userDataDir:Y,port:Q,...H.browserPath?{browserPath:H.browserPath}:{},...H.label?{label:H.label}:{},...H.proxy?{proxy:H.proxy}:{},...H.fingerprint?{fingerprint:H.fingerprint}:{},...H.webrtc===!1?{webrtc:!1}:{},...H.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};M(K,Z);B.rmSync(Y,{recursive:!0,force:!0});B.mkdirSync(Y,{recursive:!0});return Z}export async function updateManagedProfileConfig(K,H){const Q=N(K);if(!Q)throw Error(`托管 profile "${K}" 不存在`);const Y=H.webrtc!==void 0?H.webrtc:Q.webrtc,Z=H.clean!==void 0?H.clean:Q.clean,V={userDataDir:Q.userDataDir,port:Q.port,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.createdAt?{createdAt:Q.createdAt}:{},...H.label!==void 0?H.label?{label:H.label}:{}:Q.label?{label:Q.label}:{},...H.proxy!==void 0?H.proxy?{proxy:H.proxy}:{}:Q.proxy?{proxy:Q.proxy}:{},...H.fingerprint!==void 0?H.fingerprint?{fingerprint:H.fingerprint}:{}:Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Y===!1?{webrtc:!1}:{},...Z===!1?{clean:!1}:{}};let $=!1;try{$=await stopManagedChrome(Q)}catch(z){J.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${z instanceof Error?z.message:z}`)}A(K,V);return{entry:V,wasRunning:$}}async function c(K){const H=Date.now()+15000;let Q=0;while(Date.now()<H){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Y)=>setTimeout(Y,300));if(!B.existsSync(K)){if(++Q>=2)return}else Q=0}throw new O(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,H={}){const Q=N(K);if(!Q)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Q)}catch(Y){J.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Y instanceof Error?Y.message:Y}`)}if(!H.keepData){const Y=w(),Z=W.resolve(Q.userDataDir);if(Z.startsWith(Y+W.sep))await c(Z);else J.warn(`[managed-chrome] user-data-dir 不在 ${Y} 下,跳过数据删除:${Z}`)}b(K)}
|
|
1
|
+
import{execFileSync as D}from"node:child_process";import*as B from"node:fs";import*as q from"node:path";import{request as E}from"node:http";import{WebSocket as L}from"ws";import{probeCDP as I,launchDetachedApp as P}from"../launcher.js";import{withBrowserSessionLock as f}from"../browser-session-lock.js";import{CommandExecutionError as S}from"../errors.js";import{log as U}from"../logger.js";import{addManagedProfile as w,getManagedProfile as R,listManagedProfiles as u,managedProfilesBaseDir as C,removeManagedProfileEntry as y,updateManagedProfile as g}from"./profile.js";import{proxyServerFlag as m}from"./proxy-test.js";import{resolveFingerprint as h,randomSeed as l}from"./fingerprint.js";const c=500,_=20000,j=9301,v=9399;function A(K){for(const Q of K)try{B.accessSync(Q,B.constants.X_OK);return Q}catch{}return null}function d(K){for(const Q of K)try{const Y=D("which",[Q],{encoding:"utf-8",stdio:"pipe"}).trim();if(Y)return Y}catch{}return null}export function discoverChromePath(){const K=process.env.OPENCLI_CHROME_PATH?.trim();if(K)return K;if(process.platform==="darwin")return A(["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]);if(process.platform==="win32"){const Q=process.env.ProgramFiles??"C:\\Program Files",Y=process.env["ProgramFiles(x86)"]??"C:\\Program Files (x86)",Z=process.env.LOCALAPPDATA??"";return A([q.join(Q,"Google","Chrome","Application","chrome.exe"),q.join(Y,"Google","Chrome","Application","chrome.exe"),...Z?[q.join(Z,"Google","Chrome","Application","chrome.exe")]:[],q.join(Q,"Microsoft","Edge","Application","msedge.exe"),q.join(Y,"Microsoft","Edge","Application","msedge.exe")])}return d(["google-chrome-stable","google-chrome","chromium","chromium-browser","microsoft-edge-stable"])}function F(K,Q,Y=5000){return new Promise((Z,$)=>{const X=new URL(Q),H=E({hostname:X.hostname,port:X.port,path:X.pathname+X.search,method:K,timeout:Y},(V)=>{const J=[];V.on("data",(z)=>J.push(Buffer.isBuffer(z)?z:Buffer.from(z)));V.on("end",()=>{const z=Buffer.concat(J).toString("utf8");if((V.statusCode??0)<200||(V.statusCode??0)>=300){$(Error(`HTTP ${V.statusCode}: ${z.slice(0,200)}`));return}try{Z(z?JSON.parse(z):null)}catch{Z(z)}})});H.on("error",$);H.on("timeout",()=>H.destroy(Error(`请求 ${Q} 超时`)));H.end()})}export function managedEndpoint(K){return`http://127.0.0.1:${K.port}`}export async function isManagedChromeRunning(K){return I(K.port)}const i=["--no-first-run","--no-default-browser-check","--disable-background-networking","--disable-component-update","--disable-sync","--disable-domain-reliability","--disable-breakpad","--disable-client-side-phishing-detection","--no-pings","--no-service-autorun","--password-store=basic","--disable-features=Translate,OptimizationHints,MediaRouter,AutofillServerCommunication,InterestFeedContentSuggestions,CalculateNativeWinOcclusion"];function p(K){const Q=[`--user-data-dir=${K.userDataDir}`,`--remote-debugging-port=${K.port}`,"--remote-allow-origins=*"];if(K.clean!==!1)Q.push(...i);if(K.webrtc!==!1)Q.push("--force-webrtc-ip-handling-policy=disable_non_proxied_udp");if(K.proxy)Q.push(`--proxy-server=${m(K.proxy)}`);if(K.fingerprint?.enabled&&K.fingerprint.locale)Q.push(`--lang=${K.fingerprint.locale}`);Q.push("--no-startup-window");return Q}function o(K){const Q=K.fingerprint?.enabled?K.fingerprint.timezone:void 0;return Q?{TZ:Q}:void 0}async function s(K,Q){const Y=Date.now()+_;while(Date.now()<Y){if(await I(K,1000))return;await new Promise((Z)=>setTimeout(Z,c))}throw new S(`托管 Chrome「${Q}」已启动但 ${_/1000}s 内 CDP 端口 ${K} 未就绪`,"浏览器可能启动缓慢,稍后重试;或检查该端口是否被其它进程占用。")}const a="https://publishport.app/automation";async function b(K,Q){const Z=(await F("GET",`${K}/json/version`))?.webSocketDebuggerUrl;if(!Z)throw Error(`CDP 端点 ${K} 没有暴露 browser target`);const $=new L(Z);await new Promise((J,z)=>{const W=setTimeout(()=>{$.close();z(Error("browser CDP 连接超时"))},1e4);$.on("open",()=>{clearTimeout(W);J()});$.on("error",(G)=>{clearTimeout(W);z(G)})});let X=0;const H=new Map;$.on("message",(J)=>{try{const z=JSON.parse(String(J));if(!z.id||!H.has(z.id))return;const W=H.get(z.id);H.delete(z.id);if(z.error)W.reject(Error(z.error.message??"CDP error"));else W.resolve(z.result??{})}catch{}});const V=(J,z={})=>{const W=++X;return new Promise((G,T)=>{const k=setTimeout(()=>{H.delete(W);T(Error(`CDP ${J} 超时`))},1e4);H.set(W,{resolve:(N)=>{clearTimeout(k);G(N)},reject:(N)=>{clearTimeout(k);T(N)}});$.send(JSON.stringify({id:W,method:J,params:z}))})};try{return await Q(V)}finally{$.close()}}async function x(K,Q){try{const Y=await F("GET",`${K}/json`),Z=Array.isArray(Y)?Y.find(($)=>$.type==="page"):void 0;if(!Z)await b(K,async($)=>{const H=(await $("Target.createTarget",{url:a,background:!0})).targetId;if(H&&Q==="background"){const V=await $("Browser.getWindowForTarget",{targetId:H});if(V.windowId!==void 0)for(let J=0;J<4;J++){await $("Browser.setWindowBounds",{windowId:V.windowId,bounds:{windowState:"minimized"}});await new Promise((W)=>setTimeout(W,250));if((await $("Browser.getWindowBounds",{windowId:V.windowId})).bounds?.windowState==="minimized")break}}});else if(Q==="foreground"&&Z.id)await b(K,async($)=>{const X=await $("Browser.getWindowForTarget",{targetId:Z.id});if((await $("Browser.getWindowBounds",{windowId:X.windowId})).bounds?.windowState==="minimized")await $("Browser.setWindowBounds",{windowId:X.windowId,bounds:{windowState:"normal"}})})}catch(Y){U.debug(`[managed-chrome] ensurePageTarget 失败(非致命):${Y instanceof Error?Y.message:Y}`)}}const O=new Map;async function M(K){if(await I(K.port))return!0;if(process.platform==="win32")return!1;const Q=q.join(K.userDataDir,"SingletonLock"),Y=()=>{try{B.lstatSync(Q);return!0}catch{return!1}};if(!Y())return!1;for(let Z=0;Z<3;Z++){await new Promise(($)=>setTimeout($,500));if(await I(K.port,4000))return!0;if(!Y())return!1}return!1}export async function ensureManagedChrome(K,Q,Y={}){const Z=managedEndpoint(Q),$=O.get(K);if($)return $;if(await M(Q)){U.debug(`[managed-chrome] profile「${K}」已在端口 ${Q.port} 运行`);await x(Z,Y.windowMode);return Z}const X=O.get(K);if(X)return X;const H=f(async()=>{if(await M(Q)){U.debug(`[managed-chrome] profile「${K}」等锁期间已被拉起,直接复用`);await x(Z,Y.windowMode);return Z}const V=Q.browserPath??discoverChromePath();if(!V)throw new S("找不到可用的 Chrome/Chromium 浏览器。","安装 Google Chrome,或用 OPENCLI_CHROME_PATH 环境变量指定可执行文件路径。");B.mkdirSync(Q.userDataDir,{recursive:!0});U.debug(`[managed-chrome] 拉起 profile「${K}」:${V} 端口 ${Q.port}${Q.proxy?" (代理)":""}`);await P(V,p(Q),`托管 profile ${K}`,o(Q));await s(Q.port,K);await x(Z,Y.windowMode);return Z},`launch-${K}`).finally(()=>O.delete(K));O.set(K,H);return H}export async function stopManagedChrome(K){if(!await I(K.port))return!1;const Y=(await F("GET",`${managedEndpoint(K)}/json/version`))?.webSocketDebuggerUrl;if(!Y)throw Error(`端口 ${K.port} 上的 CDP 端点没有暴露 browser target`);await new Promise(($,X)=>{const H=new L(Y),V=setTimeout(()=>{H.close();X(Error("CDP Browser.close 超时"))},1e4);H.on("open",()=>{H.send(JSON.stringify({id:1,method:"Browser.close",params:{}}))});H.on("close",()=>{clearTimeout(V);$()});H.on("error",(J)=>{clearTimeout(V);X(J)})});const Z=Date.now()+5000;while(Date.now()<Z){if(!await I(K.port,500))break;await new Promise(($)=>setTimeout($,200))}if(process.platform!=="win32"){const $=q.join(K.userDataDir,"SingletonLock"),X=Date.now()+15000;while(Date.now()<X){try{B.lstatSync($)}catch{return!0}await new Promise((H)=>setTimeout(H,200))}U.warn(`[managed-chrome] Chrome 端口已释放但 15s 内 SingletonLock 未消失:${$}`)}return!0}async function n(K){return!await I(K,500)}async function t(){const K=new Set(u().map(({entry:Q})=>Q.port));for(let Q=j;Q<=v;Q++){if(K.has(Q))continue;if(await n(Q))return Q}throw Error(`托管 profile 端口区间 ${j}-${v} 已耗尽`)}export async function resolveManagedOpts(K){const Q={};if(K.label)Q.label=K.label;if(K.proxy)Q.proxy=K.proxy;if(K.webrtc===!1)Q.webrtc=!1;if(K.clean===!1)Q.clean=!1;if(K.fingerprint?.enabled){const Y=K.fingerprint,Z=Y.seed??l();Q.fingerprint=h(Z,{timezone:Y.timezone,locale:Y.locale,userAgent:Y.userAgent,platform:Y.platform,spoof:Y.spoof,hardwareConcurrency:Y.hardwareConcurrency,deviceMemory:Y.deviceMemory,screen:Y.screen,webglVendor:Y.webglVendor,webglRenderer:Y.webglRenderer,canvasNoise:Y.canvasNoise})}return{opts:Q}}export async function createManagedProfile(K,Q={}){const Y=await t(),Z=q.join(C(),K.trim()),$={userDataDir:Z,port:Y,...Q.browserPath?{browserPath:Q.browserPath}:{},...Q.label?{label:Q.label}:{},...Q.proxy?{proxy:Q.proxy}:{},...Q.fingerprint?{fingerprint:Q.fingerprint}:{},...Q.webrtc===!1?{webrtc:!1}:{},...Q.clean===!1?{clean:!1}:{},createdAt:new Date().toISOString()};w(K,$);B.rmSync(Z,{recursive:!0,force:!0});B.mkdirSync(Z,{recursive:!0});return $}export async function updateManagedProfileConfig(K,Q){const Y=R(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);const Z=Q.webrtc!==void 0?Q.webrtc:Y.webrtc,$=Q.clean!==void 0?Q.clean:Y.clean,X={userDataDir:Y.userDataDir,port:Y.port,...Y.browserPath?{browserPath:Y.browserPath}:{},...Y.createdAt?{createdAt:Y.createdAt}:{},...Q.label!==void 0?Q.label?{label:Q.label}:{}:Y.label?{label:Y.label}:{},...Q.proxy!==void 0?Q.proxy?{proxy:Q.proxy}:{}:Y.proxy?{proxy:Y.proxy}:{},...Q.fingerprint!==void 0?Q.fingerprint?{fingerprint:Q.fingerprint}:{}:Y.fingerprint?{fingerprint:Y.fingerprint}:{},...Z===!1?{webrtc:!1}:{},...$===!1?{clean:!1}:{}};let H=!1;try{H=await stopManagedChrome(Y)}catch(V){U.warn(`[managed-chrome] 更新前关闭 profile「${K}」失败(继续):${V instanceof Error?V.message:V}`)}g(K,X);return{entry:X,wasRunning:H}}async function r(K){const Q=Date.now()+15000;let Y=0;while(Date.now()<Q){try{B.rmSync(K,{recursive:!0,force:!0,maxRetries:3,retryDelay:100})}catch{}await new Promise((Z)=>setTimeout(Z,300));if(!B.existsSync(K)){if(++Y>=2)return}else Y=0}throw new S(`数据目录未能删净(浏览器可能仍在退出中):${K}`,"等浏览器窗口完全关闭后再删一次即可。")}export async function removeManagedProfile(K,Q={}){const Y=R(K);if(!Y)throw Error(`托管 profile "${K}" 不存在`);try{await stopManagedChrome(Y)}catch(Z){U.warn(`[managed-chrome] 关闭 profile「${K}」失败(继续删除):${Z instanceof Error?Z.message:Z}`)}if(!Q.keepData){const Z=C(),$=q.resolve(Y.userDataDir);if($.startsWith(Z+q.sep))await r($);else U.warn(`[managed-chrome] user-data-dir 不在 ${Z} 下,跳过数据删除:${$}`)}y(K)}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import*as I from"node:fs";import*as zq from"node:os";import*as M from"node:path";import{fileURLToPath as sK}from"node:url";import{Command as _q,InvalidArgumentError as WK,Option as tK}from"commander";import eK from"js-yaml";import{findPackageRoot,getBuiltEntryCandidates as qQ}from"./package-paths.js";import{fullName as Cq,getRegistry as Dq,strategyLabel as JK}from"./registry.js";import{serializeCommand as KQ,formatArgSummary as QQ}from"./serialization.js";import{render as Aq}from"./output.js";import{PKG_VERSION as Fq}from"./version.js";import{printCompletionScript as XQ}from"./completion.js";import{loadExternalClis as Tq,executeExternalCli as YQ,installExternalCli as ZQ,registerExternalCli as $Q,isBinaryInstalled as wq,formatExternalCliLabel as VK}from"./external.js";import{listOpenCliSkills as HQ,readOpenCliSkill as zQ}from"./skills.js";import{registerAllCommands as NQ}from"./commanderAdapter.js";import{classifyAdapter as jK,formatRootAdapterHelpText as UQ,installCommanderNamespaceStructuredHelp as Nq,installStructuredHelp as GQ,leadingPositionalFromUsage as WQ,rootHelpData as JQ}from"./help.js";import{EXIT_CODES as J,getErrorMessage as h,BrowserConnectError as VQ,CliError as hq,toEnvelope as jQ}from"./errors.js";import{TargetError as LK}from"./browser/target-errors.js";import{resolveTargetJs as LQ,getTextResolvedJs as _Q,getValueResolvedJs as DQ,getAttributesResolvedJs as AQ,selectResolvedJs as BQ,isAutocompleteResolvedJs as FQ}from"./browser/target-resolver.js";import{buildFindJs as RQ,buildSemanticFindJs as _K,isFindError as DK}from"./browser/find.js";import{inferShape as AK}from"./browser/shape.js";import{assignKeys as BK}from"./browser/network-key.js";import{DEFAULT_TTL_MS as FK,findEntry as EQ,loadNetworkCache as PQ,saveNetworkCache as OQ}from"./browser/network-cache.js";import{parseFilter as kQ,shapeMatchesFilter as IQ}from"./browser/shape-filter.js";import{buildHtmlTreeJs as MQ}from"./browser/html-tree.js";import{buildExtractHtmlJs as SQ,runExtractFromHtml as bQ}from"./browser/extract.js";import{analyzeSite as CQ}from"./browser/analyze.js";import{registerAuthCommands as TQ}from"./commands/auth.js";import{daemonRestart as wQ,daemonStatus as hQ,daemonStop as yQ,daemonWarm as vQ}from"./commands/daemon.js";import{log as n}from"./logger.js";import{bindTab as fQ,BrowserCommandError as Bq,fetchDaemonStatus as xQ,sendCommand as uQ}from"./browser/daemon-client.js";import{aliasForContextId as RK,listManagedProfiles as yq,loadProfileConfig as cQ,renameProfile as mQ,resolveProfileContextId as dQ,setDefaultProfile as gQ}from"./browser/profile.js";import{createManagedProfile as nQ,ensureManagedChrome as vq,isManagedChromeRunning as lQ,removeManagedProfile as iQ,stopManagedChrome as oQ,resolveManagedOpts as EK,updateManagedProfileConfig as pQ}from"./browser/managed-chrome.js";import{testProxy as aQ,parseProxyUrl as fq}from"./browser/proxy-test.js";import{formatDaemonVersion as rQ,isDaemonStale as sQ}from"./browser/daemon-version.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as xq}from"./browser/config.js";const uq=sK(import.meta.url),tQ='Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"',PK=1000;function Rq(N,H){if(N===void 0||N===null||N==="")return null;const $=String(N).trim(),V=/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec($);if(!V)return{error:`--${H} must be a duration like 500ms, 30s, 2m, got "${$}"`};const B=Number.parseFloat(V[1]),j=V[2]??"ms";return Math.round(B*(j==="h"?3600000:j==="m"?60000:j==="s"?1000:1))}function cq(N){return typeof N==="number"&&Number.isFinite(N)&&N>0?N:Date.now()}function Uq(N){if(typeof N!=="number"||!Number.isFinite(N)||N<=0)return;return new Date(N).toISOString()}function mq(N,H,$=Date.now()){const V=H.sinceMs!=null?$-H.sinceMs:void 0,B=H.untilMs!=null?$-H.untilMs:void 0;return N.filter((j)=>{const S=j.timestamp??$;if(V!==void 0&&S<V)return!1;if(B!==void 0&&S>B)return!1;return!0})}export function selectFreshByTimestamp(N,H){const $=N.filter((B)=>Number(B.timestamp??0)>H),V=$.length>0?Math.max(H,...$.map((B)=>Number(B.timestamp??0)).filter(Number.isFinite)):H;return{fresh:$,lastSeenTs:V}}async function Gq(N){if(N.readNetworkCapture){const $=await N.readNetworkCapture();if(Array.isArray($)&&$.length>0)return $.map((V)=>{const B=V.responsePreview??null;let j=null;if(B)try{j=JSON.parse(B)}catch{j=B}const S=typeof V.responseBodyFullSize==="number"?V.responseBodyFullSize:B?B.length:0,F=V.responseBodyTruncated===!0;return{url:V.url||"",method:V.method||"GET",status:V.responseStatus||0,size:S,ct:V.responseContentType||"",body:j,bodyFullSize:S,bodyTruncated:F,timestamp:cq(V.timestamp)}})}const H=await N.evaluate("(function(){ var out = window.__opencli_net || []; window.__opencli_net = []; return JSON.stringify(out); })()");try{return JSON.parse(H).map((V)=>({...V,timestamp:cq(V.timestamp)}))}catch{if(process.env.OPENCLI_VERBOSE)n.warn(`[network] Failed to parse interceptor buffer: ${typeof H==="string"?H.slice(0,200):String(H)}`);return[]}}function OK(N){return N.filter((H)=>{const $=H.ct?.toLowerCase()??"";return($.includes("json")||$.includes("xml")||$.includes("text/plain")||$.includes("javascript"))&&!/\.(js|css|png|jpg|gif|svg|woff|ico|map)(\?|$)/i.test(H.url)&&!/analytics|tracking|telemetry|beacon|pixel|gtag|fbevents/i.test(H.url)})}const eQ={invalid_args:J.USAGE_ERROR,invalid_filter:J.USAGE_ERROR,invalid_max_body:J.USAGE_ERROR};function i(N,H,$={}){console.log(JSON.stringify({error:{code:N,message:H,...$}},null,2));process.exitCode=eQ[N]??J.GENERIC_ERROR}const qX="Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.";function KX(N,H=Dq()){let $;try{$=new URL(N).hostname.toLowerCase().replace(/^www\./,"")}catch{return[]}const V=new Map;for(const F of H.values()){if(!F.domain)continue;let P=F.domain.toLowerCase().trim();try{P=new URL(P.includes("://")?P:`https://${P}`).hostname.toLowerCase()}catch{P=P.split("/")[0]??P}P=P.replace(/^www\./,"");if(!P)continue;if($===P||$.endsWith(`.${P}`))V.set(F.site,Math.max(V.get(F.site)??0,P.length))}const B=[...V.entries()].sort((F,P)=>P[1]-F[1]||F[0].localeCompare(P[0])).map(([F])=>F),j=$.split(".").filter(Boolean),S=j.length>=2?j[j.length-2]:j[0];return[...new Set([...B,...S?[S]:[]])]}function kK(N,H){return N.find(($)=>H($))}function QX(N,H){const $=N.replace(/[^a-zA-Z0-9_-]+/g,"-");if(!$)return{};const V=M.join(H.homeDir,".ppcli","sites",$);return{local:kK([M.join(V,"sitemap"),M.join(V,"sitemap.md")],H.fileExists),global:kK([M.join(H.packageRoot,"sitemaps",$),M.join(H.packageRoot,"sitemaps",`${$}.md`)],H.fileExists)}}export function resolveSitemapAvailabilityForUrl(N,H={}){const $=H.homeDir??zq.homedir(),V=H.packageRoot??findPackageRoot(uq),B=H.registry??Dq(),j=H.fileExists??I.existsSync;for(const S of KX(N,B)){const F=QX(S,{homeDir:$,packageRoot:V,fileExists:j});if(!F.local&&!F.global)continue;const P=F.local&&F.global?"local+global":F.local?"local":"global";return{site:S,available:!0,source:P,hint:qX,paths:F}}return null}function IK(N){const H=N.replace(/[^a-zA-Z0-9_-]+/g,"_");return M.join(SK(),"browser-sitemap-hints",`${H}.json`)}function MK(N){try{const H=JSON.parse(I.readFileSync(IK(N),"utf-8"));if(H&&typeof H==="object"&&Array.isArray(H.seenSites))return{seenSites:H.seenSites.filter(($)=>typeof $==="string"),updatedAt:typeof H.updatedAt==="string"?H.updatedAt:new Date(0).toISOString()}}catch{}return{seenSites:[],updatedAt:new Date(0).toISOString()}}function XX(N,H){const $=MK(N);if(!$.seenSites.includes(H))$.seenSites.push(H);const V=IK(N);I.mkdirSync(M.dirname(V),{recursive:!0});I.writeFileSync(V,JSON.stringify({seenSites:$.seenSites,updatedAt:new Date().toISOString()}),"utf-8")}function YX(N,H,$){const V=resolveSitemapAvailabilityForUrl(N);if(!V)return null;if(!$.oncePerSession)return V;if(MK(H).seenSites.includes(V.site))return null;XX(H,V.site);return V}export function checkSiteMemory(N){const H=M.join(zq.homedir(),".ppcli","sites",N),$=M.join(H,"endpoints.json"),V=M.join(H,"notes.md");let B=0,j=I.existsSync($);if(j)try{const F=JSON.parse(I.readFileSync($,"utf-8"));if(F&&typeof F==="object"&&!Array.isArray(F))B=Object.keys(F).length;else if(Array.isArray(F))B=F.length}catch{j=!1}const S=I.existsSync(V);return{ok:j&&B>0&&S,siteDir:H,endpoints:{present:j,count:B,path:$},notes:{present:S,path:V}}}export function printSiteMemoryReport(N,H){if(N.ok){console.log(` ✓ Memory: endpoints.json (${N.endpoints.count}), notes.md present at ${N.siteDir}`);return}const $=H?"✗":"⚠",V=[];if(!N.endpoints.present)V.push("endpoints.json");else if(N.endpoints.count===0)V.push("endpoints.json (empty)");if(!N.notes.present)V.push("notes.md");console.log(` ${$} Memory: missing ${V.join(", ")} under ${N.siteDir}`);console.log(" Write the endpoint you just verified + a 1-line session note so the next agent starts from minute 0, not minute 95.");if(!H)console.log(" (Re-run with --strict-memory to fail instead of warn.)")}export function normalizeVerifyRows(N){if(Array.isArray(N))return N.map((H)=>H&&typeof H==="object"?H:{value:H});if(N&&typeof N==="object"){const H=N;for(const $ of["rows","items","data","results"])if(Array.isArray(H[$]))return H[$].map((V)=>V&&typeof V==="object"?V:{value:V});return[H]}return[]}export function renderVerifyPreview(N,H={}){const $=H.maxRows??10,V=H.maxCols??6,B=H.cellMax??40;if(N.length===0)return" (no rows)";const j=Array.from(new Set(N.flatMap((D)=>Object.keys(D)))),S=j.slice(0,V),F=N.slice(0,$),P=(D)=>{if(D===null||D===void 0)return"";return(typeof D==="object"?JSON.stringify(D):String(D)).replace(/\s+/g," ").slice(0,B)},Kq=S.map((D)=>Math.max(D.length,...F.map((o)=>P(o[D]).length))),e=(D)=>D.map((o,Xq)=>o.padEnd(Kq[Xq])).join(" "),r=[];r.push(` ${e(S)}`);r.push(` ${Kq.map((D)=>"-".repeat(D)).join(" ")}`);for(const D of F)r.push(` ${e(S.map((o)=>P(D[o])))}`);if(N.length>$)r.push(` ... and ${N.length-$} more row(s)`);if(j.length>V)r.push(` (${j.length-V} more column(s) hidden)`);return r.join(`
|
|
2
|
-
`)}function
|
|
1
|
+
import*as I from"node:fs";import*as Nq from"node:os";import*as M from"node:path";import{fileURLToPath as eK}from"node:url";import{Command as Aq,InvalidArgumentError as LK,Option as qQ}from"commander";import KQ from"js-yaml";import{findPackageRoot,getBuiltEntryCandidates as QQ}from"./package-paths.js";import{fullName as vq,getRegistry as Bq,strategyLabel as _K}from"./registry.js";import{serializeCommand as XQ,formatArgSummary as YQ}from"./serialization.js";import{render as Fq}from"./output.js";import{PKG_VERSION as Pq}from"./version.js";import{printCompletionScript as ZQ}from"./completion.js";import{loadExternalClis as fq,executeExternalCli as $Q,installExternalCli as HQ,registerExternalCli as zQ,isBinaryInstalled as xq,formatExternalCliLabel as DK}from"./external.js";import{listOpenCliSkills as NQ,readOpenCliSkill as UQ}from"./skills.js";import{registerAllCommands as GQ}from"./commanderAdapter.js";import{classifyAdapter as AK,formatRootAdapterHelpText as WQ,installCommanderNamespaceStructuredHelp as Uq,installStructuredHelp as JQ,leadingPositionalFromUsage as VQ,rootHelpData as jQ}from"./help.js";import{EXIT_CODES as J,getErrorMessage as h,BrowserConnectError as LQ,CliError as uq,toEnvelope as _Q}from"./errors.js";import{TargetError as BK}from"./browser/target-errors.js";import{resolveTargetJs as DQ,getTextResolvedJs as AQ,getValueResolvedJs as BQ,getAttributesResolvedJs as FQ,selectResolvedJs as RQ,isAutocompleteResolvedJs as EQ}from"./browser/target-resolver.js";import{buildFindJs as PQ,buildSemanticFindJs as FK,isFindError as RK}from"./browser/find.js";import{inferShape as EK}from"./browser/shape.js";import{assignKeys as PK}from"./browser/network-key.js";import{DEFAULT_TTL_MS as OK,findEntry as OQ,loadNetworkCache as kQ,saveNetworkCache as IQ}from"./browser/network-cache.js";import{parseFilter as MQ,shapeMatchesFilter as SQ}from"./browser/shape-filter.js";import{buildHtmlTreeJs as bQ}from"./browser/html-tree.js";import{buildExtractHtmlJs as CQ,runExtractFromHtml as TQ}from"./browser/extract.js";import{analyzeSite as wQ}from"./browser/analyze.js";import{registerAuthCommands as hQ}from"./commands/auth.js";import{daemonRestart as yQ,daemonStatus as vQ,daemonStop as fQ,daemonWarm as xQ}from"./commands/daemon.js";import{log as n}from"./logger.js";import{bindTab as uQ,BrowserCommandError as Rq,fetchDaemonStatus as cQ,sendCommand as mQ}from"./browser/daemon-client.js";import{aliasForContextId as kK,listManagedProfiles as cq,loadProfileConfig as dQ,renameProfile as gQ,resolveProfile as mq,resolveProfileContextId as nQ,setDefaultProfile as lQ}from"./browser/profile.js";import{createManagedProfile as iQ,ensureManagedChrome as dq,isManagedChromeRunning as oQ,removeManagedProfile as pQ,stopManagedChrome as aQ,resolveManagedOpts as IK,updateManagedProfileConfig as rQ}from"./browser/managed-chrome.js";import{testProxy as sQ,parseProxyUrl as gq}from"./browser/proxy-test.js";import{formatDaemonVersion as tQ,isDaemonStale as eQ}from"./browser/daemon-version.js";import{DEFAULT_BROWSER_CONNECT_TIMEOUT as Oq}from"./browser/config.js";const nq=eK(import.meta.url),qX='Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"',MK=1000;function kq(H,z){if(H===void 0||H===null||H==="")return null;const $=String(H).trim(),V=/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec($);if(!V)return{error:`--${z} must be a duration like 500ms, 30s, 2m, got "${$}"`};const B=Number.parseFloat(V[1]),j=V[2]??"ms";return Math.round(B*(j==="h"?3600000:j==="m"?60000:j==="s"?1000:1))}function lq(H){return typeof H==="number"&&Number.isFinite(H)&&H>0?H:Date.now()}function Gq(H){if(typeof H!=="number"||!Number.isFinite(H)||H<=0)return;return new Date(H).toISOString()}function iq(H,z,$=Date.now()){const V=z.sinceMs!=null?$-z.sinceMs:void 0,B=z.untilMs!=null?$-z.untilMs:void 0;return H.filter((j)=>{const S=j.timestamp??$;if(V!==void 0&&S<V)return!1;if(B!==void 0&&S>B)return!1;return!0})}export function selectFreshByTimestamp(H,z){const $=H.filter((B)=>Number(B.timestamp??0)>z),V=$.length>0?Math.max(z,...$.map((B)=>Number(B.timestamp??0)).filter(Number.isFinite)):z;return{fresh:$,lastSeenTs:V}}async function Wq(H){if(H.readNetworkCapture){const $=await H.readNetworkCapture();if(Array.isArray($)&&$.length>0)return $.map((V)=>{const B=V.responsePreview??null;let j=null;if(B)try{j=JSON.parse(B)}catch{j=B}const S=typeof V.responseBodyFullSize==="number"?V.responseBodyFullSize:B?B.length:0,R=V.responseBodyTruncated===!0;return{url:V.url||"",method:V.method||"GET",status:V.responseStatus||0,size:S,ct:V.responseContentType||"",body:j,bodyFullSize:S,bodyTruncated:R,timestamp:lq(V.timestamp)}})}const z=await H.evaluate("(function(){ var out = window.__opencli_net || []; window.__opencli_net = []; return JSON.stringify(out); })()");try{return JSON.parse(z).map((V)=>({...V,timestamp:lq(V.timestamp)}))}catch{if(process.env.OPENCLI_VERBOSE)n.warn(`[network] Failed to parse interceptor buffer: ${typeof z==="string"?z.slice(0,200):String(z)}`);return[]}}function SK(H){return H.filter((z)=>{const $=z.ct?.toLowerCase()??"";return($.includes("json")||$.includes("xml")||$.includes("text/plain")||$.includes("javascript"))&&!/\.(js|css|png|jpg|gif|svg|woff|ico|map)(\?|$)/i.test(z.url)&&!/analytics|tracking|telemetry|beacon|pixel|gtag|fbevents/i.test(z.url)})}const KX={invalid_args:J.USAGE_ERROR,invalid_filter:J.USAGE_ERROR,invalid_max_body:J.USAGE_ERROR};function p(H,z,$={}){console.log(JSON.stringify({error:{code:H,message:z,...$}},null,2));process.exitCode=KX[H]??J.GENERIC_ERROR}const QX="Site sitemap available. For navigation context, use the opencli-browser-sitemap skill; treat browser state as truth if it disagrees.";function XX(H,z=Bq()){let $;try{$=new URL(H).hostname.toLowerCase().replace(/^www\./,"")}catch{return[]}const V=new Map;for(const R of z.values()){if(!R.domain)continue;let P=R.domain.toLowerCase().trim();try{P=new URL(P.includes("://")?P:`https://${P}`).hostname.toLowerCase()}catch{P=P.split("/")[0]??P}P=P.replace(/^www\./,"");if(!P)continue;if($===P||$.endsWith(`.${P}`))V.set(R.site,Math.max(V.get(R.site)??0,P.length))}const B=[...V.entries()].sort((R,P)=>P[1]-R[1]||R[0].localeCompare(P[0])).map(([R])=>R),j=$.split(".").filter(Boolean),S=j.length>=2?j[j.length-2]:j[0];return[...new Set([...B,...S?[S]:[]])]}function bK(H,z){return H.find(($)=>z($))}function YX(H,z){const $=H.replace(/[^a-zA-Z0-9_-]+/g,"-");if(!$)return{};const V=M.join(z.homeDir,".ppcli","sites",$);return{local:bK([M.join(V,"sitemap"),M.join(V,"sitemap.md")],z.fileExists),global:bK([M.join(z.packageRoot,"sitemaps",$),M.join(z.packageRoot,"sitemaps",`${$}.md`)],z.fileExists)}}export function resolveSitemapAvailabilityForUrl(H,z={}){const $=z.homeDir??Nq.homedir(),V=z.packageRoot??findPackageRoot(nq),B=z.registry??Bq(),j=z.fileExists??I.existsSync;for(const S of XX(H,B)){const R=YX(S,{homeDir:$,packageRoot:V,fileExists:j});if(!R.local&&!R.global)continue;const P=R.local&&R.global?"local+global":R.local?"local":"global";return{site:S,available:!0,source:P,hint:QX,paths:R}}return null}function CK(H){const z=H.replace(/[^a-zA-Z0-9_-]+/g,"_");return M.join(wK(),"browser-sitemap-hints",`${z}.json`)}function TK(H){try{const z=JSON.parse(I.readFileSync(CK(H),"utf-8"));if(z&&typeof z==="object"&&Array.isArray(z.seenSites))return{seenSites:z.seenSites.filter(($)=>typeof $==="string"),updatedAt:typeof z.updatedAt==="string"?z.updatedAt:new Date(0).toISOString()}}catch{}return{seenSites:[],updatedAt:new Date(0).toISOString()}}function ZX(H,z){const $=TK(H);if(!$.seenSites.includes(z))$.seenSites.push(z);const V=CK(H);I.mkdirSync(M.dirname(V),{recursive:!0});I.writeFileSync(V,JSON.stringify({seenSites:$.seenSites,updatedAt:new Date().toISOString()}),"utf-8")}function $X(H,z,$){const V=resolveSitemapAvailabilityForUrl(H);if(!V)return null;if(!$.oncePerSession)return V;if(TK(z).seenSites.includes(V.site))return null;ZX(z,V.site);return V}export function checkSiteMemory(H){const z=M.join(Nq.homedir(),".ppcli","sites",H),$=M.join(z,"endpoints.json"),V=M.join(z,"notes.md");let B=0,j=I.existsSync($);if(j)try{const R=JSON.parse(I.readFileSync($,"utf-8"));if(R&&typeof R==="object"&&!Array.isArray(R))B=Object.keys(R).length;else if(Array.isArray(R))B=R.length}catch{j=!1}const S=I.existsSync(V);return{ok:j&&B>0&&S,siteDir:z,endpoints:{present:j,count:B,path:$},notes:{present:S,path:V}}}export function printSiteMemoryReport(H,z){if(H.ok){console.log(` ✓ Memory: endpoints.json (${H.endpoints.count}), notes.md present at ${H.siteDir}`);return}const $=z?"✗":"⚠",V=[];if(!H.endpoints.present)V.push("endpoints.json");else if(H.endpoints.count===0)V.push("endpoints.json (empty)");if(!H.notes.present)V.push("notes.md");console.log(` ${$} Memory: missing ${V.join(", ")} under ${H.siteDir}`);console.log(" Write the endpoint you just verified + a 1-line session note so the next agent starts from minute 0, not minute 95.");if(!z)console.log(" (Re-run with --strict-memory to fail instead of warn.)")}export function normalizeVerifyRows(H){if(Array.isArray(H))return H.map((z)=>z&&typeof z==="object"?z:{value:z});if(H&&typeof H==="object"){const z=H;for(const $ of["rows","items","data","results"])if(Array.isArray(z[$]))return z[$].map((V)=>V&&typeof V==="object"?V:{value:V});return[z]}return[]}export function renderVerifyPreview(H,z={}){const $=z.maxRows??10,V=z.maxCols??6,B=z.cellMax??40;if(H.length===0)return" (no rows)";const j=Array.from(new Set(H.flatMap((D)=>Object.keys(D)))),S=j.slice(0,V),R=H.slice(0,$),P=(D)=>{if(D===null||D===void 0)return"";return(typeof D==="object"?JSON.stringify(D):String(D)).replace(/\s+/g," ").slice(0,B)},r=S.map((D)=>Math.max(D.length,...R.map((l)=>P(l[D]).length))),qq=(D)=>D.map((l,Kq)=>l.padEnd(r[Kq])).join(" "),o=[];o.push(` ${qq(S)}`);o.push(` ${r.map((D)=>"-".repeat(D)).join(" ")}`);for(const D of R)o.push(` ${qq(S.map((l)=>P(D[l])))}`);if(H.length>$)o.push(` ... and ${H.length-$} more row(s)`);if(j.length>V)o.push(` (${j.length-V} more column(s) hidden)`);return o.join(`
|
|
2
|
+
`)}function wK(){return process.env.OPENCLI_CACHE_DIR||M.join(Nq.homedir(),".ppcli","cache")}function hK(H){const z=H.replace(/[^a-zA-Z0-9_-]+/g,"_");return M.join(wK(),"browser-state",`${z}.json`)}function yK(H){try{const z=I.readFileSync(hK(H),"utf-8"),$=JSON.parse(z);return $&&typeof $==="object"?$:null}catch{return null}}function Jq(H,z){const $=hK(z);if(!H){I.rmSync($,{force:!0});return}I.mkdirSync(M.dirname($),{recursive:!0});I.writeFileSync($,JSON.stringify({defaultPage:H,updatedAt:new Date().toISOString()}),"utf-8")}function HX(H,z){return H.some(($)=>{return typeof $==="object"&&$!==null&&"page"in $&&typeof $.page==="string"&&$.page===z})}async function oq(H,z,$){const V=z.trim();if(!V)return;let B;try{B=await H.tabs()}catch(j){if($.source==="saved"){Jq(void 0,$.scope);return}throw Error(`Target tab ${V} could not be validated in the current browser session. The Browser Bridge session may have restarted; re-run "ppcli browser tab list" and choose a current target.`,{cause:j})}if(Array.isArray(B)&&HX(B,V))return V;if($.source==="saved"){Jq(void 0,$.scope);return}throw Error(`Target tab ${V} is not part of the current browser session. The Browser Bridge session may have restarted; re-run "ppcli browser tab list" and choose a current target.`)}function Iq(H,z){return z?`${z}:${H}`:H}async function zX(H,z){const $=yK(z)?.defaultPage?.trim();if(!$)return;return oq(H,$,{scope:z,source:"saved"})}async function NX(H,z,$,V={}){const B=mq(V.profile);if(B.kind==="managed"){if(z)throw Error(`托管环境「${B.name}」是单窗口复用标签模型,不支持 --tab 定位。直接省略 --tab 即可。`);const D=V.windowMode??pq(void 0,"foreground"),{ensureManagedChrome:l}=await import("./browser/managed-chrome.js"),{CDPBridge:Kq}=await import("./browser/cdp.js"),{generateFingerprintJs:Xq}=await import("./browser/fingerprint.js"),Eq=await l(B.name,B.entry,{windowMode:D}),Vq=B.entry.fingerprint,jq=B.entry.proxy;return new Kq().connect({timeout:Oq,session:H,surface:"browser",cdpEndpoint:Eq,contextId:B.name,windowMode:D,...Vq?.enabled?{initScript:Xq(Vq)}:{},...jq?.username?{proxyAuth:{username:jq.username,password:jq.password??""}}:{}})}const{BrowserBridge:j}=await import("./browser/index.js"),S=new j,R=process.env.OPENCLI_BROWSER_IDLE_TIMEOUT,P=R?parseInt(R,10):void 0,r=await S.connect({timeout:Oq,session:H,surface:"browser",...$&&{contextId:$},...P&&P>0&&{idleTimeout:P},windowMode:V.windowMode??pq(void 0,"foreground")}),qq=Iq(H,$),o=z?await oq(r,z,{scope:qq,source:"explicit"}):await zX(r,qq);if(o){if(!r.setActivePage)throw Error("This browser session does not support explicit tab targeting");r.setActivePage(o)}return r}function pq(H,z){const $=aq(H,"window");if($!==void 0&&$!==""){if($==="foreground"||$==="background")return $;throw Error(`--window must be one of: foreground, background. Received: "${String($)}"`)}const V=process.env.OPENCLI_WINDOW;if(V!==void 0&&V!==""){if(V==="foreground"||V==="background")return V;throw Error(`OPENCLI_WINDOW must be one of: foreground, background. Received: "${V}"`)}return z}function E(H){return H.option("--tab <targetId>",qX)}function UX(H){if(!H)return;const z=H.optsWithGlobals?H.optsWithGlobals():H.opts();return typeof z.tab==="string"&&z.tab.trim()?z.tab.trim():void 0}function aq(H,z){let $=H;while($){const V=$.opts();if(Object.prototype.hasOwnProperty.call(V,z)&&V[z]!==void 0)return V[z];$=$.parent}return}function rq(H){const z=aq(H,"session");if(typeof z==="string"&&z.trim())return z.trim();throw Error("<session> is a required positional argument: ppcli browser <session> <command>")}function sq(H){return nQ(Mq(H))}function Mq(H){const z=aq(H,"profile");return typeof z==="string"&&z.trim()?z.trim():void 0}function tq(H){const z=H.session;if(typeof z==="string"&&z.trim())return z.trim();throw Error("Browser page is missing a session")}function Sq(H){const z=H.contextId;return Iq(tq(H),typeof z==="string"&&z.trim()?z.trim():void 0)}function GX(H){return typeof H==="string"?H:JSON.stringify(H,null,2)}function WX(H,z){const $=GX(H),V=$.match(/^interactive:\s*(\d+)\s*$/m);return{ok:!0,chars:$.length,bytes:Buffer.byteLength($,"utf8"),lines:$?$.split(/\r?\n/).length:0,approx_tokens:Math.ceil($.length/4),refs:($.match(/(^|\n)\s*\[\d+\]/g)??[]).length,frame_sections:($.match(/(^|\n)frame /g)??[]).length,...V?{interactive:Number(V[1])}:{},elapsed_ms:z}}async function vK(H,z){const $=Date.now();try{const V=await H.snapshot({viewportExpand:2000,source:z});return WX(V,Date.now()-$)}catch(V){return{ok:!1,elapsed_ms:Date.now()-$,error:{...V instanceof Error&&"code"in V?{code:String(V.code)}:{},message:V instanceof Error?V.message:String(V)}}}}function fK(H,z){if(typeof H==="string"&&H.trim())return H.trim();const $=z instanceof Aq?z.opts().tab:z?.tab;if(typeof $==="string"&&$.trim())return $.trim();return}function xK(H,z,$){if(H===void 0)return $;const V=parseInt(H,10);if(Number.isNaN(V)||V<=0){console.error(`[cli] Invalid ${z}="${H}", using default ${$}`);return $}return V}function uK(H,z){if(!/^\d+$/.test(H))throw new LK(`--${z} must be a positive integer (got "${H}")`);const $=parseInt(H,10);if($<=0)throw new LK(`--${z} must be a positive integer (got "${H}")`);return $}function JX(H){if(H.verbose)process.env.OPENCLI_VERBOSE="1"}function VX(H){return[...new Set(H.commands.map((z)=>z.name()))].sort((z,$)=>z.localeCompare($)).join(", ")}function jX(H){for(const z of H.commands){if(z.commands.length===0)continue;const $=VX(z);if($)z.description($)}}export function createProgram(H,z){const $=new Aq;$.name("ppcli").description("Make any website your CLI. Zero setup. AI-powered.").version(Pq).option("--profile <name>","Chrome profile/context alias for Browser Bridge commands").enablePositionalOptions();$.command("list").description("List all available CLI commands").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((q)=>{const K=Bq(),X=[...new Set(K.values())].sort((W,_)=>vq(W).localeCompare(vq(_))),Q=q.format,Y=Q==="json"||Q==="yaml";if(Q!=="table"){const W=Y?X.map(XQ):X.map((_)=>({command:vq(_),site:_.site,name:_.name,aliases:_.aliases?.join(", ")??"",description:_.description,access:_.access,strategy:_K(_),browser:!!_.browser,args:YQ(_.args)}));Fq(W,{fmt:Q,columns:["command","site","name","aliases","description","access","strategy","browser","args",...Y?["columns","domain"]:[]],title:"ppcli/list",source:"ppcli list"});return}const Z=new Map,N=new Map;for(const W of X){const _=AK(W.domain)==="app"?Z:N,F=_.get(W.site)??[];F.push(W);_.set(W.site,F)}const U=(W,_)=>{console.log(` ${W}`);for(const F of _){const k=_K(F),f=k==="public"?"[public]":`[${k}]`,y=F.aliases?.length?` (aliases: ${F.aliases.join(", ")})`:"";console.log(` ${F.name} ${f}${y}${F.description?` — ${F.description}`:""}`)}console.log()};console.log();console.log(" ppcli"+" — available commands");console.log();if(Z.size>0){console.log(" App adapters");console.log();for(const[W,_]of Z)U(W,_)}if(N.size>0){console.log(" Site adapters");console.log();for(const[W,_]of N)U(W,_)}const G=fq();if(G.length>0){console.log(" external CLIs");for(const W of G){const F=xq(W.binary)?"[installed]":"[auto-install]";console.log(` ${DK(W)} ${F}${W.description?` — ${W.description}`:""}`)}console.log()}console.log(` ${X.length} built-in commands across ${Z.size} apps + ${N.size} sites, ${G.length} external CLIs`);console.log()});$.command("validate").description("Validate CLI definitions").argument("[target]","site or site/name").action(async(q)=>{const{validateClisWithTarget:K,renderValidationReport:X}=await import("./validate.js");console.log(X(K([H,z],q)))});$.command("verify").description("Validate + smoke test").argument("[target]").option("--smoke","Run smoke tests",!1).action(async(q,K)=>{const{verifyClis:X,renderVerifyReport:Q}=await import("./verify.js"),Y=await X({builtinClis:H,userClis:z,target:q,smoke:K.smoke});console.log(Q(Y));process.exitCode=Y.ok?J.SUCCESS:J.GENERIC_ERROR});const V=$.command("skills").description("Read bundled ppcli skills");V.command("list").description("List bundled ppcli-* skills").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((q)=>{const K=NQ();Fq(K,{fmt:q.format,fmtExplicit:!!q.format,columns:["name","description","version","path"],title:"ppcli/skills/list",source:"ppcli skills list"})});V.command("read").description("Print an ppcli-* skill's SKILL.md or reference file").argument("<skill>","Skill name, or skill/path like opencli-browser/references/foo.md").argument("[path]","Path under the skill directory").option("--json","Output a JSON envelope instead of raw markdown",!1).action((q,K,X)=>{let Q;try{Q=UQ(q,K??"")}catch(Y){console.error(`Error: ${h(Y)}`);if(Y instanceof uq&&Y.hint)console.error(`Hint: ${Y.hint}`);process.exitCode=Y instanceof uq?Y.exitCode:J.GENERIC_ERROR;return}if(X.json){console.log(JSON.stringify(Q,null,2));return}process.stdout.write(Q.content);if(!Q.content.endsWith(`
|
|
3
3
|
`))process.stdout.write(`
|
|
4
|
-
`)});const B=
|
|
4
|
+
`)});const B=hQ($);$.command("convention-audit").description("Scan adapters for agent-native convention violations").argument("[target]","site or site/name").option("--site <site>","Limit audit to one site").option("-f, --format <fmt>","Output format: table, json, yaml","table").option("--strict","Exit non-zero when violations are found",!1).action(async(q,K)=>{const{runConventionAudit:X,renderConventionAuditText:Q}=await import("./convention-audit.js"),Y=X({projectRoot:findPackageRoot(nq),target:q,site:K.site}),Z=String(K.format??"table").toLowerCase();if(Z==="json"||Z==="yaml"||Z==="yml")Fq(Y,{fmt:Z});else console.log(Q(Y));if(K.strict&&!Y.ok)process.exitCode=J.GENERIC_ERROR});const j=$.command("browser").addOption(new qQ("--session <name>","Internal — set automatically from the <session> positional").hideHelp()).option("--window <mode>","Browser window mode: foreground or background").description("Browser control — navigate, click, type, extract, wait (no LLM needed)").usage("<session> <command> [options]").addHelpText("after",`
|
|
5
5
|
<session> is a required positional: pass the name of the browser session every subcommand should operate on. Reuse the same name across calls to keep the tab/state alive; pick a different name to isolate parallel browser work.
|
|
6
6
|
|
|
7
7
|
Examples:
|
|
@@ -11,8 +11,8 @@ Examples:
|
|
|
11
11
|
$ ppcli browser work state
|
|
12
12
|
$ ppcli browser work bind
|
|
13
13
|
$ ppcli browser work unbind
|
|
14
|
-
`),S=j.description();async function
|
|
15
|
-
`);console.log(typeof Q==="string"?Q:JSON.stringify(Q,null,2))}));E(j.command("frames").description("List cross-origin iframe targets in snapshot order")).action(D(async(q)=>{const K=await q.frames?.()??[];console.log(JSON.stringify(K,null,2))}));E(j.command("screenshot").argument("[path]","Save to file (base64 if omitted)")).option("--full-page","Capture the full scrollable page, not just the viewport",!1).option("--annotate","Overlay visible browser state ref labels on the screenshot",!1).option("--width <n>","Override viewport width in CSS pixels for this screenshot only",(q)=>
|
|
14
|
+
`),S=j.description();async function R(q,K,X={}){const Q=await q.evaluate(DQ(K,X));if(!Q.ok)throw new BK({code:Q.code,message:Q.message,hint:Q.hint,candidates:Q.candidates,matches_n:Q.matches_n});return{matches_n:Q.matches_n,match_level:Q.match_level}}function P(q){if(q===void 0||q===null||q==="")return null;const K=String(q);if(!/^\d+$/.test(K))return{error:`--nth must be a non-negative integer, got "${K}"`};return Number.parseInt(K,10)}function r(q){console.log(JSON.stringify({error:{code:q.code,message:q.message,hint:q.hint,...q.candidates&&{candidates:q.candidates},...q.matches_n!==void 0&&{matches_n:q.matches_n}}},null,2))}function qq(q){return q.toLowerCase().includes("javascript dialog")}function o(q){console.log(JSON.stringify({error:{code:"javascript_dialog_open",message:q,hint:"Handle the modal first: ppcli browser dialog accept (or dismiss). Use --text for prompt dialogs."}},null,2))}function D(q){return async(...K)=>{let X=null;try{const Q=K.at(-1)instanceof Aq?K.at(-1):void 0,Y=UX(Q),Z=rq(Q),N=sq(Q),U=pq(Q,"foreground");X=await NX(Z,Y,N,{windowMode:U,profile:Mq(Q)});await q(X,...K)}catch(Q){if(Q instanceof LQ){n.error(Q.message);if(Q.hint)n.error(`Hint: ${Q.hint}`)}else if(Q instanceof Rq){if(qq(Q.message))o(Q.message);else if(Q.code)console.log(JSON.stringify({error:{code:Q.code,message:Q.message,...Q.hint?{hint:Q.hint}:{}}},null,2));n.error(Q.message);if(Q.hint)n.error(`Hint: ${Q.hint}`)}else if(Q instanceof BK){r(Q);n.error(`[${Q.code}] ${Q.message}`);if(Q.hint)n.error(`Hint: ${Q.hint}`)}else{const Y=h(Q);if(qq(Y)){o(Y);n.error(Y)}else if(Y.includes("attach failed")||Y.includes("chrome-extension://"))n.error("Browser attach failed — another extension may be interfering. Try disabling 1Password.");else n.error(Y)}process.exitCode=J.GENERIC_ERROR}}}j.command("bind").description("Bind the current Chrome tab/window to the browser session named by <session>").action(async(q,K)=>{const X=q instanceof Aq?q:K,Q=rq(X);try{if(mq(Mq(X)).kind==="managed")throw Error("托管环境不支持 bind:它是独立自动化窗口,直接用 browser open/eval 等命令即可。");const{BrowserBridge:Y}=await import("./browser/index.js"),Z=new Y,N=sq(X);await Z.connect({timeout:Oq,session:Q,surface:"browser",...N&&{contextId:N}});const U=await uQ(Q,{...N&&{contextId:N}});Jq(void 0,Iq(Q,N));console.log(JSON.stringify({session:Q,...U&&typeof U==="object"?U:{data:U}},null,2))}catch(Y){if(Y instanceof Rq&&Y.code)console.log(JSON.stringify({error:{code:Y.code,message:Y.message,...Y.hint?{hint:Y.hint}:{}}},null,2));n.error(Y instanceof Error?Y.message:String(Y));if(Y instanceof Rq&&Y.hint)n.error(`Hint: ${Y.hint}`);process.exitCode=J.GENERIC_ERROR}});j.command("unbind").description("Detach the bound browser session named by <session> without closing the user tab/window").action(async(q,K)=>{const X=q instanceof Aq?q:K,Q=rq(X);try{if(mq(Mq(X)).kind==="managed")throw Error("托管环境不支持 unbind:它没有绑定到扩展的会话。");const{BrowserBridge:Y}=await import("./browser/index.js"),Z=new Y,N=sq(X);await Z.connect({timeout:Oq,session:Q,surface:"browser",...N&&{contextId:N}});await mQ("close-window",{session:Q,surface:"browser",...N&&{contextId:N}});Jq(void 0,Iq(Q,N));console.log(JSON.stringify({unbound:!0,session:Q},null,2))}catch(Y){if(Y instanceof Rq&&Y.code)console.log(JSON.stringify({error:{code:Y.code,message:Y.message,...Y.hint?{hint:Y.hint}:{}}},null,2));n.error(Y instanceof Error?Y.message:String(Y));if(Y instanceof Rq&&Y.hint)n.error(`Hint: ${Y.hint}`);process.exitCode=J.GENERIC_ERROR}});const l=j.command("tab").description("Tab management — list, create, and close tabs in the browser session");l.command("list").description("List tabs in the browser session with target IDs").action(D(async(q)=>{const K=await q.tabs();console.log(JSON.stringify(K,null,2))}));l.command("new").argument("[url]","Optional URL to open in the new tab").description("Create a new tab and print its target ID").action(D(async(q,K)=>{if(!q.newTab)throw Error("This browser session does not support creating tabs");const X=await q.newTab(K);console.log(JSON.stringify({page:X,url:K??null},null,2))}));E(l.command("select").argument("[targetId]",'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"').description("Select a tab by target ID and make it the default browser tab")).action(D(async(q,K,X)=>{const Q=fK(K,X);if(!Q)throw Error("Target tab required. Pass it as an argument or --tab <targetId>.");await q.selectTab(Q);Jq(Q,Sq(q));console.log(JSON.stringify({selected:Q},null,2))}));E(l.command("close").argument("[targetId]",'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"').description("Close a tab by target ID")).action(D(async(q,K,X)=>{const Q=fK(K,X);if(!q.closeTab)throw Error("This browser session does not support closing tabs");if(!Q)throw Error("Target tab required. Pass it as an argument or --tab <targetId>.");const Y=await oq(q,Q,{scope:Sq(q),source:"explicit"});if(!Y)throw Error(`Target tab ${Q} is not part of the current browser session.`);await q.closeTab(Y);const Z=Sq(q);if(yK(Z)?.defaultPage===Y)Jq(void 0,Z);console.log(JSON.stringify({closed:Y},null,2))}));const Kq="(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body,timestamp:Date.now()};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if(ct.includes('json')||ct.includes('text')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()";E(j.command("open").argument("<url>").description("Open URL in the browser session")).action(D(async(q,K,X)=>{const Q=await q.startNetworkCapture?.()??!1;await q.goto(K);await q.wait(2);if(!Q)try{await q.evaluate(Kq)}catch{}const Y=await q.getCurrentUrl?.()??K,Z=$X(Y,Sq(q),{oncePerSession:!0});console.log(JSON.stringify({url:Y,...q.getActivePage?.()?{page:q.getActivePage?.()}:{},...Z?{sitemap:Z}:{}},null,2))}));E(j.command("back").description("Go back in browser history")).action(D(async(q,K)=>{await q.evaluate("history.back()");await q.wait(2);console.log("Navigated back")}));E(j.command("scroll").argument("<direction>","up or down").option("--amount <pixels>","Pixels to scroll","500")).description("Scroll page").action(D(async(q,K,X)=>{if(K!=="up"&&K!=="down"){console.error(`Invalid direction "${K}". Use "up" or "down".`);process.exitCode=J.USAGE_ERROR;return}await q.scroll(K,parseInt(X.amount,10));console.log(`Scrolled ${K}`)}));E(j.command("state").description("Page state: URL, title, interactive elements with [N] indices").option("--source <source>","Snapshot backend: dom (default) or ax prototype","dom").option("--compare-sources","Print DOM vs AX snapshot metrics for observation promotion decisions",!1)).action(D(async(q,K)=>{if(K.compareSources===!0){const[Z,N]=await Promise.all([vK(q,"dom"),vK(q,"ax")]);console.log(JSON.stringify({url:await q.getCurrentUrl?.()??"",sources:{dom:Z,ax:N}},null,2));return}const X=String(K.source??"dom").toLowerCase();if(X!=="dom"&&X!=="ax"){console.log(JSON.stringify({error:{code:"invalid_source",message:`--source must be "dom" or "ax", got "${K.source}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=await q.snapshot({viewportExpand:2000,source:X}),Y=await q.getCurrentUrl?.()??"";console.log(`URL: ${Y}
|
|
15
|
+
`);console.log(typeof Q==="string"?Q:JSON.stringify(Q,null,2))}));E(j.command("frames").description("List cross-origin iframe targets in snapshot order")).action(D(async(q)=>{const K=await q.frames?.()??[];console.log(JSON.stringify(K,null,2))}));E(j.command("screenshot").argument("[path]","Save to file (base64 if omitted)")).option("--full-page","Capture the full scrollable page, not just the viewport",!1).option("--annotate","Overlay visible browser state ref labels on the screenshot",!1).option("--width <n>","Override viewport width in CSS pixels for this screenshot only",(q)=>uK(q,"width")).option("--height <n>","Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)",(q)=>uK(q,"height")).description("Take screenshot").action(D(async(q,K,X)=>{const Q={fullPage:X.fullPage===!0,annotate:X.annotate===!0,width:X.width,height:X.height},Y=X.annotate===!0?(q.annotatedScreenshot??q.screenshot).bind(q):q.screenshot.bind(q);if(K){await Y({...Q,path:K});console.log(`Screenshot saved to: ${K}`)}else console.log(await Y({...Q,format:"png"}))}));const Xq=[".js-mocaptcha-img",".moclickcaptcha-img",".moclickcaptcha-draw",".geetest_item_img",".geetest_widget canvas",".captcha-image img",".captcha img",".captcha canvas",'img[src*="captcha" i]','img[class*="captcha" i]','canvas[class*="captcha" i]'],Eq=6;function Vq(q){const K=q&&q.trim()?[q.trim(),...Xq]:Xq;return`(() => {
|
|
16
16
|
var sels = ${JSON.stringify(K)};
|
|
17
17
|
var el = null, used = '';
|
|
18
18
|
for (var i = 0; i < sels.length; i++) {
|
|
@@ -46,7 +46,7 @@ Examples:
|
|
|
46
46
|
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
47
47
|
prompt: prompt,
|
|
48
48
|
};
|
|
49
|
-
})()`}function
|
|
49
|
+
})()`}function jq(q){const K=q&&q.trim()?[q.trim(),...Xq]:Xq;return`(() => {
|
|
50
50
|
var sels = ${JSON.stringify(K)};
|
|
51
51
|
var present = false;
|
|
52
52
|
for (var i = 0; i < sels.length; i++) {
|
|
@@ -69,7 +69,7 @@ Examples:
|
|
|
69
69
|
}
|
|
70
70
|
} catch (e) {}
|
|
71
71
|
return { captcha_present: present, status_text: status, url: (location.href || '').slice(0, 100) };
|
|
72
|
-
})()`}E(j.command("captcha").argument("[selector]","验证码图片元素 CSS 选择器(缺省自动探测常见点选验证码)")).option("--save <path>","把全视口截图存到该路径(缺省存到临时 PNG 文件)").option("--scale <n>","验证码区域裁剪图的放大倍数(1..6,缺省 3;放大后认扭曲字更准)","3").description("抓取点选验证码:存整图 + 只含验证码的放大裁剪图 + 返回坐标与提示文字,供 AI 视觉识别后配合 captcha-click 点击").action(D(async(q,K,X)=>{const Q=await q.evaluate(
|
|
72
|
+
})()`}E(j.command("captcha").argument("[selector]","验证码图片元素 CSS 选择器(缺省自动探测常见点选验证码)")).option("--save <path>","把全视口截图存到该路径(缺省存到临时 PNG 文件)").option("--scale <n>","验证码区域裁剪图的放大倍数(1..6,缺省 3;放大后认扭曲字更准)","3").description("抓取点选验证码:存整图 + 只含验证码的放大裁剪图 + 返回坐标与提示文字,供 AI 视觉识别后配合 captcha-click 点击").action(D(async(q,K,X)=>{const Q=await q.evaluate(Vq(typeof K==="string"?K:void 0));if(!Q||!Q.found){console.log(JSON.stringify({error:{code:"captcha_not_found",message:'未探测到点选验证码图片。可显式传选择器,如 ppcli browser <s> captcha ".js-mocaptcha-img"',tried:Xq}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=X?.save&&String(X.save)||M.join(Nq.tmpdir(),`opencli-captcha-${Date.now()}.png`);await q.screenshot({format:"png",path:Y});const Z=Q.rect,N=Q.dpr||1,U=Math.max(1,Math.min(6,Number(X?.scale??3)||3)),G=Eq;let W=null;const _=q.cdp;if(typeof _==="function"){const F={x:Math.max(0,Z.x-G),y:Math.max(0,Z.y-G),width:Z.w+G*2,height:Z.h+G*2,scale:U},k=await _.call(q,"Page.captureScreenshot",{format:"jpeg",quality:82,clip:F,captureBeyondViewport:!1});if(k&&k.data){W=Y.replace(/\.png$/i,"")+".crop.jpg";I.writeFileSync(W,Buffer.from(k.data,"base64"))}}console.log(JSON.stringify({ok:!0,selector:Q.selector,prompt:Q.prompt||"",crop:W,crop_scale:W?U:null,crop_pad:W?G:null,saved:Y,rect_css:Z,rect_screenshot_px:{x:Math.round(Z.x*N),y:Math.round(Z.y*N),w:Math.round(Z.w*N),h:Math.round(Z.h*N)},dpr:N,natural:Q.natural,viewport:Q.viewport,guide:"① read_local_file 打开 crop(只含验证码、已放大)认清每个待点字与顺序(注意会混入不在 prompt 里的干扰字,只点 prompt 要求的)。"+"② 坐标直接读成「在 crop 这张图里的比例」(0..1,左上角为 0,0)——不用管 pad/rect,看到字在 crop 的哪就填哪。"+'③ 执行:ppcli browser <session> captcha-click --crop --in "'+Q.selector+'" "x1,y1 x2,y2 ..."(--crop 表示坐标是 crop 图比例;空格分隔,顺序即点击顺序)。'+"命令会回读点了几个、验证码是否还在、URL 是否跳转,据此判断成功/补点/重来。"},null,2))}));E(j.command("captcha-click").argument("<points>",'待点击坐标,空格分隔且顺序即点击顺序,如 "0.30,0.46 0.68,0.66"')).option("--in <selector>","坐标映射基准元素(缺省自动探测验证码图片)").option("--crop","坐标按「captcha 裁剪图(crop)里的比例」解释(0..1,左上角为0,0)——AI 看哪张就按那张填,内部自动扣掉 pad。推荐").option("--px","坐标按「截图设备像素」解释(自动按 devicePixelRatio 换算成 CSS 像素)").option("--css","坐标按「CSS 视口像素」解释(直接点击,不做映射)").option("--delay <ms>","每次点击之间的间隔毫秒","250").description("按坐标依次点击(点选验证码用):把 AI 从图上读到的位置一次性点上,点完回读验证码是否还在/是否跳转。默认坐标为相对基准元素的比例(0..1),--crop 则为裁剪图比例").action(D(async(q,K,X)=>{const Y=String(K??"").trim().split(/\s+/).filter(Boolean).map((A)=>{const[b,x]=A.split(",");return{x:Number(b),y:Number(x)}});if(Y.length===0||Y.some((A)=>!Number.isFinite(A.x)||!Number.isFinite(A.y))){console.log(JSON.stringify({error:{code:"usage_error",message:'坐标格式应为 "x,y x,y ...",如 "0.30,0.46 0.68,0.66"'}},null,2));process.exitCode=J.USAGE_ERROR;return}const Z=X?.css?"css":X?.px?"px":X?.crop?"crop":"frac",N=Math.max(0,Number(X?.delay??250)||0);let U={x:0,y:0,w:0,h:0},G=1;if(Z!=="css"){const A=await q.evaluate(Vq(typeof X?.in==="string"?X.in:void 0));if(!A||!A.found||!A.rect){console.log(JSON.stringify({error:{code:"captcha_not_found",message:"captcha-click 找不到基准元素,请用 --in <selector> 指定,或用 --css 传绝对坐标"}},null,2));process.exitCode=J.USAGE_ERROR;return}U=A.rect;G=A.dpr||1}const W=Eq,_=U.w+W*2,F=U.h+W*2,k=Math.max(0,U.x-W),f=Math.max(0,U.y-W),y=(A)=>{if(Z==="css")return A;if(Z==="px")return{x:A.x/G,y:A.y/G};if(Z==="crop")return{x:k+A.x*_,y:f+A.y*F};return{x:U.x+A.x*U.w,y:U.y+A.y*U.h}},w=q.cdp,T=q.nativeClick,d=(A)=>new Promise((b)=>setTimeout(b,A));async function g(A,b,x,u){if(typeof w==="function"){const c=12+u%4;for(let a=1;a<=c;a++){const e=a/c,C=e*e*(3-2*e),i=Math.sin(e*Math.PI)*10*(u%2===0?1:-1),yq=x.x+(A-x.x)*C,tK=x.y+(b-x.y)*C+i;await w.call(q,"Input.dispatchMouseEvent",{type:"mouseMoved",x:Math.round(yq),y:Math.round(tK)});await d(8+a%3*6)}await w.call(q,"Input.dispatchMouseEvent",{type:"mouseMoved",x:A,y:b});await d(40+u%3*25);await w.call(q,"Input.dispatchMouseEvent",{type:"mousePressed",x:A,y:b,button:"left",clickCount:1});await d(45+u%4*20);await w.call(q,"Input.dispatchMouseEvent",{type:"mouseReleased",x:A,y:b,button:"left",clickCount:1});return"cdp"}if(typeof T==="function"){await T.call(q,A,b);return"native"}await q.evaluate(`(() => { var el = document.elementFromPoint(${A}, ${b}); if (!el) return false; ['mousemove','mousedown','mouseup','click'].forEach(function(t){ el.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window, clientX: ${A}, clientY: ${b} })); }); return true; })()`);return"synthetic"}const Qq=[];let L="synthetic",O={x:Math.round(U.x+U.w/2)||400,y:Math.round(U.y-40)||200};for(let A=0;A<Y.length;A++){const b=y(Y[A]),x=Math.round(b.x),u=Math.round(b.y);L=await g(x,u,O,A);O={x,y:u};Qq.push({x,y:u});if(A<Y.length-1&&N>0)await d(N)}await d(700);let s={};try{s=await q.evaluate(jq(typeof X?.in==="string"?X.in:void 0))}catch{}console.log(JSON.stringify({ok:!0,mode:Z,method:L,clicked_n:Qq.length,points_css:Qq,after:s},null,2))}));E(j.command("console")).option("--level <level>","Console level: all, error, warning, log, info, debug","all").option("--since <duration>","Only include messages from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include messages older than the duration from now").option("--follow","Continuously print new console messages as JSON lines",!1).description("Read recent browser console messages").action(D(async(q,K)=>{const X=kq(K.since,"since"),Q=kq(K.until,"until");if(X&&typeof X==="object"){console.log(JSON.stringify({error:{code:"invalid_since",message:X.error}},null,2));process.exitCode=J.USAGE_ERROR;return}if(Q&&typeof Q==="object"){console.log(JSON.stringify({error:{code:"invalid_until",message:Q.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=(U)=>U.map((G)=>{if(G&&typeof G==="object"){const W=G;return{...W,timestamp:lq(W.timestamp)}}return{type:"log",text:String(G),timestamp:Date.now()}}),Z=(U)=>iq(U,{sinceMs:X,untilMs:Q}).filter((G)=>{if(K.level==="all")return!0;const W=String(G.type??G.level??"").toLowerCase();return K.level==="error"?W==="error"||W==="warning":W===String(K.level).toLowerCase()});if(K.follow){let U=0;while(!0){const G=Z(Y(await q.consoleMessages("all"))),W=selectFreshByTimestamp(G,U);for(const _ of W.fresh)console.log(JSON.stringify({..._,timestamp:Gq(_.timestamp)}));U=W.lastSeenTs;await new Promise((_)=>setTimeout(_,MK))}}const N=Z(Y(await q.consoleMessages(K.level)));console.log(JSON.stringify({session:tq(q),captured_at:new Date().toISOString(),count:N.length,messages:N.map((U)=>({...U,timestamp:Gq(U.timestamp)}))},null,2))}));E(j.command("analyze").argument("<url>")).description("Classify site: anti-bot vendor, real-data API candidates, pattern (A/B/C/D), nearest adapter, next step").action(D(async(q,K)=>{const X=await q.startNetworkCapture?.()??!1;await q.goto(K);await q.wait(2);if(!X)try{await q.evaluate(Kq)}catch{}await Wq(q);await q.wait(1);const Y=(await Wq(q)).map((k)=>({url:k.url,status:k.status,contentType:k.ct,bodyPreview:typeof k.body==="string"?k.body.slice(0,2000):k.body?JSON.stringify(k.body).slice(0,2000):null})),Z=`(function(){
|
|
73
73
|
return {
|
|
74
74
|
cookieNames: (document.cookie || '').split(';').map(function(c){ return c.trim().split('=')[0]; }).filter(Boolean),
|
|
75
75
|
initialState: {
|
|
@@ -81,7 +81,7 @@ Examples:
|
|
|
81
81
|
title: document.title || '',
|
|
82
82
|
finalUrl: location.href,
|
|
83
83
|
};
|
|
84
|
-
})()`,
|
|
84
|
+
})()`,N=await q.evaluate(Z),U=(await q.getCookies({url:N.finalUrl||K}).catch(()=>[])).map((k)=>k.name).filter(Boolean),G=[...new Set([...N.cookieNames,...U])],W={requestedUrl:K,finalUrl:N.finalUrl,cookieNames:G,networkEntries:Y,initialState:N.initialState,title:N.title},_=wQ(W,Bq()),F=resolveSitemapAvailabilityForUrl(N.finalUrl||K);console.log(JSON.stringify({..._,...F?{sitemap:F}:{}},null,2))}));const m=(q)=>q.option("--role <role>","Semantic role (button, link, textbox, option, etc.)").option("--name <text>","Accessible name contains text (aria-label, label, title, placeholder, or visible text)").option("--label <text>","Associated label contains text").option("--text <text>","Visible text contains text").option("--testid <id>","data-testid / data-test / test-id contains id"),eq=(q,K)=>q.option(`--${K}-role <role>`,`${K} semantic role`).option(`--${K}-name <text>`,`${K} accessible name contains text`).option(`--${K}-label <text>`,`${K} associated label contains text`).option(`--${K}-text <text>`,`${K} visible text contains text`).option(`--${K}-testid <id>`,`${K} data-testid / data-test / test-id contains id`),Lq=(q)=>{const K={};for(const X of["role","name","label","text","testid"]){const Q=q[X];if(typeof Q==="string"&&Q.trim())K[X]=Q.trim()}return Object.keys(K).length>0?K:null},cK=(q,K)=>{const X={},Q={role:`${K}Role`,name:`${K}Name`,label:`${K}Label`,text:`${K}Text`,testid:`${K}Testid`};for(const Y of["role","name","label","text","testid"]){const Z=q[Q[Y]];if(typeof Z==="string"&&Z.trim())X[Y]=Z.trim()}return Object.keys(X).length>0?X:null},qK=async(q,K,X)=>{const Q=await q.evaluate(FK({...K,limit:6}));if(RK(Q))return Q;if(X==="write"&&Q.matches_n!==1)return{error:{code:"semantic_ambiguous",message:`Semantic locator matched ${Q.matches_n} elements; write actions require a unique target.`,hint:"Add --name/--label/--text/--testid or use browser find with a narrower locator.",matches_n:Q.matches_n,entries:Q.entries}};const Y=Q.entries[0];if(!Y)return{error:{code:"semantic_not_found",message:"Semantic locator matched 0 elements",hint:"Try browser state, --source ax, or relax the semantic locator."}};const Z=String(Y.ref);if(X==="read")return{target:Z,...Q.matches_n>1?{total_matches:Q.matches_n}:{}};return Z},mK=async(q,K,X)=>{const Q=Lq(K);if(!Q)return null;return qK(q,Q,X)},bq=async(q,K,X,Q)=>{const Y=typeof K==="string"&&K.trim()?K.trim():"",Z=!!Lq(X);if(Y&&Z)return{error:{code:"usage_error",message:"Pass either <target> or semantic locator flags, not both."}};if(Y)return Y;const N=await mK(q,X,Q);if(N)return N;return{error:{code:"usage_error",message:"Missing target. Pass a numeric ref/CSS selector, or semantic flags like --role button --name Submit."}}},Zq=(q)=>{console.log(JSON.stringify(q,null,2));process.exitCode=J.USAGE_ERROR},$q=async(q,K,X)=>{const Q=await bq(q,K,X,"write");if(typeof Q==="string")return Q;if("error"in Q)Zq(Q);return null},Cq=async(q,K,X,Q,Y)=>{const Z=!!Lq(Q);if(Z&&X!==void 0){Zq({error:{code:"usage_error",message:`When using semantic locator flags, pass only <${Y}> as the positional argument.`}});return null}const N=Z?K:X;if(N===void 0){Zq({error:{code:"usage_error",message:`Missing ${Y}.`,hint:Z?`With semantic locator flags, pass the ${Y} as the only positional argument.`:`Pass both a target and ${Y}.`}});return null}const U=await $q(q,Z?void 0:K,Q);if(!U)return null;return{target:U,value:String(N)}},KK=async(q,K,X,Q,Y)=>{const Z=typeof K==="string"&&K.trim()?K.trim():"",N=cK(X,Q);if(Z&&N){Zq({error:{code:"usage_error",message:`Pass either <${Y}> or --${Q}-* semantic locator flags, not both.`}});return null}if(Z)return Z;if(N){const U=await qK(q,N,"write");if(typeof U==="string")return U;if("error"in U)Zq(U);return null}Zq({error:{code:"usage_error",message:`Missing ${Y}. Pass a numeric ref/CSS selector, or --${Q}-role/--${Q}-name semantic flags.`}});return null};E(m(j.command("find")).option("--css <selector>","CSS selector (required)").option("--limit <n>","Max entries returned","50").option("--text-max <n>","Max chars of trimmed text per entry","120").description("Find DOM elements by CSS or semantic locator — returns JSON {matches_n, entries[]}")).action(D(async(q,K)=>{const X=Lq(K);if((!K.css||typeof K.css!=="string")&&!X){console.log(JSON.stringify({error:{code:"usage_error",message:"--css <selector> or a semantic locator flag is required",hint:'Examples: ppcli browser find --css ".btn.primary"; ppcli browser find --role button --name Save'}},null,2));process.exitCode=J.USAGE_ERROR;return}if(K.css&&X){console.log(JSON.stringify({error:{code:"usage_error",message:"Pass either --css or semantic locator flags, not both."}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=P(K.limit);if(Q&&typeof Q==="object"&&"error"in Q){console.log(JSON.stringify({error:{code:"usage_error",message:Q.error.replace("--nth","--limit")}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=P(K.textMax);if(Y&&typeof Y==="object"&&"error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error.replace("--nth","--text-max")}},null,2));process.exitCode=J.USAGE_ERROR;return}const Z=await q.evaluate(X?FK({...X,limit:Q??void 0,textMax:Y??void 0}):PQ(K.css,{limit:Q??void 0,textMax:Y??void 0}));if(RK(Z)){console.log(JSON.stringify(Z,null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify(Z,null,2))}));const Hq=j.command("get").description("Get page properties");E(Hq.command("title").description("Page title")).action(D(async(q)=>{console.log(await q.evaluate("document.title"))}));E(Hq.command("url").description("Current page URL")).action(D(async(q)=>{console.log(await q.getCurrentUrl?.()??await q.evaluate("location.href"))}));const Tq=async(q,K,X,Q,Y)=>{const Z=await bq(q,K,X,"read");if(typeof Z!=="string"&&"error"in Z){console.log(JSON.stringify(Z,null,2));process.exitCode=J.USAGE_ERROR;return}const N=typeof Z==="string"?Z:Z.target,U=typeof Z==="string"?void 0:Z.total_matches,G=P(X.nth);if(G&&typeof G==="object"&&"error"in G){console.log(JSON.stringify({error:{code:"usage_error",message:G.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:W,match_level:_}=await R(q,N,{firstOnMulti:G===null,...typeof G==="number"?{nth:G}:{}}),F=await q.evaluate(Q);let k;if(Y==="attributes")try{k=F==null?{}:JSON.parse(String(F))}catch{k=F}else k=F??null;console.log(JSON.stringify({value:k,matches_n:W,match_level:_,...U&&U>1?{total_matches:U}:{}},null,2))};E(m(Hq.command("text")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element text content — JSON envelope {value, matches_n}")).action(D(async(q,K,X)=>Tq(q,K,X??{},AQ(),"text")));E(m(Hq.command("value")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Input/textarea value — JSON envelope {value, matches_n}")).action(D(async(q,K,X)=>Tq(q,K,X??{},BQ(),"value")));E(Hq.command("html").option("--selector <css>","CSS selector scope (first match)").option("--as <format>",'Output format: "html" (default) or "json" for structured tree',"html").option("--max <n>","Max characters of raw HTML to return (0 = unlimited)","0").option("--depth <n>","(--as json) Max tree depth below root (0 = root only, 0 disables = unlimited via empty)","").option("--children-max <n>","(--as json) Max element children kept per node (empty = unlimited)","").option("--text-max <n>","(--as json) Max chars of direct text kept per node (empty = unlimited)","").description("Page HTML (or scoped); use --as json for a {tag, attrs, text, children} tree")).action(D(async(q,K)=>{const X=String(K.as||"html").toLowerCase();if(X!=="html"&&X!=="json"){console.log(JSON.stringify({error:{code:"invalid_format",message:`--as must be "html" or "json", got "${K.as}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=String(K.max??"0");if(!/^\d+$/.test(Q)){console.log(JSON.stringify({error:{code:"invalid_max",message:`--max must be a non-negative integer, got "${K.max}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=Number.parseInt(Q,10);if(X==="json"){const G=(w,T)=>{const d=T===void 0||T===null?"":String(T);if(d==="")return null;if(!/^\d+$/.test(d))return{error:`${w} must be a non-negative integer, got "${d}"`};return Number.parseInt(d,10)},W=G("--depth",K.depth),_=G("--children-max",K.childrenMax),F=G("--text-max",K.textMax);for(const w of[W,_,F])if(w&&typeof w==="object"&&"error"in w){console.log(JSON.stringify({error:{code:"invalid_budget",message:w.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const k=bQ({selector:K.selector??null,depth:W,childrenMax:_,textMax:F}),f=await q.evaluate(k);if(f&&typeof f==="object"&&"invalidSelector"in f&&f.invalidSelector){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${K.selector}" is not a valid CSS selector: ${f.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}const y=f;if(!y||y.matched===0){console.log(JSON.stringify({error:{code:"selector_not_found",message:K.selector?`Selector "${K.selector}" matched 0 elements.`:"Page has no documentElement."}},null,2));process.exitCode=J.USAGE_ERROR;return}console.log(JSON.stringify(y,null,2));return}const Z=K.selector?JSON.stringify(K.selector):"null",N=await q.evaluate(`(() => {
|
|
85
85
|
const s = ${Z};
|
|
86
86
|
if (s) {
|
|
87
87
|
try {
|
|
@@ -92,8 +92,8 @@ Examples:
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
return { kind: 'ok', html: document.documentElement ? document.documentElement.outerHTML : null };
|
|
95
|
-
})()`);if(
|
|
96
|
-
${U.slice(0,Y)}`);return}console.log(U)}));E(m($q.command("attributes")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element attributes — JSON envelope {value, matches_n}")).action(D(async(q,K,X)=>Iq(q,K,X??{},AQ(),"attributes")));function s(q){const K=P(q);if(K&&typeof K==="object"&&"error"in K)return K;if(typeof K==="number")return{opts:{nth:K}};return{opts:{}}}function cK(q){const K=Array.isArray(q)?q:[];if(K.length===0)return{error:{code:"usage_error",message:"At least one file path is required.",hint:'Example: ppcli browser upload "input[type=file]" ./receipt.pdf'}};const X=[];for(const Q of K){const Y=String(Q),Z=Y==="~"||Y.startsWith(`~${M.sep}`)?M.join(zq.homedir(),Y.slice(2)):Y,z=M.resolve(Z);if(!I.existsSync(z))return{error:{code:"file_not_found",message:`File not found: ${z}`}};if(!I.statSync(z).isFile())return{error:{code:"not_a_file",message:`Not a regular file: ${z}`}};X.push(z)}return{files:X}}function tq(q,K){const X=P(q);if(X&&typeof X==="object"&&"error"in X)return{error:X.error.replace("--nth",K)};if(typeof X==="number")return{opts:{nth:X}};return{opts:{}}}E(m(j.command("click")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element — JSON envelope {clicked, target, matches_n}")).action(D(async(q,K,X)=>{const Q=await Oq(q,K,X??{},"write");if(typeof Q!=="string"){console.log(JSON.stringify(Q,null,2));process.exitCode=J.USAGE_ERROR;return}const Y=s(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:z}=await q.click(Q,Y.opts);console.log(JSON.stringify({clicked:!0,target:Q,matches_n:Z,match_level:z},null,2))}));E(m(j.command("type")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to type").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}")).action(D(async(q,K,X,Q)=>{const Y=await kq(q,K,X,Q??{},"text");if(!Y)return;const Z=s(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}await q.click(Y.target,Z.opts);await q.wait(0.3);const{matches_n:z,match_level:U}=await q.typeText(Y.target,Y.value,Z.opts),G=await q.evaluate(FQ());if(G)await q.wait(0.4);console.log(JSON.stringify({typed:!0,text:Y.value,target:Y.target,matches_n:z,match_level:U,autocomplete:!!G},null,2))}));E(m(j.command("hover")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Move the mouse over an element — JSON envelope {hovered, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.hover!=="function")throw Error("browser hover is not supported by this browser backend");const Q=await Zq(q,K,X??{});if(!Q)return;const Y=s(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:z}=await q.hover(Q,Y.opts);console.log(JSON.stringify({hovered:!0,target:Q,matches_n:Z,match_level:z},null,2))}));E(m(j.command("focus")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Focus an element — JSON envelope {focused, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.focus!=="function")throw Error("browser focus is not supported by this browser backend");const Q=await Zq(q,K,X??{});if(!Q)return;const Y=s(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{focused:Z,matches_n:z,match_level:U}=await q.focus(Q,Y.opts);console.log(JSON.stringify({focused:Z,target:Q,matches_n:z,match_level:U},null,2))}));E(m(j.command("dblclick")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Double-click element — JSON envelope {dblclicked, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.dblClick!=="function")throw Error("browser dblclick is not supported by this browser backend");const Q=await Zq(q,K,X??{});if(!Q)return;const Y=s(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:z}=await q.dblClick(Q,Y.opts);console.log(JSON.stringify({dblclicked:!0,target:Q,matches_n:Z,match_level:z},null,2))}));const eq=async(q,K,X,Q)=>{if(typeof q.setChecked!=="function")throw Error(`browser ${Q?"check":"uncheck"} is not supported by this browser backend`);const Y=await Zq(q,K,X);if(!Y)return;const Z=s(X?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const z=await q.setChecked(Y,Q,Z.opts);console.log(JSON.stringify({checked:z.checked,changed:z.changed,target:Y,matches_n:z.matches_n,match_level:z.match_level,...z.kind?{kind:z.kind}:{}},null,2))};E(m(j.command("check")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await eq(q,K,X??{},!0)}));E(m(j.command("uncheck")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await eq(q,K,X??{},!1)}));E(m(j.command("upload")).argument("[targetOrFile]","Numeric ref/CSS target, or first file when using --role/--name/etc.").argument("[files...]","Local file path(s) to attach").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.uploadFiles!=="function")throw Error("browser upload is not supported by this browser backend");const Y=!!Vq(Q??{}),z=await Zq(q,Y?void 0:K,Q??{});if(!z)return;const U=s(Q?.nth);if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=Y?[K,...Array.isArray(X)?X:[]].filter((R)=>R!==void 0):X,W=cK(G);if("error"in W){console.log(JSON.stringify({error:W.error},null,2));process.exitCode=J.USAGE_ERROR;return}const _=await q.uploadFiles(z,W.files,U.opts);console.log(JSON.stringify(_,null,2))}));E(aq(aq(j.command("drag"),"from"),"to").argument("[source]","Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.").argument("[target]","Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.").option("--from-nth <n>","When <source> is a multi-match CSS selector, pick the nth match (0-based)").option("--to-nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.drag!=="function")throw Error("browser drag is not supported by this browser backend");const Y=await sq(q,K,Q??{},"from","source");if(!Y)return;const Z=await sq(q,X,Q??{},"to","target");if(!Z)return;const z=tq(Q?.fromNth,"--from-nth");if("error"in z){console.log(JSON.stringify({error:{code:"usage_error",message:z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const U=tq(Q?.toNth,"--to-nth");if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=await q.drag(Y,Z,{from:z.opts,to:U.opts});console.log(JSON.stringify(G,null,2))}));E(m(j.command("fill")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to set exactly").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}")).action(D(async(q,K,X,Q)=>{const Y=await kq(q,K,X,Q??{},"text");if(!Y)return;const Z=s(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const z=await q.fillText(Y.target,Y.value,Z.opts);if(!z.verified)process.exitCode=J.GENERIC_ERROR;console.log(JSON.stringify({filled:z.filled,verified:z.verified,target:Y.target,text:Y.value,actual:z.actual,length:z.length,matches_n:z.matches_n,match_level:z.match_level,...z.mode?{mode:z.mode}:{}},null,2))}));E(m(j.command("select")).argument("[targetOrOption]","Numeric ref/CSS target, or option text when using --role/--name/etc.").argument("[option]","Option text (or value) to select").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Select dropdown option — JSON envelope {selected, target, matches_n}")).action(D(async(q,K,X,Q)=>{const Y=await kq(q,K,X,Q??{},"option");if(!Y)return;const Z=s(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:z,match_level:U}=await F(q,Y.target,Z.opts),G=await q.evaluate(BQ(Y.value));if(G?.error){console.log(JSON.stringify({error:{code:G.error==="Not a <select>"?"not_a_select":"option_not_found",message:G.error,...G.available&&{available:G.available},matches_n:z}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({selected:G?.selected??Y.value,target:Y.target,matches_n:z,match_level:U},null,2))}));E(j.command("keys").argument("<key>","Key to press (Enter, Escape, Tab, Control+a)")).description("Press keyboard key").action(D(async(q,K)=>{await q.pressKey(K);console.log(`Pressed: ${K}`)}));const qK=j.command("dialog").description("Handle a blocking JavaScript alert/confirm/prompt dialog");E(qK.command("accept").option("--text <text>","Prompt text to submit for prompt() dialogs").description("Accept the currently open JavaScript dialog")).action(D(async(q,K)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!0,K?.text)}catch(X){if(h(X).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw X}console.log(JSON.stringify({handled:!0,action:"accept",...K?.text!==void 0&&{text:K.text}},null,2))}));E(qK.command("dismiss").description("Dismiss the currently open JavaScript dialog")).action(D(async(q)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!1)}catch(K){if(h(K).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw K}console.log(JSON.stringify({handled:!0,action:"dismiss"},null,2))}));E(j.command("wait")).argument("<type>","selector, text, time, xhr, or download").argument("[value]","CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern").option("--timeout <ms>","Timeout in milliseconds","10000").description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)').action(D(async(q,K,X,Q)=>{const Y=parseInt(Q.timeout,10);if(K==="time"){const Z=parseFloat(X??"2");await q.wait(Z);console.log(`Waited ${Z}s`)}else if(K==="selector"){if(!X){console.error("Missing CSS selector");process.exitCode=J.USAGE_ERROR;return}await q.wait({selector:X,timeout:Y/1000});console.log(`Element "${X}" appeared`)}else if(K==="text"){if(!X){console.error("Missing text");process.exitCode=J.USAGE_ERROR;return}await q.wait({text:X,timeout:Y/1000});console.log(`Text "${X}" appeared`)}else if(K==="xhr"){if(!X){console.error("Missing XHR URL regex");process.exitCode=J.USAGE_ERROR;return}let Z;try{Z=new RegExp(X)}catch(_){console.error(`Invalid regex "${X}": ${_ instanceof Error?_.message:String(_)}`);process.exitCode=J.USAGE_ERROR;return}if(!(await q.startNetworkCapture?.()??!1))try{await q.evaluate(Xq)}catch{}await Gq(q);const U=Date.now()+Y,G=400;let W=null;while(Date.now()<U&&!W){W=(await Gq(q)).find((R)=>Z.test(R.url))??null;if(!W)await new Promise((R)=>setTimeout(R,G))}if(!W){console.log(JSON.stringify({error:{code:"xhr_not_seen",message:`No captured XHR matched /${X}/ within ${Y}ms`,hint:"Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled."}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({matched:{url:W.url,status:W.status,contentType:W.ct}},null,2))}else if(K==="download"){if(typeof q.waitForDownload!=="function"){console.log(JSON.stringify({error:{code:"download_wait_unavailable",message:"The active browser backend does not support download lifecycle waits.",hint:"Use the Browser Bridge extension version 1.0.8 or newer, then retry the command."}},null,2));process.exitCode=J.GENERIC_ERROR;return}const Z=await q.waitForDownload(String(X??""),Y);if(!Z.downloaded){const z=Z.state==="interrupted"&&Z.id!==void 0?"download_failed":"download_not_seen";console.log(JSON.stringify({error:{code:z,message:Z.error??`No download matched "${X??"*"}" within ${Y}ms`,hint:"Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly."},download:Z},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify(Z,null,2))}else{console.error(`Unknown wait type "${K}". Use: selector, text, time, xhr, or download`);process.exitCode=J.USAGE_ERROR}}));E(j.command("eval").argument("<js>","JavaScript code").option("--frame <index>",'Cross-origin iframe index from "browser frames"').description("Execute JS in page context, return result")).action(D(async(q,K,X)=>{let Q;if(X.frame!==void 0){const Y=Number.parseInt(X.frame,10);if(!Number.isInteger(Y)||Y<0){console.error(`Invalid frame index "${X.frame}". Use a 0-based index from "browser frames".`);process.exitCode=J.USAGE_ERROR;return}if(!q.evaluateInFrame)throw Error("This browser session does not support frame-targeted evaluation");Q=await q.evaluateInFrame(K,Y)}else Q=await q.evaluate(K);if(typeof Q==="string")console.log(Q);else console.log(JSON.stringify(Q,null,2))}));E(j.command("extract").option("--selector <css>","CSS selector scope; defaults to <main>/<article>/<body>").option("--chunk-size <chars>","Target chunk size in chars","20000").option("--start <char>","Start offset (use next_start_char from a previous extract)","0").description("Extract page content as markdown, paragraph-aware chunks for long pages")).action(D(async(q,K)=>{const X=String(K.chunkSize??"20000");if(!/^\d+$/.test(X)||Number.parseInt(X,10)<=0){console.log(JSON.stringify({error:{code:"invalid_chunk_size",message:`--chunk-size must be a positive integer, got "${K.chunkSize}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=String(K.start??"0");if(!/^\d+$/.test(Q)){console.log(JSON.stringify({error:{code:"invalid_start",message:`--start must be a non-negative integer, got "${K.start}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=Number.parseInt(X,10),Z=Number.parseInt(Q,10),z=typeof K.selector==="string"&&K.selector.length>0?K.selector:null,U=SQ(z),G=await q.evaluate(U);if(!G){console.log(JSON.stringify({error:{code:"extract_failed",message:"Page returned no root element."}},null,2));process.exitCode=J.USAGE_ERROR;return}if("invalidSelector"in G){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${z}" is not a valid CSS selector: ${G.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}if("notFound"in G){console.log(JSON.stringify({error:{code:"selector_not_found",message:z?`Selector "${z}" matched 0 elements.`:"Page has no body/main/article element."}},null,2));process.exitCode=J.USAGE_ERROR;return}const W=bQ({html:G.html,url:G.url,title:G.title,selector:z,start:Z,chunkSize:Y});console.log(JSON.stringify(W,null,2))}));E(j.command("network")).option("--detail <key>","Emit full body for the entry with this key").option("--all","Include static resources (js/css/images/telemetry)").option("--raw","Emit full bodies for every entry (skip shape preview)").option("--filter <fields>","Comma-separated field names; keep only entries whose body shape has ALL names as path segments").option("--since <duration>","Only include entries from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include entries older than the duration from now").option("--follow","Continuously print new matching entries as JSON lines",!1).option("--failed","Only include failed HTTP requests (status 0 or >= 400)",!1).option("--max-body <chars>","With --detail: cap the emitted body at N chars (0 = unlimited, default)","0").option("--ttl <ms>","Cache TTL in ms for --detail lookups",String(FK)).description("Capture network requests as shape previews; retrieve full bodies by key").action(D(async(q,K)=>{const X=yK(K.ttl,"ttl",FK),Q=iq(q),Y=typeof K.detail==="string"&&K.detail.length>0,Z=typeof K.filter==="string",z=Rq(K.since,"since"),U=Rq(K.until,"until");if(z&&typeof z==="object"){i("invalid_since",z.error);return}if(U&&typeof U==="object"){i("invalid_until",U.error);return}if(Y&&Z){i("invalid_args","--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).");return}let G=null;if(Z){const L=kQ(K.filter);if("reason"in L){i("invalid_filter",L.reason);return}G=L.fields}if(Y&&K.follow){i("invalid_args","--follow cannot be used with --detail.");return}if(Y){const L=PQ(Q,{ttlMs:X});if(L.status==="missing"){i("cache_missing",`No cached capture. Run "browser network" first (in session "${Q}").`);return}if(L.status==="expired"){i("cache_expired",`Cache is stale (age ${L.ageMs}ms > ttl ${X}ms). Re-run "browser network" to refresh.`);return}if(L.status==="corrupt"||!L.file){i("cache_corrupt",'Cache file is malformed; re-run "browser network" to regenerate.');return}const O=EQ(L.file,K.detail);if(!O){i("key_not_found",`Key "${K.detail}" not in cache.`,{available_keys:L.file.entries.map((p)=>p.key)});return}const a=String(K.maxBody??"0");if(!/^\d+$/.test(a)){i("invalid_max_body",`--max-body must be a non-negative integer, got "${K.maxBody}"`);return}const A=Number.parseInt(a,10);let b=O.body,x=!1;if(A>0&&typeof O.body==="string"&&O.body.length>A){b=O.body.slice(0,A);x=!0}const u=O.body_truncated===!0,c={key:O.key,url:O.url,method:O.method,status:O.status,ct:O.ct,size:O.size,...typeof O.timestamp==="number"?{timestamp:Uq(O.timestamp)}:{},shape:AK(O.body),body:b};if(u||x){c.body_truncated=!0;c.body_full_size=O.body_full_size??O.size;c.body_truncation_reason=u?"capture-limit":"max-body"}console.log(JSON.stringify(c,null,2));return}if(K.follow){if(!await q.startNetworkCapture?.())try{await q.evaluate(Xq)}catch{}while(!0){const L=await Gq(q).catch((A)=>{i("capture_failed",`Could not read network capture: ${A.message}`);return[]});let O=K.all?L:OK(L);O=mq(O,{sinceMs:z,untilMs:U});if(K.failed)O=O.filter((A)=>A.status===0||A.status>=400);const a=BK(O);for(const A of a)console.log(JSON.stringify({key:A.key,timestamp:Uq(A.timestamp),method:A.method,status:A.status,url:A.url,ct:A.ct,size:A.size,...A.bodyTruncated?{body_truncated:!0}:{}}));await new Promise((A)=>setTimeout(A,PK))}}let W;try{W=await Gq(q)}catch(L){i("capture_failed",`Could not read network capture: ${L.message}`);return}let _=K.all?W:OK(W);_=mq(_,{sinceMs:z,untilMs:U});if(K.failed)_=_.filter((L)=>L.status===0||L.status>=400);const R=W.length-_.length,f=BK(_).map((L)=>({key:L.key,url:L.url,method:L.method,status:L.status,size:L.size,ct:L.ct,body:L.body,...typeof L.timestamp==="number"?{timestamp:L.timestamp}:{},...L.bodyTruncated?{body_truncated:!0}:{},...L.bodyTruncated&&typeof L.bodyFullSize==="number"?{body_full_size:L.bodyFullSize}:{}}));let y=null;try{OQ(Q,f)}catch(L){y=`Could not persist capture cache: ${L.message}. --detail lookups may miss this capture.`}const w=f.map((L)=>({entry:L,shape:AK(L.body)})),T=G?w.filter((L)=>IQ(L.shape,G)):w,d=G?w.length-T.length:0,g={session:Q,captured_at:new Date().toISOString(),count:T.length,filtered_out:R};if(G){g.filter=G;g.filter_dropped=d}if(y)g.cache_warning=y;const qq=T.filter((L)=>L.entry.body_truncated).length;if(qq>0){g.body_truncated_count=qq;g.body_truncated_hint="Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix."}if(K.raw)g.entries=T.map((L)=>({...L.entry,...typeof L.entry.timestamp==="number"?{timestamp:Uq(L.entry.timestamp)}:{}}));else{g.entries=T.map((L)=>({key:L.entry.key,method:L.entry.method,...typeof L.entry.timestamp==="number"?{timestamp:Uq(L.entry.timestamp)}:{},status:L.entry.status,url:L.entry.url,ct:L.entry.ct,size:L.entry.size,shape:L.shape,...L.entry.body_truncated?{body_truncated:!0}:{}}));g.detail_hint='Run "browser network --detail <key>" for full body.'}console.log(JSON.stringify(g,null,2))}));j.command("init").argument("<name>","Adapter name in site/command format (e.g. hn/top)").description("Generate adapter scaffold in ~/.ppcli/clis/").action(async(q)=>{try{const K=q.split("/");if(K.length!==2||!K[0]||!K[1]){console.error("Name must be site/command format (e.g. hn/top)");process.exitCode=J.USAGE_ERROR;return}const[X,Q]=K;if(!/^[a-zA-Z0-9_-]+$/.test(X)||!/^[a-zA-Z0-9_-]+$/.test(Q)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=J.USAGE_ERROR;return}const Y=await import("node:os"),Z=await import("node:fs"),z=await import("node:path"),U=z.join(Y.homedir(),".ppcli","clis",X),G=z.join(U,`${Q}.js`);if(Z.existsSync(G)){console.log(`Adapter already exists: ${G}`);return}const _=`import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
95
|
+
})()`);if(N.kind==="invalid_selector"){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${K.selector}" is not a valid CSS selector: ${N.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}const U=N.html;if(U===null){if(K.selector){console.log(JSON.stringify({error:{code:"selector_not_found",message:`Selector "${K.selector}" matched 0 elements.`}},null,2));process.exitCode=J.USAGE_ERROR;return}console.log("(empty)");return}if(Y>0&&U.length>Y){console.log(`<!-- ppcli: truncated ${Y} of ${U.length} chars; re-run without --max (or --max 0) for full -->
|
|
96
|
+
${U.slice(0,Y)}`);return}console.log(U)}));E(m(Hq.command("attributes")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","Pick the nth match (0-based) when <target> is a multi-match CSS selector").description("Element attributes — JSON envelope {value, matches_n}")).action(D(async(q,K,X)=>Tq(q,K,X??{},FQ(),"attributes")));function t(q){const K=P(q);if(K&&typeof K==="object"&&"error"in K)return K;if(typeof K==="number")return{opts:{nth:K}};return{opts:{}}}function dK(q){const K=Array.isArray(q)?q:[];if(K.length===0)return{error:{code:"usage_error",message:"At least one file path is required.",hint:'Example: ppcli browser upload "input[type=file]" ./receipt.pdf'}};const X=[];for(const Q of K){const Y=String(Q),Z=Y==="~"||Y.startsWith(`~${M.sep}`)?M.join(Nq.homedir(),Y.slice(2)):Y,N=M.resolve(Z);if(!I.existsSync(N))return{error:{code:"file_not_found",message:`File not found: ${N}`}};if(!I.statSync(N).isFile())return{error:{code:"not_a_file",message:`Not a regular file: ${N}`}};X.push(N)}return{files:X}}function QK(q,K){const X=P(q);if(X&&typeof X==="object"&&"error"in X)return{error:X.error.replace("--nth",K)};if(typeof X==="number")return{opts:{nth:X}};return{opts:{}}}E(m(j.command("click")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element — JSON envelope {clicked, target, matches_n}")).action(D(async(q,K,X)=>{const Q=await bq(q,K,X??{},"write");if(typeof Q!=="string"){console.log(JSON.stringify(Q,null,2));process.exitCode=J.USAGE_ERROR;return}const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.click(Q,Y.opts);console.log(JSON.stringify({clicked:!0,target:Q,matches_n:Z,match_level:N},null,2))}));E(m(j.command("type")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to type").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"text");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}await q.click(Y.target,Z.opts);await q.wait(0.3);const{matches_n:N,match_level:U}=await q.typeText(Y.target,Y.value,Z.opts),G=await q.evaluate(EQ());if(G)await q.wait(0.4);console.log(JSON.stringify({typed:!0,text:Y.value,target:Y.target,matches_n:N,match_level:U,autocomplete:!!G},null,2))}));E(m(j.command("hover")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Move the mouse over an element — JSON envelope {hovered, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.hover!=="function")throw Error("browser hover is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.hover(Q,Y.opts);console.log(JSON.stringify({hovered:!0,target:Q,matches_n:Z,match_level:N},null,2))}));E(m(j.command("focus")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Focus an element — JSON envelope {focused, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.focus!=="function")throw Error("browser focus is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{focused:Z,matches_n:N,match_level:U}=await q.focus(Q,Y.opts);console.log(JSON.stringify({focused:Z,target:Q,matches_n:N,match_level:U},null,2))}));E(m(j.command("dblclick")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Double-click element — JSON envelope {dblclicked, target, matches_n}")).action(D(async(q,K,X)=>{if(typeof q.dblClick!=="function")throw Error("browser dblclick is not supported by this browser backend");const Q=await $q(q,K,X??{});if(!Q)return;const Y=t(X?.nth);if("error"in Y){console.log(JSON.stringify({error:{code:"usage_error",message:Y.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:Z,match_level:N}=await q.dblClick(Q,Y.opts);console.log(JSON.stringify({dblclicked:!0,target:Q,matches_n:Z,match_level:N},null,2))}));const XK=async(q,K,X,Q)=>{if(typeof q.setChecked!=="function")throw Error(`browser ${Q?"check":"uncheck"} is not supported by this browser backend`);const Y=await $q(q,K,X);if(!Y)return;const Z=t(X?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const N=await q.setChecked(Y,Q,Z.opts);console.log(JSON.stringify({checked:N.checked,changed:N.changed,target:Y,matches_n:N.matches_n,match_level:N.match_level,...N.kind?{kind:N.kind}:{}},null,2))};E(m(j.command("check")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await XK(q,K,X??{},!0)}));E(m(j.command("uncheck")).argument("[target]","Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}")).action(D(async(q,K,X)=>{await XK(q,K,X??{},!1)}));E(m(j.command("upload")).argument("[targetOrFile]","Numeric ref/CSS target, or first file when using --role/--name/etc.").argument("[files...]","Local file path(s) to attach").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.uploadFiles!=="function")throw Error("browser upload is not supported by this browser backend");const Y=!!Lq(Q??{}),N=await $q(q,Y?void 0:K,Q??{});if(!N)return;const U=t(Q?.nth);if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=Y?[K,...Array.isArray(X)?X:[]].filter((F)=>F!==void 0):X,W=dK(G);if("error"in W){console.log(JSON.stringify({error:W.error},null,2));process.exitCode=J.USAGE_ERROR;return}const _=await q.uploadFiles(N,W.files,U.opts);console.log(JSON.stringify(_,null,2))}));E(eq(eq(j.command("drag"),"from"),"to").argument("[source]","Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.").argument("[target]","Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.").option("--from-nth <n>","When <source> is a multi-match CSS selector, pick the nth match (0-based)").option("--to-nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}")).action(D(async(q,K,X,Q)=>{if(typeof q.drag!=="function")throw Error("browser drag is not supported by this browser backend");const Y=await KK(q,K,Q??{},"from","source");if(!Y)return;const Z=await KK(q,X,Q??{},"to","target");if(!Z)return;const N=QK(Q?.fromNth,"--from-nth");if("error"in N){console.log(JSON.stringify({error:{code:"usage_error",message:N.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const U=QK(Q?.toNth,"--to-nth");if("error"in U){console.log(JSON.stringify({error:{code:"usage_error",message:U.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const G=await q.drag(Y,Z,{from:N.opts,to:U.opts});console.log(JSON.stringify(G,null,2))}));E(m(j.command("fill")).argument("[targetOrText]","Numeric ref/CSS target, or text when using --role/--name/etc.").argument("[text]","Text to set exactly").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"text");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const N=await q.fillText(Y.target,Y.value,Z.opts);if(!N.verified)process.exitCode=J.GENERIC_ERROR;console.log(JSON.stringify({filled:N.filled,verified:N.verified,target:Y.target,text:Y.value,actual:N.actual,length:N.length,matches_n:N.matches_n,match_level:N.match_level,...N.mode?{mode:N.mode}:{}},null,2))}));E(m(j.command("select")).argument("[targetOrOption]","Numeric ref/CSS target, or option text when using --role/--name/etc.").argument("[option]","Option text (or value) to select").option("--nth <n>","When <target> is a multi-match CSS selector, pick the nth match (0-based)").description("Select dropdown option — JSON envelope {selected, target, matches_n}")).action(D(async(q,K,X,Q)=>{const Y=await Cq(q,K,X,Q??{},"option");if(!Y)return;const Z=t(Q?.nth);if("error"in Z){console.log(JSON.stringify({error:{code:"usage_error",message:Z.error}},null,2));process.exitCode=J.USAGE_ERROR;return}const{matches_n:N,match_level:U}=await R(q,Y.target,Z.opts),G=await q.evaluate(RQ(Y.value));if(G?.error){console.log(JSON.stringify({error:{code:G.error==="Not a <select>"?"not_a_select":"option_not_found",message:G.error,...G.available&&{available:G.available},matches_n:N}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({selected:G?.selected??Y.value,target:Y.target,matches_n:N,match_level:U},null,2))}));E(j.command("keys").argument("<key>","Key to press (Enter, Escape, Tab, Control+a)")).description("Press keyboard key").action(D(async(q,K)=>{await q.pressKey(K);console.log(`Pressed: ${K}`)}));const YK=j.command("dialog").description("Handle a blocking JavaScript alert/confirm/prompt dialog");E(YK.command("accept").option("--text <text>","Prompt text to submit for prompt() dialogs").description("Accept the currently open JavaScript dialog")).action(D(async(q,K)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!0,K?.text)}catch(X){if(h(X).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw X}console.log(JSON.stringify({handled:!0,action:"accept",...K?.text!==void 0&&{text:K.text}},null,2))}));E(YK.command("dismiss").description("Dismiss the currently open JavaScript dialog")).action(D(async(q)=>{if(!q.handleJavaScriptDialog)throw Error("This browser session does not support JavaScript dialog handling");try{await q.handleJavaScriptDialog(!1)}catch(K){if(h(K).toLowerCase().includes("no dialog")){console.log(JSON.stringify({error:{code:"no_javascript_dialog",message:"No JavaScript dialog is currently open."}},null,2));process.exitCode=J.USAGE_ERROR;return}throw K}console.log(JSON.stringify({handled:!0,action:"dismiss"},null,2))}));E(j.command("wait")).argument("<type>","selector, text, time, xhr, or download").argument("[value]","CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern").option("--timeout <ms>","Timeout in milliseconds","10000").description('Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)').action(D(async(q,K,X,Q)=>{const Y=parseInt(Q.timeout,10);if(K==="time"){const Z=parseFloat(X??"2");await q.wait(Z);console.log(`Waited ${Z}s`)}else if(K==="selector"){if(!X){console.error("Missing CSS selector");process.exitCode=J.USAGE_ERROR;return}await q.wait({selector:X,timeout:Y/1000});console.log(`Element "${X}" appeared`)}else if(K==="text"){if(!X){console.error("Missing text");process.exitCode=J.USAGE_ERROR;return}await q.wait({text:X,timeout:Y/1000});console.log(`Text "${X}" appeared`)}else if(K==="xhr"){if(!X){console.error("Missing XHR URL regex");process.exitCode=J.USAGE_ERROR;return}let Z;try{Z=new RegExp(X)}catch(_){console.error(`Invalid regex "${X}": ${_ instanceof Error?_.message:String(_)}`);process.exitCode=J.USAGE_ERROR;return}if(!(await q.startNetworkCapture?.()??!1))try{await q.evaluate(Kq)}catch{}await Wq(q);const U=Date.now()+Y,G=400;let W=null;while(Date.now()<U&&!W){W=(await Wq(q)).find((F)=>Z.test(F.url))??null;if(!W)await new Promise((F)=>setTimeout(F,G))}if(!W){console.log(JSON.stringify({error:{code:"xhr_not_seen",message:`No captured XHR matched /${X}/ within ${Y}ms`,hint:"Check the pattern against `browser network` output; the endpoint may not have fired yet, or capture is disabled."}},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify({matched:{url:W.url,status:W.status,contentType:W.ct}},null,2))}else if(K==="download"){if(typeof q.waitForDownload!=="function"){console.log(JSON.stringify({error:{code:"download_wait_unavailable",message:"The active browser backend does not support download lifecycle waits.",hint:"Use the Browser Bridge extension version 1.0.8 or newer, then retry the command."}},null,2));process.exitCode=J.GENERIC_ERROR;return}const Z=await q.waitForDownload(String(X??""),Y);if(!Z.downloaded){const N=Z.state==="interrupted"&&Z.id!==void 0?"download_failed":"download_not_seen";console.log(JSON.stringify({error:{code:N,message:Z.error??`No download matched "${X??"*"}" within ${Y}ms`,hint:"Check the pattern against the expected filename or URL; use a longer --timeout if the download starts slowly."},download:Z},null,2));process.exitCode=J.GENERIC_ERROR;return}console.log(JSON.stringify(Z,null,2))}else{console.error(`Unknown wait type "${K}". Use: selector, text, time, xhr, or download`);process.exitCode=J.USAGE_ERROR}}));E(j.command("eval").argument("<js>","JavaScript code").option("--frame <index>",'Cross-origin iframe index from "browser frames"').description("Execute JS in page context, return result")).action(D(async(q,K,X)=>{let Q;if(X.frame!==void 0){const Y=Number.parseInt(X.frame,10);if(!Number.isInteger(Y)||Y<0){console.error(`Invalid frame index "${X.frame}". Use a 0-based index from "browser frames".`);process.exitCode=J.USAGE_ERROR;return}if(!q.evaluateInFrame)throw Error("This browser session does not support frame-targeted evaluation");Q=await q.evaluateInFrame(K,Y)}else Q=await q.evaluate(K);if(typeof Q==="string")console.log(Q);else console.log(JSON.stringify(Q,null,2))}));E(j.command("extract").option("--selector <css>","CSS selector scope; defaults to <main>/<article>/<body>").option("--chunk-size <chars>","Target chunk size in chars","20000").option("--start <char>","Start offset (use next_start_char from a previous extract)","0").description("Extract page content as markdown, paragraph-aware chunks for long pages")).action(D(async(q,K)=>{const X=String(K.chunkSize??"20000");if(!/^\d+$/.test(X)||Number.parseInt(X,10)<=0){console.log(JSON.stringify({error:{code:"invalid_chunk_size",message:`--chunk-size must be a positive integer, got "${K.chunkSize}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Q=String(K.start??"0");if(!/^\d+$/.test(Q)){console.log(JSON.stringify({error:{code:"invalid_start",message:`--start must be a non-negative integer, got "${K.start}"`}},null,2));process.exitCode=J.USAGE_ERROR;return}const Y=Number.parseInt(X,10),Z=Number.parseInt(Q,10),N=typeof K.selector==="string"&&K.selector.length>0?K.selector:null,U=CQ(N),G=await q.evaluate(U);if(!G){console.log(JSON.stringify({error:{code:"extract_failed",message:"Page returned no root element."}},null,2));process.exitCode=J.USAGE_ERROR;return}if("invalidSelector"in G){console.log(JSON.stringify({error:{code:"invalid_selector",message:`Selector "${N}" is not a valid CSS selector: ${G.reason}`}},null,2));process.exitCode=J.USAGE_ERROR;return}if("notFound"in G){console.log(JSON.stringify({error:{code:"selector_not_found",message:N?`Selector "${N}" matched 0 elements.`:"Page has no body/main/article element."}},null,2));process.exitCode=J.USAGE_ERROR;return}const W=TQ({html:G.html,url:G.url,title:G.title,selector:N,start:Z,chunkSize:Y});console.log(JSON.stringify(W,null,2))}));E(j.command("network")).option("--detail <key>","Emit full body for the entry with this key").option("--all","Include static resources (js/css/images/telemetry)").option("--raw","Emit full bodies for every entry (skip shape preview)").option("--filter <fields>","Comma-separated field names; keep only entries whose body shape has ALL names as path segments").option("--since <duration>","Only include entries from the last duration (for example: 30s, 2m)").option("--until <duration>","Only include entries older than the duration from now").option("--follow","Continuously print new matching entries as JSON lines",!1).option("--failed","Only include failed HTTP requests (status 0 or >= 400)",!1).option("--max-body <chars>","With --detail: cap the emitted body at N chars (0 = unlimited, default)","0").option("--ttl <ms>","Cache TTL in ms for --detail lookups",String(OK)).description("Capture network requests as shape previews; retrieve full bodies by key").action(D(async(q,K)=>{const X=xK(K.ttl,"ttl",OK),Q=tq(q),Y=typeof K.detail==="string"&&K.detail.length>0,Z=typeof K.filter==="string",N=kq(K.since,"since"),U=kq(K.until,"until");if(N&&typeof N==="object"){p("invalid_since",N.error);return}if(U&&typeof U==="object"){p("invalid_until",U.error);return}if(Y&&Z){p("invalid_args","--filter and --detail cannot be used together (one narrows a list, the other fetches a specific entry).");return}let G=null;if(Z){const L=MQ(K.filter);if("reason"in L){p("invalid_filter",L.reason);return}G=L.fields}if(Y&&K.follow){p("invalid_args","--follow cannot be used with --detail.");return}if(Y){const L=kQ(Q,{ttlMs:X});if(L.status==="missing"){p("cache_missing",`No cached capture. Run "browser network" first (in session "${Q}").`);return}if(L.status==="expired"){p("cache_expired",`Cache is stale (age ${L.ageMs}ms > ttl ${X}ms). Re-run "browser network" to refresh.`);return}if(L.status==="corrupt"||!L.file){p("cache_corrupt",'Cache file is malformed; re-run "browser network" to regenerate.');return}const O=OQ(L.file,K.detail);if(!O){p("key_not_found",`Key "${K.detail}" not in cache.`,{available_keys:L.file.entries.map((a)=>a.key)});return}const s=String(K.maxBody??"0");if(!/^\d+$/.test(s)){p("invalid_max_body",`--max-body must be a non-negative integer, got "${K.maxBody}"`);return}const A=Number.parseInt(s,10);let b=O.body,x=!1;if(A>0&&typeof O.body==="string"&&O.body.length>A){b=O.body.slice(0,A);x=!0}const u=O.body_truncated===!0,c={key:O.key,url:O.url,method:O.method,status:O.status,ct:O.ct,size:O.size,...typeof O.timestamp==="number"?{timestamp:Gq(O.timestamp)}:{},shape:EK(O.body),body:b};if(u||x){c.body_truncated=!0;c.body_full_size=O.body_full_size??O.size;c.body_truncation_reason=u?"capture-limit":"max-body"}console.log(JSON.stringify(c,null,2));return}if(K.follow){if(!await q.startNetworkCapture?.())try{await q.evaluate(Kq)}catch{}while(!0){const L=await Wq(q).catch((A)=>{p("capture_failed",`Could not read network capture: ${A.message}`);return[]});let O=K.all?L:SK(L);O=iq(O,{sinceMs:N,untilMs:U});if(K.failed)O=O.filter((A)=>A.status===0||A.status>=400);const s=PK(O);for(const A of s)console.log(JSON.stringify({key:A.key,timestamp:Gq(A.timestamp),method:A.method,status:A.status,url:A.url,ct:A.ct,size:A.size,...A.bodyTruncated?{body_truncated:!0}:{}}));await new Promise((A)=>setTimeout(A,MK))}}let W;try{W=await Wq(q)}catch(L){p("capture_failed",`Could not read network capture: ${L.message}`);return}let _=K.all?W:SK(W);_=iq(_,{sinceMs:N,untilMs:U});if(K.failed)_=_.filter((L)=>L.status===0||L.status>=400);const F=W.length-_.length,f=PK(_).map((L)=>({key:L.key,url:L.url,method:L.method,status:L.status,size:L.size,ct:L.ct,body:L.body,...typeof L.timestamp==="number"?{timestamp:L.timestamp}:{},...L.bodyTruncated?{body_truncated:!0}:{},...L.bodyTruncated&&typeof L.bodyFullSize==="number"?{body_full_size:L.bodyFullSize}:{}}));let y=null;try{IQ(Q,f)}catch(L){y=`Could not persist capture cache: ${L.message}. --detail lookups may miss this capture.`}const w=f.map((L)=>({entry:L,shape:EK(L.body)})),T=G?w.filter((L)=>SQ(L.shape,G)):w,d=G?w.length-T.length:0,g={session:Q,captured_at:new Date().toISOString(),count:T.length,filtered_out:F};if(G){g.filter=G;g.filter_dropped=d}if(y)g.cache_warning=y;const Qq=T.filter((L)=>L.entry.body_truncated).length;if(Qq>0){g.body_truncated_count=Qq;g.body_truncated_hint="Some bodies exceeded the capture limit; their `shape` reflects only the captured prefix."}if(K.raw)g.entries=T.map((L)=>({...L.entry,...typeof L.entry.timestamp==="number"?{timestamp:Gq(L.entry.timestamp)}:{}}));else{g.entries=T.map((L)=>({key:L.entry.key,method:L.entry.method,...typeof L.entry.timestamp==="number"?{timestamp:Gq(L.entry.timestamp)}:{},status:L.entry.status,url:L.entry.url,ct:L.entry.ct,size:L.entry.size,shape:L.shape,...L.entry.body_truncated?{body_truncated:!0}:{}}));g.detail_hint='Run "browser network --detail <key>" for full body.'}console.log(JSON.stringify(g,null,2))}));j.command("init").argument("<name>","Adapter name in site/command format (e.g. hn/top)").description("Generate adapter scaffold in ~/.ppcli/clis/").action(async(q)=>{try{const K=q.split("/");if(K.length!==2||!K[0]||!K[1]){console.error("Name must be site/command format (e.g. hn/top)");process.exitCode=J.USAGE_ERROR;return}const[X,Q]=K;if(!/^[a-zA-Z0-9_-]+$/.test(X)||!/^[a-zA-Z0-9_-]+$/.test(Q)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=J.USAGE_ERROR;return}const Y=await import("node:os"),Z=await import("node:fs"),N=await import("node:path"),U=N.join(Y.homedir(),".ppcli","clis",X),G=N.join(U,`${Q}.js`);if(Z.existsSync(G)){console.log(`Adapter already exists: ${G}`);return}const _=`import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
97
97
|
|
|
98
98
|
cli({
|
|
99
99
|
site: '${X}',
|
|
@@ -115,18 +115,18 @@ cli({
|
|
|
115
115
|
return [];
|
|
116
116
|
},
|
|
117
117
|
});
|
|
118
|
-
`;Z.mkdirSync(U,{recursive:!0});Z.writeFileSync(G,_,"utf-8");console.log(`Created: ${G}`);console.log("First time on this site? Run: ppcli browser analyze <url>");console.log(`Edit the file to implement your adapter, then run: ppcli browser verify ${q}`)}catch(K){console.error(`Error: ${K instanceof Error?K.message:String(K)}`);process.exitCode=J.GENERIC_ERROR}});j.command("verify").argument("<name>","Adapter name in site/command format (e.g. hn/top)").option("--write-fixture","Write a starter fixture to ~/.ppcli/sites/<site>/verify/<command>.json if none exists").option("--update-fixture","Overwrite an existing fixture with one derived from current output").option("--no-fixture","Ignore any fixture file for this run (no value-level validation)").option("--strict-memory","Fail (not just warn) when ~/.ppcli/sites/<site>/endpoints.json or notes.md is missing").option("--seed-args <value>","Seed args when no fixture exists; use JSON array/object for multiple args or flags").option("--trace <mode>","Trace capture for the adapter subprocess: off, on, retain-on-failure","off").description("Execute an adapter and validate output; uses fixture at ~/.ppcli/sites/<site>/verify/<cmd>.json when present").action(async(q,K={})=>{try{const X=q.split("/");if(X.length!==2){console.error("Name must be site/command format");process.exitCode=J.USAGE_ERROR;return}const[Q,Y]=X;if(!/^[a-zA-Z0-9_-]+$/.test(Q)||!/^[a-zA-Z0-9_-]+$/.test(Y)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=J.USAGE_ERROR;return}const{execFileSync:Z}=await import("node:child_process"),{loadFixture:
|
|
119
|
-
`);console.log(` Loading: ${y}`);let T=K.fixture!==!1?
|
|
120
|
-
`);const
|
|
118
|
+
`;Z.mkdirSync(U,{recursive:!0});Z.writeFileSync(G,_,"utf-8");console.log(`Created: ${G}`);console.log("First time on this site? Run: ppcli browser analyze <url>");console.log(`Edit the file to implement your adapter, then run: ppcli browser verify ${q}`)}catch(K){console.error(`Error: ${K instanceof Error?K.message:String(K)}`);process.exitCode=J.GENERIC_ERROR}});j.command("verify").argument("<name>","Adapter name in site/command format (e.g. hn/top)").option("--write-fixture","Write a starter fixture to ~/.ppcli/sites/<site>/verify/<command>.json if none exists").option("--update-fixture","Overwrite an existing fixture with one derived from current output").option("--no-fixture","Ignore any fixture file for this run (no value-level validation)").option("--strict-memory","Fail (not just warn) when ~/.ppcli/sites/<site>/endpoints.json or notes.md is missing").option("--seed-args <value>","Seed args when no fixture exists; use JSON array/object for multiple args or flags").option("--trace <mode>","Trace capture for the adapter subprocess: off, on, retain-on-failure","off").description("Execute an adapter and validate output; uses fixture at ~/.ppcli/sites/<site>/verify/<cmd>.json when present").action(async(q,K={})=>{try{const X=q.split("/");if(X.length!==2){console.error("Name must be site/command format");process.exitCode=J.USAGE_ERROR;return}const[Q,Y]=X;if(!/^[a-zA-Z0-9_-]+$/.test(Q)||!/^[a-zA-Z0-9_-]+$/.test(Y)){console.error("Name parts must be alphanumeric/dash/underscore only");process.exitCode=J.USAGE_ERROR;return}const{execFileSync:Z}=await import("node:child_process"),{loadFixture:N,writeFixture:U,deriveFixture:G,validateRows:W,validateRowShape:_,fixturePath:F,expandFixtureArgs:k,parseSeedArgs:f}=await import("./browser/verify-fixture.js"),y=M.join(Nq.homedir(),".ppcli","clis",Q,`${Y}.js`);if(!I.existsSync(y)){console.error(`Adapter not found: ${y}`);console.error(`Run "ppcli browser init ${q}" to create it.`);process.exitCode=J.GENERIC_ERROR;return}console.log(`\uD83D\uDD0D Verifying ${q}...
|
|
119
|
+
`);console.log(` Loading: ${y}`);let T=K.fixture!==!1?N(Q,Y):null;const d=I.readFileSync(y,"utf-8"),g=/['"]limit['"]/.test(d),Qq=f(K.seedArgs),L=T?.args??Qq,O=k(L);if(L===void 0&&O.length===0&&g)O.push("--limit","3");const s=K.trace&&K.trace!=="off"?["--trace",K.trace]:[],A=[...O,...s].join(" "),b=resolveBrowserVerifyInvocation(),x=[...b.args,Q,Y,...O,...s,"--format","json"];let u;try{u=Z(b.binary,x,{cwd:b.cwd,timeout:30000,encoding:"utf-8",env:process.env,stdio:["pipe","pipe","pipe"],...b.shell?{shell:!0}:{}})}catch(C){console.log(` Executing: ppcli ${Q} ${Y} ${A}
|
|
120
|
+
`);const i=C;if(i.stdout)console.log(String(i.stdout));if(i.stderr)console.error(String(i.stderr).slice(0,500));console.log(`
|
|
121
121
|
✗ Adapter failed. Fix the code and try again.`);process.exitCode=J.GENERIC_ERROR;return}console.log(` Executing: ppcli ${Q} ${Y} ${A}
|
|
122
122
|
`);let c;try{c=normalizeVerifyRows(JSON.parse(u))}catch{console.log(u);console.log("\n ✗ Could not parse adapter output as JSON. Is `--format json` broken?");process.exitCode=J.GENERIC_ERROR;return}console.log(renderVerifyPreview(c));console.log(`
|
|
123
|
-
→ ${c.length} row${c.length===1?"":"s"}`);const
|
|
124
|
-
✗ Adapter output violates row shape conventions:`);for(const C of
|
|
123
|
+
→ ${c.length} row${c.length===1?"":"s"}`);const a=_(c);if(a.length>0){console.log(`
|
|
124
|
+
✗ Adapter output violates row shape conventions:`);for(const C of a.slice(0,20)){const i=C.rowIndex!==void 0?`row[${C.rowIndex}] `:"";console.log(` - [${C.rule}] ${i}${C.detail}`)}if(a.length>20)console.log(` ... and ${a.length-20} more failure(s)`);console.log(`
|
|
125
125
|
Keep rows agent-native: <=12 top-level keys, nesting depth <=1, and id-shaped fields at top level.`);process.exitCode=J.GENERIC_ERROR;return}if(K.writeFixture||K.updateFixture)if(T&&!K.updateFixture){console.log(`
|
|
126
|
-
Fixture already exists at ${
|
|
127
|
-
${T?"↻ Updated":"✎ Wrote"} fixture: ${
|
|
128
|
-
✓ Adapter runs. (No fixture at ${
|
|
129
|
-
✓ Adapter matches fixture (${
|
|
130
|
-
✗ Adapter output does not match fixture:`);for(const C of
|
|
131
|
-
`);for(const
|
|
132
|
-
Official baseline: ${Z.length} sites in package`)}catch{console.log("No local adapter overrides. All sites use the official baseline.")}});jq.command("eject").description("Copy an official adapter to ~/.ppcli/clis/ for local editing").argument("<site>","Site name (e.g. twitter, bilibili)").action(async(q)=>{const K=await import("node:os"),X=M.join(K.homedir(),".ppcli","clis"),Q=M.join(N,q),Y=M.join(X,q);try{await I.promises.access(Q)}catch{console.error(`Error: Site "${q}" not found in official adapters.`);process.exitCode=J.USAGE_ERROR;return}try{await I.promises.access(Y);console.error(`Site "${q}" already exists in ~/.ppcli/clis/. Use "ppcli adapter reset ${q}" first to restore official version.`);process.exitCode=J.USAGE_ERROR;return}catch{}I.cpSync(Q,Y,{recursive:!0});console.log(`✅ Ejected "${q}" to ~/.ppcli/clis/${q}/`);console.log("You can now edit the adapter files. Changes take effect immediately.");console.log("Note: Official updates to this adapter will overwrite your changes.")});jq.command("reset").description("Remove local override and restore official adapter version").argument("[site]","Site name (e.g. twitter, bilibili)").option("--all","Reset all local overrides").action(async(q,K)=>{const X=await import("node:os"),Q=M.join(X.homedir(),".ppcli","clis");if(K.all){try{const U=(await I.promises.readdir(Q,{withFileTypes:!0})).filter((G)=>G.isDirectory());if(U.length===0){console.log("No local sites to reset.");return}for(const G of U)I.rmSync(M.join(Q,G.name),{recursive:!0,force:!0});console.log(`✅ Reset ${U.length} site(s). All adapters now use official baseline.`)}catch{console.log("No local sites to reset.")}return}if(!q){console.error("Error: Please specify a site name or use --all.");process.exitCode=J.USAGE_ERROR;return}const Y=M.join(Q,q);try{await I.promises.access(Y)}catch{console.error(`Site "${q}" has no local override.`);return}const Z=I.existsSync(M.join(N,q));I.rmSync(Y,{recursive:!0,force:!0});console.log(Z?`✅ Reset "${q}". Now using official baseline.`:`✅ Removed custom site "${q}".`)});const v=$.command("profile").description("Manage Browser Bridge Chrome profiles"),gK=v.description();v.command("list").description("List Chrome profiles: extension-connected and managed (dedicated Chrome instances)").option("-f, --format <fmt>","Output format: text, json","text").action(async(q)=>{const K=await xQ(),X=cQ(),Q=K?.profiles??[],Y=!!K&&!sQ(K,Fq)&&Array.isArray(K.profiles),Z=await Promise.all(yq().map(async({name:G,entry:W})=>({name:G,port:W.port,userDataDir:W.userDataDir,running:await lQ(W),isDefault:X.defaultContextId===G,createdAt:W.createdAt??"",label:W.label??"",proxy:W.proxy?{scheme:W.proxy.scheme,host:W.proxy.host,port:W.proxy.port,hasAuth:!!W.proxy.username,username:W.proxy.username??""}:null,fingerprint:W.fingerprint?.enabled?{enabled:!0,timezone:W.fingerprint.timezone??"",locale:W.fingerprint.locale??"",hardwareConcurrency:W.fingerprint.hardwareConcurrency??0,deviceMemory:W.fingerprint.deviceMemory??0,screen:W.fingerprint.screen??null,webglVendor:W.fingerprint.webglVendor??"",webglRenderer:W.fingerprint.webglRenderer??"",canvasNoise:W.fingerprint.canvasNoise===!0}:{enabled:!1},webrtc:W.webrtc!==!1,clean:W.clean!==!1})));if(q.format==="json"){const G=Y?Q.map((W)=>({contextId:W.contextId,alias:RK(X,W.contextId)??"",connected:!0,version:W.extensionVersion??"",isDefault:X.defaultContextId===W.contextId})):[];console.log(JSON.stringify({managed:Z,extension:G,daemonRunning:!!K,daemonStale:!!K&&!Y,defaultProfile:X.defaultContextId??""},null,2));return}if(Z.length>0){console.log("Managed profiles (dedicated Chrome instances)");console.log();for(const G of Z){const W=G.isDefault?" default":"";console.log(` ${G.name}${W} — ${G.running?`running on port ${G.port}`:"stopped"}`)}console.log()}if(!K){console.log("Daemon is not running. Run ppcli doctor after opening Chrome.");return}if(!Y){console.log(`Daemon ${rQ(K)} is stale for CLI v${Fq}.`);console.log("Run: ppcli daemon restart");return}if(Q.length===0){console.log("No Browser Bridge profiles connected.");console.log("Open a Chrome profile with the ppcli extension installed, then run ppcli profile list again.");return}const z=new Set(Q.map((G)=>G.contextId));console.log("Connected Browser Bridge profiles");console.log();for(const G of Q){const W=RK(X,G.contextId),_=X.defaultContextId===G.contextId?" default":"",R=W?` ${W}`:"",k=G.extensionVersion?` v${G.extensionVersion}`:" version unknown";console.log(` ${G.contextId}${R}${_} — connected${k}`)}const U=Object.entries(X.aliases).filter(([,G])=>!z.has(G));if(U.length>0||X.defaultContextId&&!z.has(X.defaultContextId)){console.log();console.log("Disconnected saved profiles:");const G=new Set;for(const[W,_]of U){G.add(_);console.log(` ${_} ${W} — not connected`)}if(X.defaultContextId&&!G.has(X.defaultContextId)&&!z.has(X.defaultContextId))console.log(` ${X.defaultContextId} — default, not connected`)}});const KK=(q)=>{if(q.config){const Q=JSON.parse(q.config);if(typeof Q.proxy==="string")Q.proxy=fq(Q.proxy);return Q}const K={};if(q.label)K.label=q.label;if(q.proxy)K.proxy=fq(q.proxy);if(q.webrtc===!1)K.webrtc=!1;if(q.clean===!1)K.clean=!1;const X=q.fingerprint;if(X&&X!=="off")K.fingerprint={enabled:!0,spoof:X!=="lite",...q.fpTimezone?{timezone:q.fpTimezone}:{},...q.fpLocale?{locale:q.fpLocale}:{},...q.fpUa?{userAgent:q.fpUa}:{},...q.fpPlatform?{platform:q.fpPlatform}:{},...q.fpSeed?{seed:Number(q.fpSeed)}:{}};return K},QK=(q)=>q?{scheme:q.scheme,host:q.host,port:q.port,hasAuth:!!q.username,username:q.username??""}:null,XK=(q)=>q?.enabled?{enabled:!0,seed:q.seed,timezone:q.timezone??"",locale:q.locale??"",userAgent:q.userAgent??"",platform:q.platform??"",hardwareConcurrency:q.hardwareConcurrency??0,deviceMemory:q.deviceMemory??0,screen:q.screen??null,webglVendor:q.webglVendor??"",webglRenderer:q.webglRenderer??"",canvasNoise:q.canvasNoise===!0}:{enabled:!1},YK=(q)=>q.option("--label <text>","Display label / note for the environment").option("--proxy <url>","Per-env proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("--fingerprint <mode>","Fingerprint mode: off | on (spoof soft signals) | lite (timezone/locale follow only)").option("--fp-timezone <iana>","Fingerprint timezone, e.g. America/New_York (must be set explicitly)").option("--fp-locale <locale>","Fingerprint locale, e.g. en-US (must be set explicitly)").option("--fp-ua <ua>","Override navigator.userAgent (use with care)").option("--fp-platform <p>","Override navigator.platform, e.g. Win32").option("--fp-seed <n>","Fixed fingerprint seed (for reproducible fingerprint)").option("--no-webrtc","Disable WebRTC leak protection (on by default)").option("--no-clean","Disable lean launch flags / telemetry suppression (on by default)").option("--config <json>","Full config as JSON: { label, proxy, fingerprint } — overrides granular flags");v.command("create").description("Create a managed profile: a dedicated Chrome instance with its own user data dir (for a second account)").argument("<name>","Profile name, e.g. work / brand2").option("--browser-path <path>","Explicit browser executable (defaults to auto-discovered Chrome)").option("--no-start","Create only; do not launch Chrome now").option("-f, --format <fmt>","Output format: text, json","text").action(async(q,K)=>{try{const X=KK(K),{opts:Q}=await EK({...X,browserPath:K.browserPath}),Y=await nQ(q,{...Q,browserPath:K.browserPath});if(K.start!==!1)await vq(q,Y);if(K.format==="json"){console.log(JSON.stringify({ok:!0,name:q,port:Y.port,userDataDir:Y.userDataDir,label:Y.label??"",proxy:QK(Y.proxy),fingerprint:XK(Y.fingerprint),webrtc:Y.webrtc!==!1,clean:Y.clean!==!1,started:K.start!==!1},null,2));return}console.log(`✅ 托管 profile "${q}" 已创建(端口 ${Y.port})`);console.log(` 数据目录:${Y.userDataDir}`);if(Y.proxy)console.log(` 代理:${Y.proxy.scheme}://${Y.proxy.host}:${Y.proxy.port}`);if(Y.fingerprint?.enabled)console.log(` 指纹:已开启${Y.fingerprint.timezone?` 时区 ${Y.fingerprint.timezone}`:""}${Y.fingerprint.locale?` 语言 ${Y.fingerprint.locale}`:""}`);if(K.start!==!1)console.log(` Chrome 已启动。用 ppcli <site> login --profile ${q} 在新窗口里登录账号。`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});YK(v.commands[v.commands.length-1]);v.command("start").description("Launch the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(q)=>{try{const K=yq().find((Q)=>Q.name===q.trim());if(!K)throw Error(`托管 profile "${q}" 不存在,先 ppcli profile create ${q}`);const X=await vq(K.name,K.entry);console.log(`✅ profile "${q}" 运行中(${X})`)}catch(K){console.error(`Error: ${h(K)}`);process.exitCode=J.USAGE_ERROR}});v.command("stop").description("Gracefully close the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(q)=>{try{const K=yq().find((Q)=>Q.name===q.trim());if(!K)throw Error(`托管 profile "${q}" 不存在`);const X=await oQ(K.entry);console.log(X?`✅ profile "${q}" 已关闭`:`profile "${q}" 本来就没在运行`)}catch(K){console.error(`Error: ${h(K)}`);process.exitCode=J.USAGE_ERROR}});v.command("remove").description("Remove a managed profile (closes Chrome and deletes its login data)").argument("<name>","Managed profile name").option("--keep-data","Keep the user data dir on disk",!1).action(async(q,K)=>{try{await iQ(q,{keepData:K.keepData===!0});console.log(`✅ 托管 profile "${q}" 已删除${K.keepData?"(保留数据目录)":"(含登录数据)"}`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});v.command("update").description("Reconfigure a managed profile (label / proxy / fingerprint); takes effect on next start").argument("<name>","Managed profile name").option("--clear-proxy","Remove the proxy from this environment").option("--restart","Relaunch the environment after updating",!1).option("-f, --format <fmt>","Output format: text, json","text").action(async(q,K)=>{try{const X=KK(K),{opts:Q}=await EK(X),Y={};if(X.label!==void 0)Y.label=X.label??null;if(K.clearProxy)Y.proxy=null;else if(Q.proxy)Y.proxy=Q.proxy;if(K.fingerprint==="off")Y.fingerprint=null;else if(Q.fingerprint)Y.fingerprint=Q.fingerprint;if(X.webrtc!==void 0)Y.webrtc=X.webrtc;if(X.clean!==void 0)Y.clean=X.clean;const{entry:Z,wasRunning:z}=await pQ(q,Y);if(K.restart&&z)await vq(q,Z);if(K.format==="json"){console.log(JSON.stringify({ok:!0,name:q,label:Z.label??"",proxy:QK(Z.proxy),fingerprint:XK(Z.fingerprint),webrtc:Z.webrtc!==!1,clean:Z.clean!==!1,wasRunning:z,restarted:!!(K.restart&&z)},null,2));return}console.log(`✅ 托管 profile "${q}" 配置已更新${z?"(已关闭,重启后生效)":""}`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});YK(v.commands[v.commands.length-1]);v.command("test-proxy").description("Test proxy connectivity and detect exit IP / country / timezone").argument("<url>","Proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("-f, --format <fmt>","Output format: text, json","text").action(async(q,K)=>{try{const X=fq(q),Q=await aQ(X);if(K.format==="json"){console.log(JSON.stringify(Q,null,2));return}if(Q.ok){console.log(`✅ 代理可用 — 出口 IP ${Q.ip}`);console.log(` 位置:${Q.country??"?"}${Q.city?" / "+Q.city:""}`);console.log(` 时区:${Q.timezone??"?"} 语言:${Q.locale??"?"}`)}else{console.log(`❌ 代理不可用:${Q.error}`);process.exitCode=J.USAGE_ERROR}}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});v.command("rename").description("Assign a local alias to a connected Browser Bridge profile").argument("<contextId>","Profile contextId from ppcli profile list").argument("<alias>","Local alias, e.g. work or personal").action((q,K)=>{try{mQ(q,K);console.log(`Profile ${q} is now aliased as ${K}.`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});v.command("use").description("Set the default Browser Bridge profile for future commands").argument("<profile>","Profile alias or contextId").action((q)=>{try{const K=gQ(q);console.log(`Default Browser Bridge profile: ${K.defaultContextId??q}`)}catch(K){console.error(`Error: ${h(K)}`);process.exitCode=J.USAGE_ERROR}});const Hq=$.command("daemon").description("Manage the ppcli daemon"),nK=Hq.description();Hq.command("status").description("Show daemon status").action(async()=>{await hQ()});Hq.command("stop").description("Stop the daemon").action(async()=>{await yQ()});Hq.command("restart").description("Restart the daemon").action(async()=>{await wQ()});Hq.command("warm").description("Pre-open the background automation window (keeps a tab alive for reuse)").option("--url <url>","http(s) info page to keep in the warm tab (falls back to about:blank)").action(async(q)=>{await vQ(q.url)});const Lq=Tq(),Mq=$.command("external").description("Manage external CLI passthrough commands");Mq.command("install").description("Install an external CLI").argument("<name>","Name of the external CLI").action((q)=>{const K=Lq.find((X)=>X.name===q);if(!K){console.error(`External CLI '${q}' not found in registry.`);process.exitCode=J.USAGE_ERROR;return}ZQ(K)});Mq.command("register").description("Register an external CLI").argument("<name>","Name of the CLI").option("--binary <bin>","Binary name if different from name").option("--install <cmd>","Auto-install command").option("--desc <text>","Description").action((q,K)=>{$Q(q,{binary:K.binary,install:K.install,description:K.desc})});Mq.command("list").description("List registered external CLIs").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((q)=>{const K=Tq().map((X)=>({name:X.name,package:X.package??"",binary:X.binary,installed:wq(X.binary),description:X.description??"",homepage:X.homepage??"",tags:X.tags?.join(", ")??""}));Aq(K,{fmt:q.format,columns:["name","package","binary","installed","description","homepage","tags"],title:"ppcli/external/list",source:"ppcli external list"})});function lK(q,K){const X=K??(()=>{const Q=process.argv.indexOf(q);return process.argv.slice(Q+1)})();try{YQ(q,X,Lq)}catch(Q){if(Q instanceof hq){process.stderr.write(eK.dump(jQ(Q),{sortKeys:!1,lineWidth:120,noRefs:!0}));process.exitCode=Q.exitCode}else{console.error(`Error: ${h(Q)}`);process.exitCode=J.GENERIC_ERROR}}}for(const q of Lq){if($.commands.some((K)=>K.name()===q.name))continue;$.command(q.name).description(`(External) ${q.description||q.name}`).argument("[args...]").allowUnknownOption().passThroughOptions().helpOption(!1).action((K)=>lK(q.name,K))}const ZK=$.command("antigravity").description("antigravity commands");ZK.command("serve").description("Start Anthropic-compatible API proxy for Antigravity").option("--port <port>","Server port (default: 8082)","8082").option("--timeout <seconds>","Maximum time to wait for a reply (default: 120s)").action(async(q)=>{const{startServe:K}=await import("../../clis/antigravity/serve.js");await K({port:parseInt(q.port,10),timeout:q.timeout?yK(q.timeout,"--timeout",120):void 0})});const $K=new Map;$K.set("antigravity",ZK);const HK=NQ($,$K);JX($);const iK=Lq.map((q)=>q.name),oK=Lq.map((q)=>({name:q.name,label:VK(q)})),Sq=new Map;for(const[,q]of Dq())if(!Sq.has(q.site))Sq.set(q.site,q.domain);const zK=[],NK=[];for(const q of HK)if(jK(Sq.get(q))==="app")zK.push(q);else NK.push(q);const UK={external:oK,apps:zK,sites:NK},pK=new Set([...iK,...HK]);Nq(j,{globalCommand:$,description:S});Nq(B,{globalCommand:$,description:"Inspect website login status"});Nq(Hq,{globalCommand:$,description:nK});Nq(Qq,{globalCommand:$,description:mK});Nq(jq,{globalCommand:$,description:dK});Nq(v,{globalCommand:$,description:gK});$.configureHelp({visibleCommands:(q)=>q.commands.filter((K)=>q!==$||!pK.has(K.name()))});const aK=(q)=>{const K=[];let X=q.parent;while(X){const Q=WQ(X);K.unshift(Q?`${X.name()} ${Q}`:X.name());X=X.parent}return[...K,q.name(),q.usage()].filter(Boolean).join(" ").trim()};function GK(q){q.configureHelp({commandUsage:aK});for(const K of q.commands)GK(K)}GK(j);GQ($,()=>JQ($,UK),()=>UQ(UK));$.on("command:*",(q)=>{const K=q[0];console.error(`error: unknown command '${K}'`);if(wq(K))console.error(` Tip: '${K}' exists on your PATH. Use 'ppcli external register ${K}' to add it as an external CLI.`);$.outputHelp();process.exitCode=J.USAGE_ERROR});return $}export function runCli(N,H){createProgram(N,H).parse()}export{findPackageRoot};export function resolveBrowserVerifyInvocation(N={}){const H=N.platform??process.platform,$=N.fileExists??I.existsSync,V=N.readFile??((F)=>I.readFileSync(F,"utf-8")),B=N.projectRoot??findPackageRoot(uq,$);for(const F of qQ(B,V))if($(F))return{binary:process.execPath,args:[F],cwd:B};const j=M.join(B,"src","main.ts");if(!$(j))throw Error(`Could not find ppcli entrypoint under ${B}. Expected built entry from package.json or src/main.ts.`);const S=M.join(B,"node_modules",".bin",H==="win32"?"tsx.cmd":"tsx");if($(S))return{binary:S,args:[j],cwd:B,...H==="win32"?{shell:!0}:{}};return{binary:H==="win32"?"npx.cmd":"npx",args:["tsx",j],cwd:B,...H==="win32"?{shell:!0}:{}}}
|
|
126
|
+
Fixture already exists at ${F(Q,Y)}.`);console.log(" Use --update-fixture to overwrite.")}else{const i=G(c,L!==void 0?L:g?{limit:3}:void 0),yq=U(Q,Y,i);console.log(`
|
|
127
|
+
${T?"↻ Updated":"✎ Wrote"} fixture: ${yq}`);console.log(" Review and hand-tune the derived expectations (add patterns / notEmpty, tighten rowCount).");T=i}if(!T){console.log(`
|
|
128
|
+
✓ Adapter runs. (No fixture at ${F(Q,Y)} — consider --write-fixture to seed one.)`);const C=checkSiteMemory(Q);printSiteMemoryReport(C,K.strictMemory);if(!C.ok&&K.strictMemory)process.exitCode=J.GENERIC_ERROR;return}const e=W(c,T);if(e.length===0){console.log(`
|
|
129
|
+
✓ Adapter matches fixture (${F(Q,Y)}).`);const C=checkSiteMemory(Q);printSiteMemoryReport(C,K.strictMemory);if(!C.ok&&K.strictMemory)process.exitCode=J.GENERIC_ERROR;return}console.log(`
|
|
130
|
+
✗ Adapter output does not match fixture:`);for(const C of e.slice(0,20)){const i=C.rowIndex!==void 0?`row[${C.rowIndex}] `:"";console.log(` - [${C.rule}] ${i}${C.detail}`)}if(e.length>20)console.log(` ... and ${e.length-20} more failure(s)`);process.exitCode=J.GENERIC_ERROR}catch(X){console.error(`Error: ${X instanceof Error?X.message:String(X)}`);process.exitCode=J.GENERIC_ERROR}});j.command("close").description("Release the current browser session tab lease").action(D(async(q)=>{await q.closeWindow?.();console.log("Browser session tab lease released")}));$.command("doctor").description("Diagnose ppcli browser bridge connectivity").option("-v, --verbose","Debug output").action(async(q)=>{JX(q);const{runBrowserDoctor:K,renderBrowserDoctorReport:X}=await import("./doctor.js"),Q=await K({cliVersion:Pq});console.log(X(Q))});$.command("completion").description("Output shell completion script").argument("<shell>","Shell type: bash, zsh, or fish").action((q)=>{ZQ(q)});const Yq=$.command("plugin").description("Manage ppcli plugins"),gK=Yq.description();Yq.command("install").description("Install a plugin from a git repository").argument("<source>","Plugin source (e.g. github:user/repo)").action(async(q)=>{const{installPlugin:K}=await import("./plugin.js"),{discoverPlugins:X}=await import("./discovery.js");try{const Q=K(q);await X();if(Array.isArray(Q))if(Q.length===0)console.log("No plugins were installed (all skipped or incompatible).");else console.log(`✅ Installed ${Q.length} plugin(s) from monorepo: ${Q.join(", ")}`);else console.log(`✅ Plugin "${Q}" installed successfully. Commands are ready to use.`)}catch(Q){console.error(`Error: ${h(Q)}`);process.exitCode=J.GENERIC_ERROR}});Yq.command("uninstall").description("Uninstall a plugin").argument("<name>","Plugin name").action(async(q)=>{const{uninstallPlugin:K}=await import("./plugin.js");try{K(q);console.log(`✅ Plugin "${q}" uninstalled.`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.GENERIC_ERROR}});Yq.command("update").description("Update a plugin (or all plugins) to the latest version").argument("[name]","Plugin name (required unless --all is passed)").option("--all","Update all installed plugins").action(async(q,K)=>{if(!q&&!K.all){console.error("Error: Please specify a plugin name or use the --all flag.");process.exitCode=J.USAGE_ERROR;return}if(q&&K.all){console.error("Error: Cannot specify both a plugin name and --all.");process.exitCode=J.USAGE_ERROR;return}const{updatePlugin:X,updateAllPlugins:Q}=await import("./plugin.js"),{discoverPlugins:Y}=await import("./discovery.js");if(K.all){const Z=Q();if(Z.length>0)await Y();let N=!1;console.log(" Update Results:");for(const U of Z){if(U.success){console.log(` ✓ ${U.name}`);continue}N=!0;console.log(` ✗ ${U.name} — ${String(U.error)}`)}if(Z.length===0){console.log(" No plugins installed.");return}console.log();if(N){console.error("Completed with some errors.");process.exitCode=J.GENERIC_ERROR}else console.log("✅ All plugins updated successfully.");return}try{X(q);await Y();console.log(`✅ Plugin "${q}" updated successfully.`)}catch(Z){console.error(`Error: ${h(Z)}`);process.exitCode=J.GENERIC_ERROR}});Yq.command("list").description("List installed plugins").option("-f, --format <fmt>","Output format: table, json","table").action(async(q)=>{const{listPlugins:K}=await import("./plugin.js"),X=K();if(X.length===0){console.log(" No plugins installed.");console.log(" Install one with: ppcli plugin install github:user/repo");return}if(q.format==="json"){Fq(X,{fmt:"json",columns:["name","commands","source"],title:"ppcli/plugins",source:"ppcli plugin list"});return}console.log();console.log(" Installed plugins");console.log();const Q=X.filter((Z)=>!Z.monorepoName),Y=new Map;for(const Z of X){if(!Z.monorepoName)continue;const N=Y.get(Z.monorepoName)??[];N.push(Z);Y.set(Z.monorepoName,N)}for(const Z of Q){const N=Z.version?` @${Z.version}`:"",U=Z.description?` — ${Z.description}`:"",G=Z.commands.length>0?` (${Z.commands.join(", ")})`:"",W=Z.source?` ← ${Z.source}`:"";console.log(` ${Z.name}${N}${U}${G}${W}`)}for(const[Z,N]of Y){console.log();console.log(` \uD83D\uDCE6 ${Z} (monorepo)`);for(const U of N){const G=U.version?` @${U.version}`:"",W=U.description?` — ${U.description}`:"",_=U.commands.length>0?` (${U.commands.join(", ")})`:"";console.log(` ${U.name}${G}${W}${_}`)}}console.log();console.log(` ${X.length} plugin(s) installed`);console.log()});Yq.command("create").description("Create a new plugin scaffold").argument("<name>","Plugin name (lowercase, hyphens allowed)").option("-d, --dir <path>","Output directory (default: ./<name>)").option("--description <text>","Plugin description").action(async(q,K)=>{const{createPluginScaffold:X}=await import("./plugin-scaffold.js");try{const Q=X(q,{dir:K.dir,description:K.description});console.log(`✅ Plugin scaffold created at ${Q.dir}`);console.log();console.log(" Files created:");for(const Y of Q.files)console.log(` ${Y}`);console.log();console.log(" Next steps:");console.log(` cd ${Q.dir}`);console.log(` ppcli plugin install file://${Q.dir}`);console.log(` ppcli ${q} hello`)}catch(Q){console.error(`Error: ${h(Q)}`);process.exitCode=J.GENERIC_ERROR}});const _q=$.command("adapter").description("Manage CLI adapters"),nK=_q.description();_q.command("status").description("Show which sites have local overrides vs using official baseline").action(async()=>{const q=await import("node:os"),K=M.join(q.homedir(),".ppcli","clis"),X=H;try{const Y=(await I.promises.readdir(K,{withFileTypes:!0})).filter((N)=>N.isDirectory()).map((N)=>N.name).sort();let Z=[];try{Z=(await I.promises.readdir(X,{withFileTypes:!0})).filter((U)=>U.isDirectory()).map((U)=>U.name).sort()}catch{}if(Y.length===0){console.log("No local adapter overrides. All sites use the official baseline.");return}console.log(`Local overrides in ~/.ppcli/clis/ (${Y.length} sites):
|
|
131
|
+
`);for(const N of Y){const G=Z.includes(N)?"override":"custom";console.log(` ${N} [${G}]`)}console.log(`
|
|
132
|
+
Official baseline: ${Z.length} sites in package`)}catch{console.log("No local adapter overrides. All sites use the official baseline.")}});_q.command("eject").description("Copy an official adapter to ~/.ppcli/clis/ for local editing").argument("<site>","Site name (e.g. twitter, bilibili)").action(async(q)=>{const K=await import("node:os"),X=M.join(K.homedir(),".ppcli","clis"),Q=M.join(H,q),Y=M.join(X,q);try{await I.promises.access(Q)}catch{console.error(`Error: Site "${q}" not found in official adapters.`);process.exitCode=J.USAGE_ERROR;return}try{await I.promises.access(Y);console.error(`Site "${q}" already exists in ~/.ppcli/clis/. Use "ppcli adapter reset ${q}" first to restore official version.`);process.exitCode=J.USAGE_ERROR;return}catch{}I.cpSync(Q,Y,{recursive:!0});console.log(`✅ Ejected "${q}" to ~/.ppcli/clis/${q}/`);console.log("You can now edit the adapter files. Changes take effect immediately.");console.log("Note: Official updates to this adapter will overwrite your changes.")});_q.command("reset").description("Remove local override and restore official adapter version").argument("[site]","Site name (e.g. twitter, bilibili)").option("--all","Reset all local overrides").action(async(q,K)=>{const X=await import("node:os"),Q=M.join(X.homedir(),".ppcli","clis");if(K.all){try{const U=(await I.promises.readdir(Q,{withFileTypes:!0})).filter((G)=>G.isDirectory());if(U.length===0){console.log("No local sites to reset.");return}for(const G of U)I.rmSync(M.join(Q,G.name),{recursive:!0,force:!0});console.log(`✅ Reset ${U.length} site(s). All adapters now use official baseline.`)}catch{console.log("No local sites to reset.")}return}if(!q){console.error("Error: Please specify a site name or use --all.");process.exitCode=J.USAGE_ERROR;return}const Y=M.join(Q,q);try{await I.promises.access(Y)}catch{console.error(`Site "${q}" has no local override.`);return}const Z=I.existsSync(M.join(H,q));I.rmSync(Y,{recursive:!0,force:!0});console.log(Z?`✅ Reset "${q}". Now using official baseline.`:`✅ Removed custom site "${q}".`)});const v=$.command("profile").description("Manage Browser Bridge Chrome profiles"),lK=v.description();v.command("list").description("List Chrome profiles: extension-connected and managed (dedicated Chrome instances)").option("-f, --format <fmt>","Output format: text, json","text").action(async(q)=>{const K=await cQ(),X=dQ(),Q=K?.profiles??[],Y=!!K&&!eQ(K,Pq)&&Array.isArray(K.profiles),Z=await Promise.all(cq().map(async({name:G,entry:W})=>({name:G,port:W.port,userDataDir:W.userDataDir,running:await oQ(W),isDefault:X.defaultContextId===G,createdAt:W.createdAt??"",label:W.label??"",proxy:W.proxy?{scheme:W.proxy.scheme,host:W.proxy.host,port:W.proxy.port,hasAuth:!!W.proxy.username,username:W.proxy.username??""}:null,fingerprint:W.fingerprint?.enabled?{enabled:!0,timezone:W.fingerprint.timezone??"",locale:W.fingerprint.locale??"",hardwareConcurrency:W.fingerprint.hardwareConcurrency??0,deviceMemory:W.fingerprint.deviceMemory??0,screen:W.fingerprint.screen??null,webglVendor:W.fingerprint.webglVendor??"",webglRenderer:W.fingerprint.webglRenderer??"",canvasNoise:W.fingerprint.canvasNoise===!0}:{enabled:!1},webrtc:W.webrtc!==!1,clean:W.clean!==!1})));if(q.format==="json"){const G=Y?Q.map((W)=>({contextId:W.contextId,alias:kK(X,W.contextId)??"",connected:!0,version:W.extensionVersion??"",isDefault:X.defaultContextId===W.contextId})):[];console.log(JSON.stringify({managed:Z,extension:G,daemonRunning:!!K,daemonStale:!!K&&!Y,defaultProfile:X.defaultContextId??""},null,2));return}if(Z.length>0){console.log("Managed profiles (dedicated Chrome instances)");console.log();for(const G of Z){const W=G.isDefault?" default":"";console.log(` ${G.name}${W} — ${G.running?`running on port ${G.port}`:"stopped"}`)}console.log()}if(!K){console.log("Daemon is not running. Run ppcli doctor after opening Chrome.");return}if(!Y){console.log(`Daemon ${tQ(K)} is stale for CLI v${Pq}.`);console.log("Run: ppcli daemon restart");return}if(Q.length===0){console.log("No Browser Bridge profiles connected.");console.log("Open a Chrome profile with the ppcli extension installed, then run ppcli profile list again.");return}const N=new Set(Q.map((G)=>G.contextId));console.log("Connected Browser Bridge profiles");console.log();for(const G of Q){const W=kK(X,G.contextId),_=X.defaultContextId===G.contextId?" default":"",F=W?` ${W}`:"",k=G.extensionVersion?` v${G.extensionVersion}`:" version unknown";console.log(` ${G.contextId}${F}${_} — connected${k}`)}const U=Object.entries(X.aliases).filter(([,G])=>!N.has(G));if(U.length>0||X.defaultContextId&&!N.has(X.defaultContextId)){console.log();console.log("Disconnected saved profiles:");const G=new Set;for(const[W,_]of U){G.add(_);console.log(` ${_} ${W} — not connected`)}if(X.defaultContextId&&!G.has(X.defaultContextId)&&!N.has(X.defaultContextId))console.log(` ${X.defaultContextId} — default, not connected`)}});const ZK=(q)=>{if(q.config){const Q=JSON.parse(q.config);if(typeof Q.proxy==="string")Q.proxy=gq(Q.proxy);return Q}const K={};if(q.label)K.label=q.label;if(q.proxy)K.proxy=gq(q.proxy);if(q.webrtc===!1)K.webrtc=!1;if(q.clean===!1)K.clean=!1;const X=q.fingerprint;if(X&&X!=="off")K.fingerprint={enabled:!0,spoof:X!=="lite",...q.fpTimezone?{timezone:q.fpTimezone}:{},...q.fpLocale?{locale:q.fpLocale}:{},...q.fpUa?{userAgent:q.fpUa}:{},...q.fpPlatform?{platform:q.fpPlatform}:{},...q.fpSeed?{seed:Number(q.fpSeed)}:{}};return K},$K=(q)=>q?{scheme:q.scheme,host:q.host,port:q.port,hasAuth:!!q.username,username:q.username??""}:null,HK=(q)=>q?.enabled?{enabled:!0,seed:q.seed,timezone:q.timezone??"",locale:q.locale??"",userAgent:q.userAgent??"",platform:q.platform??"",hardwareConcurrency:q.hardwareConcurrency??0,deviceMemory:q.deviceMemory??0,screen:q.screen??null,webglVendor:q.webglVendor??"",webglRenderer:q.webglRenderer??"",canvasNoise:q.canvasNoise===!0}:{enabled:!1},zK=(q)=>q.option("--label <text>","Display label / note for the environment").option("--proxy <url>","Per-env proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("--fingerprint <mode>","Fingerprint mode: off | on (spoof soft signals) | lite (timezone/locale follow only)").option("--fp-timezone <iana>","Fingerprint timezone, e.g. America/New_York (must be set explicitly)").option("--fp-locale <locale>","Fingerprint locale, e.g. en-US (must be set explicitly)").option("--fp-ua <ua>","Override navigator.userAgent (use with care)").option("--fp-platform <p>","Override navigator.platform, e.g. Win32").option("--fp-seed <n>","Fixed fingerprint seed (for reproducible fingerprint)").option("--no-webrtc","Disable WebRTC leak protection (on by default)").option("--no-clean","Disable lean launch flags / telemetry suppression (on by default)").option("--config <json>","Full config as JSON: { label, proxy, fingerprint } — overrides granular flags");v.command("create").description("Create a managed profile: a dedicated Chrome instance with its own user data dir (for a second account)").argument("<name>","Profile name, e.g. work / brand2").option("--browser-path <path>","Explicit browser executable (defaults to auto-discovered Chrome)").option("--no-start","Create only; do not launch Chrome now").option("-f, --format <fmt>","Output format: text, json","text").action(async(q,K)=>{try{const X=ZK(K),{opts:Q}=await IK({...X,browserPath:K.browserPath}),Y=await iQ(q,{...Q,browserPath:K.browserPath});if(K.start!==!1)await dq(q,Y);if(K.format==="json"){console.log(JSON.stringify({ok:!0,name:q,port:Y.port,userDataDir:Y.userDataDir,label:Y.label??"",proxy:$K(Y.proxy),fingerprint:HK(Y.fingerprint),webrtc:Y.webrtc!==!1,clean:Y.clean!==!1,started:K.start!==!1},null,2));return}console.log(`✅ 托管 profile "${q}" 已创建(端口 ${Y.port})`);console.log(` 数据目录:${Y.userDataDir}`);if(Y.proxy)console.log(` 代理:${Y.proxy.scheme}://${Y.proxy.host}:${Y.proxy.port}`);if(Y.fingerprint?.enabled)console.log(` 指纹:已开启${Y.fingerprint.timezone?` 时区 ${Y.fingerprint.timezone}`:""}${Y.fingerprint.locale?` 语言 ${Y.fingerprint.locale}`:""}`);if(K.start!==!1)console.log(` Chrome 已启动。用 ppcli <site> login --profile ${q} 在新窗口里登录账号。`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});zK(v.commands[v.commands.length-1]);v.command("start").description("Launch the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(q)=>{try{const K=cq().find((Q)=>Q.name===q.trim());if(!K)throw Error(`托管 profile "${q}" 不存在,先 ppcli profile create ${q}`);const X=await dq(K.name,K.entry);console.log(`✅ profile "${q}" 运行中(${X})`)}catch(K){console.error(`Error: ${h(K)}`);process.exitCode=J.USAGE_ERROR}});v.command("stop").description("Gracefully close the dedicated Chrome instance of a managed profile").argument("<name>","Managed profile name").action(async(q)=>{try{const K=cq().find((Q)=>Q.name===q.trim());if(!K)throw Error(`托管 profile "${q}" 不存在`);const X=await aQ(K.entry);console.log(X?`✅ profile "${q}" 已关闭`:`profile "${q}" 本来就没在运行`)}catch(K){console.error(`Error: ${h(K)}`);process.exitCode=J.USAGE_ERROR}});v.command("remove").description("Remove a managed profile (closes Chrome and deletes its login data)").argument("<name>","Managed profile name").option("--keep-data","Keep the user data dir on disk",!1).action(async(q,K)=>{try{await pQ(q,{keepData:K.keepData===!0});console.log(`✅ 托管 profile "${q}" 已删除${K.keepData?"(保留数据目录)":"(含登录数据)"}`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});v.command("update").description("Reconfigure a managed profile (label / proxy / fingerprint); takes effect on next start").argument("<name>","Managed profile name").option("--clear-proxy","Remove the proxy from this environment").option("--restart","Relaunch the environment after updating",!1).option("-f, --format <fmt>","Output format: text, json","text").action(async(q,K)=>{try{const X=ZK(K),{opts:Q}=await IK(X),Y={};if(X.label!==void 0)Y.label=X.label??null;if(K.clearProxy)Y.proxy=null;else if(Q.proxy)Y.proxy=Q.proxy;if(K.fingerprint==="off")Y.fingerprint=null;else if(Q.fingerprint)Y.fingerprint=Q.fingerprint;if(X.webrtc!==void 0)Y.webrtc=X.webrtc;if(X.clean!==void 0)Y.clean=X.clean;const{entry:Z,wasRunning:N}=await rQ(q,Y);if(K.restart&&N)await dq(q,Z);if(K.format==="json"){console.log(JSON.stringify({ok:!0,name:q,label:Z.label??"",proxy:$K(Z.proxy),fingerprint:HK(Z.fingerprint),webrtc:Z.webrtc!==!1,clean:Z.clean!==!1,wasRunning:N,restarted:!!(K.restart&&N)},null,2));return}console.log(`✅ 托管 profile "${q}" 配置已更新${N?"(已关闭,重启后生效)":""}`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});zK(v.commands[v.commands.length-1]);v.command("test-proxy").description("Test proxy connectivity and detect exit IP / country / timezone").argument("<url>","Proxy: scheme://[user:pass@]host:port (scheme = http/https/socks5)").option("-f, --format <fmt>","Output format: text, json","text").action(async(q,K)=>{try{const X=gq(q),Q=await sQ(X);if(K.format==="json"){console.log(JSON.stringify(Q,null,2));return}if(Q.ok){console.log(`✅ 代理可用 — 出口 IP ${Q.ip}`);console.log(` 位置:${Q.country??"?"}${Q.city?" / "+Q.city:""}`);console.log(` 时区:${Q.timezone??"?"} 语言:${Q.locale??"?"}`)}else{console.log(`❌ 代理不可用:${Q.error}`);process.exitCode=J.USAGE_ERROR}}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});v.command("rename").description("Assign a local alias to a connected Browser Bridge profile").argument("<contextId>","Profile contextId from ppcli profile list").argument("<alias>","Local alias, e.g. work or personal").action((q,K)=>{try{gQ(q,K);console.log(`Profile ${q} is now aliased as ${K}.`)}catch(X){console.error(`Error: ${h(X)}`);process.exitCode=J.USAGE_ERROR}});v.command("use").description("Set the default Browser Bridge profile for future commands").argument("<profile>","Profile alias or contextId").action((q)=>{try{const K=lQ(q);console.log(`Default Browser Bridge profile: ${K.defaultContextId??q}`)}catch(K){console.error(`Error: ${h(K)}`);process.exitCode=J.USAGE_ERROR}});const zq=$.command("daemon").description("Manage the ppcli daemon"),iK=zq.description();zq.command("status").description("Show daemon status").action(async()=>{await vQ()});zq.command("stop").description("Stop the daemon").action(async()=>{await fQ()});zq.command("restart").description("Restart the daemon").action(async()=>{await yQ()});zq.command("warm").description("Pre-open the background automation window (keeps a tab alive for reuse)").option("--url <url>","http(s) info page to keep in the warm tab (falls back to about:blank)").action(async(q)=>{await xQ(q.url)});const Dq=fq(),wq=$.command("external").description("Manage external CLI passthrough commands");wq.command("install").description("Install an external CLI").argument("<name>","Name of the external CLI").action((q)=>{const K=Dq.find((X)=>X.name===q);if(!K){console.error(`External CLI '${q}' not found in registry.`);process.exitCode=J.USAGE_ERROR;return}HQ(K)});wq.command("register").description("Register an external CLI").argument("<name>","Name of the CLI").option("--binary <bin>","Binary name if different from name").option("--install <cmd>","Auto-install command").option("--desc <text>","Description").action((q,K)=>{zQ(q,{binary:K.binary,install:K.install,description:K.desc})});wq.command("list").description("List registered external CLIs").option("-f, --format <fmt>","Output format: table, json, yaml, md, csv","table").action((q)=>{const K=fq().map((X)=>({name:X.name,package:X.package??"",binary:X.binary,installed:xq(X.binary),description:X.description??"",homepage:X.homepage??"",tags:X.tags?.join(", ")??""}));Fq(K,{fmt:q.format,columns:["name","package","binary","installed","description","homepage","tags"],title:"ppcli/external/list",source:"ppcli external list"})});function oK(q,K){const X=K??(()=>{const Q=process.argv.indexOf(q);return process.argv.slice(Q+1)})();try{$Q(q,X,Dq)}catch(Q){if(Q instanceof uq){process.stderr.write(KQ.dump(_Q(Q),{sortKeys:!1,lineWidth:120,noRefs:!0}));process.exitCode=Q.exitCode}else{console.error(`Error: ${h(Q)}`);process.exitCode=J.GENERIC_ERROR}}}for(const q of Dq){if($.commands.some((K)=>K.name()===q.name))continue;$.command(q.name).description(`(External) ${q.description||q.name}`).argument("[args...]").allowUnknownOption().passThroughOptions().helpOption(!1).action((K)=>oK(q.name,K))}const NK=$.command("antigravity").description("antigravity commands");NK.command("serve").description("Start Anthropic-compatible API proxy for Antigravity").option("--port <port>","Server port (default: 8082)","8082").option("--timeout <seconds>","Maximum time to wait for a reply (default: 120s)").action(async(q)=>{const{startServe:K}=await import("../../clis/antigravity/serve.js");await K({port:parseInt(q.port,10),timeout:q.timeout?xK(q.timeout,"--timeout",120):void 0})});const UK=new Map;UK.set("antigravity",NK);const GK=GQ($,UK);jX($);const pK=Dq.map((q)=>q.name),aK=Dq.map((q)=>({name:q.name,label:DK(q)})),hq=new Map;for(const[,q]of Bq())if(!hq.has(q.site))hq.set(q.site,q.domain);const WK=[],JK=[];for(const q of GK)if(AK(hq.get(q))==="app")WK.push(q);else JK.push(q);const VK={external:aK,apps:WK,sites:JK},rK=new Set([...pK,...GK]);Uq(j,{globalCommand:$,description:S});Uq(B,{globalCommand:$,description:"Inspect website login status"});Uq(zq,{globalCommand:$,description:iK});Uq(Yq,{globalCommand:$,description:gK});Uq(_q,{globalCommand:$,description:nK});Uq(v,{globalCommand:$,description:lK});$.configureHelp({visibleCommands:(q)=>q.commands.filter((K)=>q!==$||!rK.has(K.name()))});const sK=(q)=>{const K=[];let X=q.parent;while(X){const Q=VQ(X);K.unshift(Q?`${X.name()} ${Q}`:X.name());X=X.parent}return[...K,q.name(),q.usage()].filter(Boolean).join(" ").trim()};function jK(q){q.configureHelp({commandUsage:sK});for(const K of q.commands)jK(K)}jK(j);JQ($,()=>jQ($,VK),()=>WQ(VK));$.on("command:*",(q)=>{const K=q[0];console.error(`error: unknown command '${K}'`);if(xq(K))console.error(` Tip: '${K}' exists on your PATH. Use 'ppcli external register ${K}' to add it as an external CLI.`);$.outputHelp();process.exitCode=J.USAGE_ERROR});return $}export function runCli(H,z){createProgram(H,z).parse()}export{findPackageRoot};export function resolveBrowserVerifyInvocation(H={}){const z=H.platform??process.platform,$=H.fileExists??I.existsSync,V=H.readFile??((R)=>I.readFileSync(R,"utf-8")),B=H.projectRoot??findPackageRoot(nq,$);for(const R of QQ(B,V))if($(R))return{binary:process.execPath,args:[R],cwd:B};const j=M.join(B,"src","main.ts");if(!$(j))throw Error(`Could not find ppcli entrypoint under ${B}. Expected built entry from package.json or src/main.ts.`);const S=M.join(B,"node_modules",".bin",z==="win32"?"tsx.cmd":"tsx");if($(S))return{binary:S,args:[j],cwd:B,...z==="win32"?{shell:!0}:{}};return{binary:z==="win32"?"npx.cmd":"npx",args:["tsx",j],cwd:B,...z==="win32"?{shell:!0}:{}}}
|
package/dist/src/execution.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{getRegistry as i,fullName as R}from"./registry.js";import{pathToFileURL as m}from"node:url";import*as n from"node:crypto";import*as h from"node:fs";import*as t from"node:os";import{executePipeline as b}from"./pipeline/index.js";import{adapterLoadError as d,ArgumentError as F,CommandExecutionError as U,attachTraceReceipt as s,getErrorMessage as f}from"./errors.js";import{shouldUseBrowserSession as o}from"./capabilityRouting.js";import{getBrowserFactory as a,browserSession as r,runWithTimeout as v,DEFAULT_BROWSER_COMMAND_TIMEOUT as e}from"./runtime.js";import{withBrowserSessionLockIf as qq}from"./browser-session-lock.js";import{resolveProfile as Qq}from"./browser/profile.js";import{ensureManagedChrome as Xq}from"./browser/managed-chrome.js";import{generateFingerprintJs as Yq}from"./browser/fingerprint.js";import{CDPBridge as Zq}from"./browser/index.js";import{emitHook as
|
|
1
|
+
import{getRegistry as i,fullName as R}from"./registry.js";import{pathToFileURL as m}from"node:url";import*as n from"node:crypto";import*as h from"node:fs";import*as t from"node:os";import{executePipeline as b}from"./pipeline/index.js";import{adapterLoadError as d,ArgumentError as F,CommandExecutionError as U,attachTraceReceipt as s,getErrorMessage as f}from"./errors.js";import{shouldUseBrowserSession as o}from"./capabilityRouting.js";import{getBrowserFactory as a,browserSession as r,runWithTimeout as v,DEFAULT_BROWSER_COMMAND_TIMEOUT as e}from"./runtime.js";import{withBrowserSessionLockIf as qq}from"./browser-session-lock.js";import{resolveProfile as Qq}from"./browser/profile.js";import{ensureManagedChrome as Xq}from"./browser/managed-chrome.js";import{generateFingerprintJs as Yq}from"./browser/fingerprint.js";import{CDPBridge as Zq}from"./browser/index.js";import{emitHook as N}from"./hooks.js";import{log as w}from"./logger.js";import{isElectronApp as $q}from"./electron-apps.js";import{probeCDP as Jq,resolveElectronEndpoint as Gq}from"./launcher.js";import{ObservationSession as Hq,exportObservationSession as Vq}from"./observation/index.js";import{resolveAdapterSourcePath as Lq}from"./adapter-source.js";const j=new Map,x=new Map,zq=`${t.homedir()}/.ppcli/clis/`;function Wq(q){if(q===void 0||q===null||q===""||q==="off")return"off";if(q==="on"||q==="retain-on-failure")return q;throw new F(`--trace must be one of: off, on, retain-on-failure. Received: "${String(q)}"`)}export function coerceAndValidateArgs(q,Q){const Y={...Q};for(const X of q){const Z=Y[X.name];if(X.required&&(Z===void 0||Z===null||Z===""))throw new F(`Argument "${X.name}" is required.`,X.help??`Provide a value for --${X.name}`);if(Z!==void 0&&Z!==null){if(X.type==="int"||X.type==="number"){const V=Number(Z);if(Number.isNaN(V))throw new F(`Argument "${X.name}" must be a valid number. Received: "${Z}"`);Y[X.name]=V}else if(X.type==="boolean"||X.type==="bool")if(typeof Z==="string"){const V=Z.toLowerCase();if(V==="true"||V==="1")Y[X.name]=!0;else if(V==="false"||V==="0")Y[X.name]=!1;else throw new F(`Argument "${X.name}" must be a boolean (true/false). Received: "${Z}"`)}else Y[X.name]=Boolean(Z);const J=Y[X.name];if(X.choices&&X.choices.length>0){if(!X.choices.map(String).includes(String(J)))throw new F(`Argument "${X.name}" must be one of: ${X.choices.join(", ")}. Received: "${J}"`)}}else if(X.default!==void 0)Y[X.name]=X.default}return Y}async function A(q,Q,Y,X){const Z=q;if(Z._lazy&&Z._modulePath){const J=Z._modulePath;if(J.startsWith(zq)&&j.has(J))try{const z=h.statSync(J),$=x.get(J);if($!==void 0&&z.mtimeMs!==$)j.delete(J)}catch{}if(!j.has(J)){const z=m(J).href,y=import(x.has(J)?`${z}?t=${Date.now()}`:z).then(()=>{try{x.set(J,h.statSync(J).mtimeMs)}catch{}},(K)=>{j.delete(J);throw d(`Failed to load adapter module ${J}: ${f(K)}`,"Check that the adapter file exists and has no syntax errors.")});j.set(J,y)}await j.get(J);const W=i().get(R(q));if(W?.func)return u(W,Q,Y,X);if(W?.pipeline)return b(Q,W.pipeline,{args:Y,debug:X})}if(q.func)return u(q,Q,Y,X);if(q.pipeline)return b(Q,q.pipeline,{args:Y,debug:X});throw new U(`Command ${R(q)} has no func or pipeline`,"This is likely a bug in the adapter definition. Please report this issue.")}function u(q,Q,Y,X){if(q.browser===!1)return q.func(Y,X);if(!Q)throw new U(`Command ${R(q)} requires a browser session but none was provided`);return q.func(Q,Y,X)}function Fq(q){if(q.navigateBefore===!1)return null;if(typeof q.navigateBefore==="string")return q.navigateBefore;return null}function Kq(q,Q){if(!q||!Q)return!1;try{const Y=new URL(q).hostname;return Y===Q||Y.endsWith(`.${Q}`)}catch{return!1}}function Bq(q,Q){if(!Q)return!1;try{const Y=new URL(q),X=Y.hostname===Q||Y.hostname.endsWith(`.${Q}`),Z=Y.pathname===""||Y.pathname==="/";return X&&Z&&Y.search===""&&Y.hash===""}catch{return!1}}async function Iq(q,Q,Y,X){if(Y!=="persistent"||!q.domain)return!0;if(!Bq(X,q.domain))return!0;const Z=await Q.getCurrentUrl?.().catch(()=>null);return!Kq(Z,q.domain)}export async function executeCommand(q,Q,Y=!1,X={}){let Z;try{Z=X.prepared?Q:prepareCommandArgs(q,Q)}catch($){if($ instanceof F)throw $;throw new F(f($))}const J=Nq(q,Z),V=Wq(X.trace),W={command:R(q),args:Z,startedAt:Date.now()};await N("onBeforeExecute",W);let z;try{if(o(q)){const $=$q(q.site),y=Qq(X.profile),K=!$&&y.kind==="managed"?y:null;let O;const C=Cq(q.defaultWindowMode??"background",X.windowMode);let D,M;if(K){O=await Xq(K.name,K.entry,{windowMode:C});const L=K.entry.fingerprint;if(L?.enabled)D=Yq(L);const G=K.entry.proxy;if(G?.username)M={username:G.username,password:G.password??""}}else if($){const L=process.env.OPENCLI_CDP_ENDPOINT;if(L){const G=Number(new URL(L).port);if(!await Jq(G))throw new U(`CDP not reachable at ${L}`,"Check that the app is running with --remote-debugging-port and the endpoint is correct.");O=L}else O=await Gq(q.site)}const l=K?Zq:a(q.site),k=!K&&y.kind==="extension"?y.contextId:void 0,c=q,_=jq(q,X.siteSession),P=Oq(q,_),E=Uq(_,X.keepTab);z=await qq(C!=="foreground",()=>r(l,async(L)=>{const G=V==="off"?null:new Hq({scope:{contextId:k??K?.name,session:P,target:L.getActivePage?.(),site:q.site,command:R(q),adapterSourcePath:Lq(c)}});if(G){G.record({stream:"action",name:"command",phase:"start",data:{args:Z}});await L.startNetworkCapture?.().catch(()=>!1)}const I=Fq(q);if(I&&await Iq(q,L,_,I)){G?.record({stream:"action",name:"pre_navigate",phase:"start",data:{url:I}});try{await L.goto(I);G?.record({stream:"action",name:"pre_navigate",phase:"end",data:{url:I}})}catch(H){G?.record({stream:"action",name:"pre_navigate",phase:"error",data:{url:I,error:H instanceof Error?H.message:String(H)}});const B=new U(`Pre-navigation to ${I} failed: ${H instanceof Error?H.message:H}`,"Check that the site is reachable and the browser extension is running.");if(G&&(V==="on"||V==="retain-on-failure")){G.record({stream:"error",message:B.message,stack:B.stack,code:B.code,hint:B.hint});await T(G,L).catch(()=>{});S(G,"failure",B,X.onTraceExport)}throw B}}try{const H=J!==null?J+p:e,B=await v(A(q,L,Z,Y),{timeout:H,label:R(q)});G?.record({stream:"action",name:"command",phase:"end"});if(G&&V==="on"){await T(G,L).catch(()=>{});S(G,"success",void 0,X.onTraceExport)}if(!E)await L.closeWindow?.().catch(()=>{});return B}catch(H){if(G){G.record({stream:"action",name:"command",phase:"error",data:{error:H instanceof Error?H.message:String(H)}});G.record({stream:"error",message:H instanceof Error?H.message:String(H),stack:H instanceof Error?H.stack:void 0});if(V==="on"||V==="retain-on-failure"){await T(G,L).catch(()=>{});S(G,"failure",H,X.onTraceExport)}}if(!E)await L.closeWindow?.().catch(()=>{});throw H}},{session:P,cdpEndpoint:O,contextId:k,windowMode:C,surface:"adapter",siteSession:_,initScript:D,proxyAuth:M}),K?.name)}else if(J!==null){const $=J+p;z=await v(A(q,null,Z,Y),{timeout:$,label:R(q),hint:`Pass a higher --timeout value (currently ${J}s)`})}else z=await A(q,null,Z,Y)}catch($){W.error=$;W.finishedAt=Date.now();await N("onAfterExecute",W);throw $}W.finishedAt=Date.now();await N("onAfterExecute",W,z);return z}async function T(q,Q){const Y=Q.getActivePage?.()??q.scope.target,[X,Z,J,V,W]=await Promise.all([Q.getCurrentUrl?.().catch(()=>null)??Promise.resolve(null),Q.snapshot().catch(()=>{return}),Q.readNetworkCapture?.().catch(()=>[])??Promise.resolve([]),Q.consoleMessages("all").catch(()=>[]),Q.screenshot({format:"png"}).catch(()=>{return})]);if(Z!==void 0||X!==void 0)q.record({stream:"state",url:X,target:Y,snapshot:Z,label:"final"});for(const z of Array.isArray(J)?J:[]){const $=z;q.record({stream:"network",url:String($.url??""),method:typeof $.method==="string"?$.method:void 0,status:typeof $.responseStatus==="number"?$.responseStatus:void 0,contentType:typeof $.responseContentType==="string"?$.responseContentType:void 0,size:typeof $.responseBodyFullSize==="number"?$.responseBodyFullSize:void 0,requestHeaders:$.requestHeaders,responseHeaders:$.responseHeaders,requestBody:$.requestBodyPreview,responseBody:$.responsePreview,ts:typeof $.timestamp==="number"?$.timestamp:void 0})}for(const z of Array.isArray(V)?V:[])if(z&&typeof z==="object"){const $=z;q.record({stream:"console",level:String($.type??$.level??"log"),text:String($.text??$.message??""),ts:typeof $.timestamp==="number"?$.timestamp:void 0})}else q.record({stream:"console",level:"log",text:String(z)});if(typeof W==="string"&&W)q.record({stream:"screenshot",format:"png",data:W,label:"final"})}function S(q,Q,Y,X){try{const Z=Vq(q,{error:Y,status:Q});if(Q==="failure"&&Y!==void 0)s(Y,Z.receipt);else process.stderr.write(`ppcli trace artifact: ${Z.dir}
|
|
2
2
|
`);try{X?.(Z)}catch(J){w.warn(`[trace] Trace export callback failed: ${J instanceof Error?J.message:String(J)}`)}return Z}catch(Z){w.warn(`[trace] Failed to export trace artifact: ${Z instanceof Error?Z.message:String(Z)}`);return}}export function prepareCommandArgs(q,Q){const Y=coerceAndValidateArgs(q.args,Q);q.validateArgs?.(Y);return Y}const p=30;function Rq(q){if(q===void 0||q===null||q==="")return null;if(q==="ephemeral"||q==="persistent")return q;throw new F(`--site-session must be one of: ephemeral, persistent. Received: "${String(q)}"`)}function yq(){const q=process.env.PUBLISHPORT_SITE_SESSION;if(q==="persistent"||q==="ephemeral")return q;return null}function jq(q,Q){return Rq(Q)??yq()??q.siteSession??"persistent"}function Oq(q,Q){if(Q==="persistent")return`site:${q.site}`;return`site:${q.site}:${n.randomUUID()}`}function _q(q,Q){if(Q===void 0||Q==="")return null;if(Q==="true")return!0;if(Q==="false")return!1;throw new F(`${q} must be one of: true, false. Received: "${String(Q)}"`)}function Uq(q,Q){if(q==="persistent")return!0;return _q("--keep-tab",Q)??!1}function g(q,Q){if(Q===void 0||Q==="")return null;if(Q==="foreground"||Q==="background")return Q;throw new F(`${q} must be one of: foreground, background. Received: "${String(Q)}"`)}function Cq(q="background",Q){return g("--window",Q)??g("OPENCLI_WINDOW",process.env.OPENCLI_WINDOW)??q}function Nq(q,Q){if(!q.args.some((Z)=>Z.name==="timeout"))return null;const Y=Q.timeout;if(Y===void 0||Y===null||Y==="")throw new F(`Argument "timeout" must be a positive integer. Received: "${String(Y)}"`);const X=Number(Y);if(!Number.isInteger(X)||X<=0)throw new F(`Argument "timeout" must be a positive integer. Received: "${String(Y)}"`);return X}
|