publishport-opencli 1.0.5 → 1.0.7

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.
@@ -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 C}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,N=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 x(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),$=N.get(J);if($!==void 0&&z.mtimeMs!==$)j.delete(J)}catch{}if(!j.has(J)){const z=m(J).href,y=import(N.has(J)?`${z}?t=${Date.now()}`:z).then(()=>{try{N.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 C("onBeforeExecute",W);let z;try{if(o(q)){const $=$q(q.site),y=Qq(X.profile),K=!$&&y.kind==="managed"?y:null;let O,S,D;if(K){O=await Xq(K.name,K.entry);const L=K.entry.fingerprint;if(L?.enabled)S=Yq(L);const G=K.entry.proxy;if(G?.username)D={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),M=!K&&y.kind==="extension"?y.contextId:void 0,c=q,_=jq(q,X.siteSession),k=Oq(q,_),P=Uq(_,X.keepTab),E=Cq(q.defaultWindowMode??"background",X.windowMode);z=await qq(E!=="foreground",()=>r(l,async(L)=>{const G=V==="off"?null:new Hq({scope:{contextId:M??K?.name,session:k,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 A(G,L).catch(()=>{});T(G,"failure",B,X.onTraceExport)}throw B}}try{const H=J!==null?J+p:e,B=await v(x(q,L,Z,Y),{timeout:H,label:R(q)});G?.record({stream:"action",name:"command",phase:"end"});if(G&&V==="on"){await A(G,L).catch(()=>{});T(G,"success",void 0,X.onTraceExport)}if(!P)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 A(G,L).catch(()=>{});T(G,"failure",H,X.onTraceExport)}}if(!P)await L.closeWindow?.().catch(()=>{});throw H}},{session:k,cdpEndpoint:O,contextId:M,windowMode:E,surface:"adapter",siteSession:_,initScript:S,proxyAuth:D}),K?.name)}else if(J!==null){const $=J+p;z=await v(x(q,null,Z,Y),{timeout:$,label:R(q),hint:`Pass a higher --timeout value (currently ${J}s)`})}else z=await x(q,null,Z,Y)}catch($){W.error=$;W.finishedAt=Date.now();await C("onAfterExecute",W);throw $}W.finishedAt=Date.now();await C("onAfterExecute",W,z);return z}async function A(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 T(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}
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}
@@ -1,5 +1,5 @@
1
1
  import*as J from"node:fs";import*as p from"node:os";import*as B from"node:path";import{execSync as e,execFileSync as S}from"node:child_process";import{fileURLToPath as k}from"node:url";import{PLUGINS_DIR as T}from"./discovery.js";import{getErrorMessage as U,PluginError as x}from"./errors.js";import{log as w}from"./logger.js";import{isRecord as b}from"./utils.js";import{readPluginManifest as F,isMonorepo as y,getEnabledPlugins as qq,checkCompatibility as O,getCompatibilityRange as M}from"./plugin-manifest.js";const N=process.platform==="win32",d="local:";function a(){return process.env.HOME||process.env.USERPROFILE||p.homedir()}export function getLockFilePath(){return B.join(a(),".ppcli","plugins.lock.json")}export function getMonoreposDir(){return B.join(a(),".ppcli","monorepos")}function h(q){if(!q)return;if(q.startsWith(d))return{kind:"local",path:B.resolve(q.slice(d.length))};return{kind:"git",url:q}}function isLocalPluginSource(q){return h(q)?.kind==="local"}function toStoredPluginSource(q){if(q.kind==="local")return`${d}${B.resolve(q.path)}`;return q.url}function toLocalPluginSource(q){return toStoredPluginSource({kind:"local",path:q})}function Kq(q){if(!b(q))return;if(typeof q.name!=="string"||typeof q.subPath!=="string")return;return{name:q.name,subPath:q.subPath}}function Qq(q,K){if(typeof q==="string"){const Q=h(q);if(!Q)return;if(Q.kind==="git"&&K)return{kind:"monorepo",url:Q.url,repoName:K.name,subPath:K.subPath};return Q}if(!b(q)||typeof q.kind!=="string")return;switch(q.kind){case"git":return typeof q.url==="string"?{kind:"git",url:q.url}:void 0;case"local":return typeof q.path==="string"?{kind:"local",path:B.resolve(q.path)}:void 0;case"monorepo":return typeof q.url==="string"&&typeof q.repoName==="string"&&typeof q.subPath==="string"?{kind:"monorepo",url:q.url,repoName:q.repoName,subPath:q.subPath}:void 0;default:return}}function Vq(q){if(!b(q))return;const K=Kq(q.monorepo),Q=Qq(q.source,K);if(!Q)return;if(typeof q.commitHash!=="string"||typeof q.installedAt!=="string")return;const V={source:Q,commitHash:q.commitHash,installedAt:q.installedAt};if(typeof q.updatedAt==="string")V.updatedAt=q.updatedAt;return V}function resolvePluginSource(q,K){if(q)return q.source;return h(_q(K))}function resolveStoredPluginSource(q,K){const Q=resolvePluginSource(q,K);return Q?toStoredPluginSource(Q):void 0}function moveDir(q,K,Q=J){try{Q.renameSync(q,K)}catch(V){if(V.code==="EXDEV"){try{Q.cpSync(q,K,{recursive:!0})}catch(Y){try{Q.rmSync(K,{recursive:!0,force:!0})}catch{}throw Y}Q.rmSync(q,{recursive:!0,force:!0})}else throw V}}function L(q,K){const Q=`${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;return B.join(B.dirname(q),`.${B.basename(q)}.${K}-${Q}`)}function Yq(q){const K=B.join(p.tmpdir(),`opencli-clone-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);try{S("git",["clone","--depth","1",q,K],{encoding:"utf-8",stdio:["pipe","pipe","pipe"]})}catch(Q){throw new x(`Failed to clone plugin: ${U(Q)}`,"Check the repository URL and your network connection.")}return K}function m(q,K){const Q=Yq(q);try{return K(Q)}finally{try{J.rmSync(Q,{recursive:!0,force:!0})}catch{}}}function Zq(q,K){const Q=resolvePluginSource(q,K);if(!Q||Q.kind==="local")throw Error(`Unable to determine remote source for plugin at ${K}`);return Q.url}function D(q){try{J.lstatSync(q);return!0}catch{return!1}}function C(q,K){const Q=B.resolve(q,K);if(!Q.startsWith(q+B.sep)&&Q!==q)throw new x(`Plugin path "${K}" escapes repo root.`);return Q}function f(q){try{if(J.lstatSync(q).isSymbolicLink()){J.unlinkSync(q);return}J.rmSync(q,{recursive:!0,force:!0})}catch{}}class i{#K=[];#q=!1;track(q){this.#K.push(q);return q}commit(){if(this.#q)return;this.#q=!0;for(const q of this.#K)q.finalize()}rollback(){if(this.#q)return;this.#q=!0;for(const q of[...this.#K].reverse())q.rollback()}}function s(q){const K=new i;try{const Q=q(K);K.commit();return Q}catch(Q){K.rollback();throw Q}}function n(q,K,Q=J){const V=Q.existsSync(K);Q.mkdirSync(B.dirname(K),{recursive:!0});const Y=L(K,"tmp"),Z=V?L(K,"bak"):null;let X=!1;try{moveDir(q,Y,Q);if(Z)Q.renameSync(K,Z);Q.renameSync(Y,K)}catch($){try{Q.rmSync(Y,{recursive:!0,force:!0})}catch{}if(Z&&!Q.existsSync(K))try{Q.renameSync(Z,K)}catch{}throw $}return{finalize(){if(X)return;X=!0;if(Z)try{Q.rmSync(Z,{recursive:!0,force:!0})}catch{}},rollback(){if(X)return;X=!0;try{Q.rmSync(K,{recursive:!0,force:!0})}catch{}if(Z)try{Q.renameSync(Z,K)}catch{}try{Q.rmSync(Y,{recursive:!0,force:!0})}catch{}}}}function $q(q,K){const Q=D(K);if(Q&&!isSymlinkSync(K))throw Error(`Expected monorepo plugin link at ${K} to be a symlink`);J.mkdirSync(B.dirname(K),{recursive:!0});const V=L(K,"tmp"),Y=Q?L(K,"bak"):null,Z=N?"junction":"dir";let X=!1;try{J.symlinkSync(q,V,Z);if(Y)J.renameSync(K,Y);J.renameSync(V,K)}catch($){f(V);if(Y&&!D(K))try{J.renameSync(Y,K)}catch{}throw $}return{finalize(){if(X)return;X=!0;if(Y)f(Y)},rollback(){if(X)return;X=!0;f(K);if(Y&&!D(K))try{J.renameSync(Y,K)}catch{}f(V)}}}function readLockFileWithWriter(q=writeLockFile){try{const K=J.readFileSync(getLockFilePath(),"utf-8"),Q=JSON.parse(K);if(!b(Q))return{};const V={};let Y=!1;for(const[Z,X]of Object.entries(Q)){const $=Vq(X);if(!$){Y=!0;continue}V[Z]=$;if(JSON.stringify(X)!==JSON.stringify($))Y=!0}if(Y)try{q(V)}catch{}return V}catch{return{}}}export function readLockFile(){return readLockFileWithWriter(writeLockFile)}function writeLockFileWithFs(q,K=J){const Q=getLockFilePath();K.mkdirSync(B.dirname(Q),{recursive:!0});const V=L(Q,"tmp");try{K.writeFileSync(V,JSON.stringify(q,null,2)+`
2
- `);K.renameSync(V,Q)}catch(Y){try{K.rmSync(V,{force:!0})}catch{}throw Y}}export function writeLockFile(q){writeLockFileWithFs(q,J)}export function getCommitHash(q){try{return S("git",["rev-parse","HEAD"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return}}export function validatePluginStructure(q){const K=[];if(!J.existsSync(q))return{valid:!1,errors:["Plugin directory does not exist"]};const Q=J.readdirSync(q),V=Q.some((Z)=>Z.endsWith(".ts")&&!Z.endsWith(".d.ts")&&!Z.endsWith(".test.ts")),Y=Q.some((Z)=>Z.endsWith(".js")&&!Z.endsWith(".d.js"));if(!V&&!Y)K.push("No command files found in plugin directory. A plugin must contain at least one .ts or .js command file.");if(V){const Z=B.join(q,"package.json");if(!J.existsSync(Z))K.push('Plugin contains .ts files but no package.json. A package.json with "type": "module" and "@jackwener/opencli" peer dependency is required for TS plugins.');else try{if(JSON.parse(J.readFileSync(Z,"utf-8")).type!=="module")K.push('Plugin package.json must have "type": "module" for TypeScript plugins.')}catch{K.push("Plugin package.json is malformed or invalid JSON.")}}return{valid:K.length===0,errors:K}}function Xq(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return!1;try{const Q=JSON.parse(J.readFileSync(K,"utf-8"));return Q.dependencies!=null&&Object.keys(Q.dependencies).length>0}catch{return!1}}function installDependencies(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return;try{S("npm",["install","--omit=dev"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"],...N&&{shell:!0}})}catch(Q){throw new x(`npm install failed in ${q}: ${U(Q)}`,"Check your network connection and npm configuration.")}}function g(q){xq(q);Tq(q)}function o(q){installDependencies(q);g(q)}function postInstallMonorepoLifecycle(q,K){installDependencies(q);for(const Q of K){if(Q!==q&&Xq(Q))installDependencies(Q);g(Q)}}function l(q){const K=validatePluginStructure(q);if(!K.valid)throw new x(`Invalid plugin structure:
2
+ `);K.renameSync(V,Q)}catch(Y){try{K.rmSync(V,{force:!0})}catch{}throw Y}}export function writeLockFile(q){writeLockFileWithFs(q,J)}export function getCommitHash(q){try{return S("git",["rev-parse","HEAD"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()}catch{return}}export function validatePluginStructure(q){const K=[];if(!J.existsSync(q))return{valid:!1,errors:["Plugin directory does not exist"]};const Q=J.readdirSync(q),V=Q.some((Z)=>Z.endsWith(".ts")&&!Z.endsWith(".d.ts")&&!Z.endsWith(".test.ts")),Y=Q.some((Z)=>Z.endsWith(".js")&&!Z.endsWith(".d.js"));if(!V&&!Y)K.push("No command files found in plugin directory. A plugin must contain at least one .ts or .js command file.");if(V){const Z=B.join(q,"package.json");if(!J.existsSync(Z))K.push('Plugin contains .ts files but no package.json. A package.json with "type": "module" and "@jackwener/opencli" peer dependency is required for TS plugins.');else try{if(JSON.parse(J.readFileSync(Z,"utf-8")).type!=="module")K.push('Plugin package.json must have "type": "module" for TypeScript plugins.')}catch{K.push("Plugin package.json is malformed or invalid JSON.")}}return{valid:K.length===0,errors:K}}function Xq(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return!1;try{const Q=JSON.parse(J.readFileSync(K,"utf-8"));return Q.dependencies!=null&&Object.keys(Q.dependencies).length>0}catch{return!1}}function installDependencies(q){const K=B.join(q,"package.json");if(!J.existsSync(K))return;try{S("npm",["install","--omit=dev","--ignore-scripts"],{cwd:q,encoding:"utf-8",stdio:["pipe","pipe","pipe"],...N&&{shell:!0}})}catch(Q){throw new x(`npm install failed in ${q}: ${U(Q)}`,"Check your network connection and npm configuration.")}}function g(q){xq(q);Tq(q)}function o(q){installDependencies(q);g(q)}function postInstallMonorepoLifecycle(q,K){installDependencies(q);for(const Q of K){if(Q!==q&&Xq(Q))installDependencies(Q);g(Q)}}function l(q){const K=validatePluginStructure(q);if(!K.valid)throw new x(`Invalid plugin structure:
3
3
  - ${K.errors.join(`
4
4
  - `)}`);o(q)}function R(q,K,Q){q[K]={...Q,installedAt:Q.installedAt??new Date().toISOString()}}function r(q,K,Q){s((V)=>{V.track(n(q,K));Q(getCommitHash(K))})}function t(q,K,Q,V,Y){s((Z)=>{if(V){J.mkdirSync(V.parentDir,{recursive:!0});Z.track(n(V.stagingDir,q))}const X=getCommitHash(q);for(const $ of Q){const A=B.join(K,$.name),z=C(q,$.subPath);Z.track($q(z,A))}Y?.(X)})}export function installPlugin(q){const K=parseSource(q);if(!K)throw Error(`Invalid plugin source: "${q}"
5
5
  Supported formats:
@@ -54,4 +54,4 @@ export declare function parseJsonOrThrowLoginWall(response: Response, opts?: {
54
54
  * return res;
55
55
  *
56
56
  * The Node side then calls `throwIfLoginWall(res, { url })` on the result. */
57
- export declare const BROWSER_JSON_SNIFF_FN = "\nasync function fetchJsonOrLoginWall(input, init) {\n const r = await fetch(input, init);\n const contentType = r.headers.get('content-type') || '';\n const text = await r.text();\n const trimmed = text.replace(/^\\s+/, '');\n const looksLikeHtml =\n contentType.toLowerCase().includes('text/html')\n || trimmed.startsWith('<!DOCTYPE')\n || trimmed.startsWith('<!doctype')\n || trimmed.startsWith('<html')\n || trimmed.startsWith('<HTML');\n if (looksLikeHtml) {\n return {\n __loginWall: true,\n status: r.status,\n url: r.url || (typeof input === 'string' ? input : ''),\n contentType,\n bodyPreview: trimmed.slice(0, 100),\n };\n }\n if (!r.ok) {\n return { error: r.status };\n }\n try {\n return JSON.parse(text);\n } catch (err) {\n throw new Error(\n 'JSON parse failed (status=' + r.status + ', body[0..50]=' + JSON.stringify(trimmed.slice(0, 50)) + '): '\n + (err && err.message ? err.message : String(err))\n );\n }\n}\n";
57
+ export declare const BROWSER_JSON_SNIFF_FN = "\nasync function fetchJsonOrLoginWall(input, init) {\n const r = await fetch(input, init);\n const contentType = r.headers.get('content-type') || '';\n const text = await r.text();\n const trimmed = text.replace(/^\\s+/, '');\n const looksLikeHtml =\n contentType.toLowerCase().includes('text/html')\n || /^<(?:!doctype|html|head|body|title)(?:[\\s>/]|$)/i.test(trimmed);\n if (looksLikeHtml) {\n return {\n __loginWall: true,\n status: r.status,\n url: r.url || (typeof input === 'string' ? input : ''),\n contentType,\n bodyPreview: trimmed.slice(0, 100),\n };\n }\n if (!r.ok) {\n return { error: r.status };\n }\n try {\n return JSON.parse(text);\n } catch (err) {\n throw new Error(\n 'JSON parse failed (status=' + r.status + ', body[0..50]=' + JSON.stringify(trimmed.slice(0, 50)) + '): '\n + (err && err.message ? err.message : String(err))\n );\n }\n}\n";
package/dist/src/utils.js CHANGED
@@ -2,7 +2,7 @@ import*as X from"node:fs";import*as Z from"node:path";import C from"turndown";im
2
2
  `});if(q)q(z);return z}export function htmlToMarkdown(q,z){return createMarkdownConverter(z).turndown(q||"").replace(/\u00a0/g," ").replace(/\n{4,}/g,`
3
3
 
4
4
 
5
- `).replace(/[ \t]+$/gm,"").trim()}function H(q){return typeof q==="object"&&q!==null&&q.__loginWall===!0&&typeof q.status==="number"}export function throwIfLoginWall(q,z={}){if(H(q))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",q.bodyPreview);return q}export async function parseJsonOrThrowLoginWall(q,z={}){const D=q.headers.get("content-type")||"",G=await q.text(),A=G.trimStart();if(D.toLowerCase().includes("text/html")||A.startsWith("<!DOCTYPE")||A.startsWith("<!doctype")||A.startsWith("<html")||A.startsWith("<HTML"))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",A.slice(0,100));try{return JSON.parse(G)}catch(K){throw Error(`JSON parse failed (status=${q.status}, body[0..50]=${JSON.stringify(A.slice(0,50))}): `+(K instanceof Error?K.message:String(K)))}}export const BROWSER_JSON_SNIFF_FN=`
5
+ `).replace(/[ \t]+$/gm,"").trim()}function H(q){return typeof q==="object"&&q!==null&&q.__loginWall===!0&&typeof q.status==="number"}export function throwIfLoginWall(q,z={}){if(H(q))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",q.bodyPreview);return q}export async function parseJsonOrThrowLoginWall(q,z={}){const D=q.headers.get("content-type")||"",G=await q.text(),A=G.trimStart();if(D.toLowerCase().includes("text/html")||/^<(?:!doctype|html|head|body|title)(?:[\s>/]|$)/i.test(A))throw new Y(`Server returned HTML instead of JSON (status=${q.status}). Likely a login wall, rate limit, or WAF challenge.`,q.status,z.url||q.url||"",A.slice(0,100));try{return JSON.parse(G)}catch(K){throw Error(`JSON parse failed (status=${q.status}, body[0..50]=${JSON.stringify(A.slice(0,50))}): `+(K instanceof Error?K.message:String(K)))}}export const BROWSER_JSON_SNIFF_FN=`
6
6
  async function fetchJsonOrLoginWall(input, init) {
7
7
  const r = await fetch(input, init);
8
8
  const contentType = r.headers.get('content-type') || '';
@@ -10,10 +10,7 @@ async function fetchJsonOrLoginWall(input, init) {
10
10
  const trimmed = text.replace(/^\\s+/, '');
11
11
  const looksLikeHtml =
12
12
  contentType.toLowerCase().includes('text/html')
13
- || trimmed.startsWith('<!DOCTYPE')
14
- || trimmed.startsWith('<!doctype')
15
- || trimmed.startsWith('<html')
16
- || trimmed.startsWith('<HTML');
13
+ || /^<(?:!doctype|html|head|body|title)(?:[\\s>/]|$)/i.test(trimmed);
17
14
  if (looksLikeHtml) {
18
15
  return {
19
16
  __loginWall: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false