publishport-opencli 1.8.5-pp.26 → 1.8.5-pp.28

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/cli-manifest.json CHANGED
@@ -11126,6 +11126,43 @@
11126
11126
  "modulePath": "devto/read.js",
11127
11127
  "sourceFile": "devto/read.js"
11128
11128
  },
11129
+ {
11130
+ "site": "devto",
11131
+ "name": "stats",
11132
+ "description": "dev.to 文章数据(每篇浏览/反应/评论等运营指标,含草稿)。首次调用要拉起浏览器会话、偏慢,客户端超时属正常,重试一次即可",
11133
+ "access": "read",
11134
+ "domain": "dev.to",
11135
+ "strategy": "cookie",
11136
+ "browser": true,
11137
+ "args": [
11138
+ {
11139
+ "name": "limit",
11140
+ "type": "number",
11141
+ "default": 20,
11142
+ "required": false,
11143
+ "help": "返回条数"
11144
+ }
11145
+ ],
11146
+ "columns": [
11147
+ "id",
11148
+ "type",
11149
+ "title",
11150
+ "url",
11151
+ "published_at",
11152
+ "views",
11153
+ "likes",
11154
+ "comments",
11155
+ "collects",
11156
+ "shares",
11157
+ "tags",
11158
+ "read_time",
11159
+ "extra"
11160
+ ],
11161
+ "type": "js",
11162
+ "modulePath": "devto/stats.js",
11163
+ "sourceFile": "devto/stats.js",
11164
+ "navigateBefore": false
11165
+ },
11129
11166
  {
11130
11167
  "site": "devto",
11131
11168
  "name": "tag",
@@ -25578,6 +25615,46 @@
25578
25615
  "sourceFile": "medium/search.js",
25579
25616
  "navigateBefore": "https://medium.com"
25580
25617
  },
25618
+ {
25619
+ "site": "medium",
25620
+ "name": "stats",
25621
+ "description": "Medium 文章数据(每篇曝光/浏览/读完/读完率/收益等运营指标)。首次调用要拉起浏览器会话、偏慢,客户端超时属正常,重试一次即可",
25622
+ "access": "read",
25623
+ "domain": "medium.com",
25624
+ "strategy": "cookie",
25625
+ "browser": true,
25626
+ "args": [
25627
+ {
25628
+ "name": "limit",
25629
+ "type": "number",
25630
+ "default": 20,
25631
+ "required": false,
25632
+ "help": "返回条数"
25633
+ }
25634
+ ],
25635
+ "columns": [
25636
+ "id",
25637
+ "type",
25638
+ "title",
25639
+ "url",
25640
+ "published_at",
25641
+ "views",
25642
+ "likes",
25643
+ "comments",
25644
+ "collects",
25645
+ "shares",
25646
+ "displays",
25647
+ "reads",
25648
+ "read_ratio",
25649
+ "earnings",
25650
+ "read_time",
25651
+ "extra"
25652
+ ],
25653
+ "type": "js",
25654
+ "modulePath": "medium/stats.js",
25655
+ "sourceFile": "medium/stats.js",
25656
+ "navigateBefore": false
25657
+ },
25581
25658
  {
25582
25659
  "site": "medium",
25583
25660
  "name": "tag",
@@ -30490,6 +30567,42 @@
30490
30567
  "sourceFile": "qiita/publish.js",
30491
30568
  "navigateBefore": "https://qiita.com"
30492
30569
  },
30570
+ {
30571
+ "site": "qiita",
30572
+ "name": "stats",
30573
+ "description": "Qiita 文章数据(每篇浏览/点赞/收藏/评论等运营指标)。逐篇补浏览数、篇数多时偏慢,客户端超时属正常,重试一次即可",
30574
+ "access": "read",
30575
+ "domain": "qiita.com",
30576
+ "strategy": "cookie",
30577
+ "browser": true,
30578
+ "args": [
30579
+ {
30580
+ "name": "limit",
30581
+ "type": "number",
30582
+ "default": 20,
30583
+ "required": false,
30584
+ "help": "返回条数"
30585
+ }
30586
+ ],
30587
+ "columns": [
30588
+ "id",
30589
+ "type",
30590
+ "title",
30591
+ "url",
30592
+ "published_at",
30593
+ "views",
30594
+ "likes",
30595
+ "comments",
30596
+ "collects",
30597
+ "shares",
30598
+ "tags",
30599
+ "extra"
30600
+ ],
30601
+ "type": "js",
30602
+ "modulePath": "qiita/stats.js",
30603
+ "sourceFile": "qiita/stats.js",
30604
+ "navigateBefore": false
30605
+ },
30493
30606
  {
30494
30607
  "site": "qiita",
30495
30608
  "name": "whoami",
@@ -0,0 +1,27 @@
1
+ import{cli as C,Strategy as F}from"@jackwener/opencli/registry";import{EmptyResultError as G}from"@jackwener/opencli/errors";const I="https://dev.to/dashboard";C({site:"devto",name:"stats",access:"read",description:"dev.to 文章数据(每篇浏览/反应/评论等运营指标,含草稿)。首次调用要拉起浏览器会话、偏慢,客户端超时属正常,重试一次即可",domain:"dev.to",strategy:F.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","tags","read_time","extra"],func:async(h,x)=>{const z=Math.max(1,Number(x.limit)||20);await h.goto(I);const f=await h.evaluate(`(async () => {
2
+ try {
3
+ if (!location.href.includes('dev.to')) {
4
+ return { error: '未登录 dev.to(页面被重定向到 ' + location.href + '),请先在 Chrome 里登录 dev.to' };
5
+ }
6
+ const limit = ${JSON.stringify(z)};
7
+ const items = [];
8
+ for (let pageNo = 1; items.length < limit && pageNo <= 50; pageNo++) {
9
+ const per = Math.min(100, limit - items.length + 1);
10
+ const resp = await fetch('/api/articles/me/all?per_page=' + per + '&page=' + pageNo, {
11
+ credentials: 'include',
12
+ headers: { accept: 'application/json' },
13
+ });
14
+ if (resp.status === 401) return { error: '未登录 dev.to:请先在 Chrome 里登录 dev.to 再运行 devto stats' };
15
+ if (!resp.ok) return { error: 'dev.to 接口 HTTP ' + resp.status + '(/api/articles/me/all 第 ' + pageNo + ' 页)' };
16
+ const batch = await resp.json();
17
+ if (!Array.isArray(batch) || batch.length === 0) break;
18
+ items.push(...batch);
19
+ if (batch.length < per) break;
20
+ }
21
+ // body_markdown 是全文、体积巨大,出页面前先剔掉
22
+ for (const it of items) { delete it.body_markdown; delete it.user; }
23
+ return { items: items.slice(0, limit) };
24
+ } catch (e) {
25
+ return { error: String((e && e.message) || e) };
26
+ }
27
+ })()`);if(!f||f.error)throw Error(f?.error||"devto stats:evaluate 无返回(浏览器会话异常)");const q=Array.isArray(f.items)?f.items:[];if(q.length===0)throw new G("devto stats","该 dev.to 账号没有任何文章(含草稿)。");return q.map((b)=>({id:String(b.id??""),type:"article",title:b.title||"",url:b.url||"",published_at:b.published_at||"",views:b.page_views_count??0,likes:b.public_reactions_count??0,comments:b.comments_count??0,collects:"",shares:"",tags:Array.isArray(b.tag_list)?b.tag_list.join("|"):String(b.tag_list||""),read_time:b.reading_time_minutes?`${b.reading_time_minutes}min`:"",extra:JSON.stringify({published:b.published??null,positive_reactions:b.positive_reactions_count??null,description:b.description||"",cover_image:b.cover_image||"",canonical_url:b.canonical_url||""})}))}});
@@ -0,0 +1,56 @@
1
+ import{cli as I,Strategy as J}from"@jackwener/opencli/registry";import{EmptyResultError as K}from"@jackwener/opencli/errors";const M="https://medium.com/me/stats?publishedAt=DESC";I({site:"medium",name:"stats",access:"read",description:"Medium 文章数据(每篇曝光/浏览/读完/读完率/收益等运营指标)。首次调用要拉起浏览器会话、偏慢,客户端超时属正常,重试一次即可",domain:"medium.com",strategy:J.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","displays","reads","read_ratio","earnings","read_time","extra"],func:async(D,G)=>{const H=Math.max(1,Number(G.limit)||20);await D.goto(M);const q=await D.evaluate(`(async () => {
2
+ try {
3
+ if (!location.href.includes('medium.com')) {
4
+ return { error: '未登录 Medium(页面被重定向到 ' + location.href + '),请先在 Chrome 里登录 medium.com' };
5
+ }
6
+ if (location.pathname.includes('/m/signin') || location.pathname === '/') {
7
+ return { error: '未登录 Medium:请先在 Chrome 里登录 medium.com 再运行 medium stats' };
8
+ }
9
+ const limit = ${JSON.stringify(H)};
10
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
11
+ const postsInCache = () => {
12
+ const c = window.__APOLLO_CLIENT__ && window.__APOLLO_CLIENT__.cache;
13
+ if (!c) return null;
14
+ const state = c.extract();
15
+ const posts = [];
16
+ for (const k of Object.keys(state)) {
17
+ if (k.startsWith('Post:') && state[k] && state[k].totalStats) posts.push(state[k]);
18
+ }
19
+ return posts;
20
+ };
21
+ // 等首屏统计数据进缓存(Apollo 客户端 + 至少一篇带 totalStats 的 Post)
22
+ let posts = null;
23
+ for (let i = 0; i < 40; i++) {
24
+ posts = postsInCache();
25
+ if (posts && posts.length > 0) break;
26
+ await sleep(500);
27
+ }
28
+ if (!window.__APOLLO_CLIENT__) return { error: 'Medium 页面没有 Apollo 客户端(页面结构可能已改版),请重试或反馈' };
29
+ if (!posts || posts.length === 0) {
30
+ // 可能确实一篇都没有——再区分:页面文案有 "You haven\\u2019t published" 之类时按空处理
31
+ return { items: [] };
32
+ }
33
+ // 滚动加载更多(stats 列表按滚动分页),直到数量不再增长或已够 limit
34
+ let stall = 0;
35
+ while (posts.length < limit && stall < 3) {
36
+ const before = posts.length;
37
+ window.scrollTo(0, document.body.scrollHeight);
38
+ await sleep(1200);
39
+ posts = postsInCache() || posts;
40
+ stall = posts.length > before ? 0 : stall + 1;
41
+ }
42
+ return { items: posts.slice(0, limit).map((p) => ({
43
+ id: p.id,
44
+ title: p.title || '',
45
+ url: p.mediumUrl || '',
46
+ publishedAt: p.firstPublishedAt || null,
47
+ stats: p.totalStats || {},
48
+ earnings: p.earnings && p.earnings.total ? p.earnings.total : null,
49
+ readingTime: p.readingTime || null,
50
+ visibility: p.visibility || '',
51
+ isLocked: p.isLocked ?? null,
52
+ })) };
53
+ } catch (e) {
54
+ return { error: String((e && e.message) || e) };
55
+ }
56
+ })()`);if(!q||q.error)throw Error(q?.error||"medium stats:evaluate 无返回(浏览器会话异常)");const z=Array.isArray(q.items)?q.items:[];if(z.length===0)throw new K("medium stats","该 Medium 账号没有任何已发布文章。");z.sort((h,x)=>(x.publishedAt||0)-(h.publishedAt||0));return z.map((h)=>{const x=h.stats||{},B=x.views??0,F=x.reads??0,C=h.earnings?Number(h.earnings.units||0)+Number(h.earnings.nanos||0)/1e9:null;return{id:h.id||"",type:"article",title:h.title||"",url:h.url||"",published_at:h.publishedAt?new Date(h.publishedAt).toISOString():"",views:B,likes:"",comments:"",collects:"",shares:"",displays:x.presentations??0,reads:F,read_ratio:B>0?`${(F/B*100).toFixed(1)}%`:"",earnings:C!=null&&C>0?`$${C.toFixed(2)}`:"",read_time:h.readingTime?`${Math.round(h.readingTime)}min`:"",extra:JSON.stringify({visibility:h.visibility||"",is_locked:h.isLocked})}})}});
@@ -0,0 +1,37 @@
1
+ import{cli as F,Strategy as G}from"@jackwener/opencli/registry";import{EmptyResultError as I,AuthRequiredError as J}from"@jackwener/opencli/errors";import{qiitaViewer as K}from"./gql.js";const L="https://qiita.com/";F({site:"qiita",name:"stats",access:"read",description:"Qiita 文章数据(每篇浏览/点赞/收藏/评论等运营指标)。逐篇补浏览数、篇数多时偏慢,客户端超时属正常,重试一次即可",domain:"qiita.com",strategy:G.COOKIE,browser:!0,navigateBefore:!1,args:[{name:"limit",type:"number",default:20,help:"返回条数"}],columns:["id","type","title","url","published_at","views","likes","comments","collects","shares","tags","extra"],func:async(v,B)=>{const C=Math.max(1,Number(B.limit)||20);await v.goto(L);const x=(await K(v))?.urlName;if(!x)throw new J("qiita.com","未登录 Qiita:请先在 Chrome 里登录 qiita.com 再运行 qiita stats");const f=await v.evaluate(`(async () => {
2
+ try {
3
+ const limit = ${JSON.stringify(C)};
4
+ const urlName = ${JSON.stringify(x)};
5
+ const items = [];
6
+ for (let pageNo = 1; items.length < limit && pageNo <= 100; pageNo++) {
7
+ const resp = await fetch('/api/v2/users/' + encodeURIComponent(urlName) + '/items?per_page=100&page=' + pageNo, {
8
+ headers: { accept: 'application/json' },
9
+ });
10
+ if (!resp.ok) return { error: 'Qiita 接口 HTTP ' + resp.status + '(/api/v2/users/' + urlName + '/items 第 ' + pageNo + ' 页)' };
11
+ const batch = await resp.json();
12
+ if (!Array.isArray(batch) || batch.length === 0) break;
13
+ for (const it of batch) { delete it.body; delete it.rendered_body; delete it.user; }
14
+ items.push(...batch);
15
+ if (batch.length < 100) break;
16
+ }
17
+ const picked = items.slice(0, limit);
18
+ // 逐篇带 cookie 拉文章页 HTML,抠作者可见的「N views」(SSR 渲染,含千分位/k 缩写)
19
+ for (const it of picked) {
20
+ try {
21
+ const r = await fetch(it.url, { credentials: 'include' });
22
+ if (!r.ok) { it.__views = null; continue; }
23
+ const html = await r.text();
24
+ const m = html.match(/aria-hidden="true">([0-9.,]+[kKmM]?)\\s*views</);
25
+ if (!m) { it.__views = null; continue; }
26
+ let v = m[1].replace(/,/g, '');
27
+ let mul = 1;
28
+ if (/k$/i.test(v)) { mul = 1000; v = v.slice(0, -1); }
29
+ else if (/m$/i.test(v)) { mul = 1000000; v = v.slice(0, -1); }
30
+ it.__views = Math.round(parseFloat(v) * mul);
31
+ } catch (e) { it.__views = null; }
32
+ }
33
+ return { items: picked };
34
+ } catch (e) {
35
+ return { error: String((e && e.message) || e) };
36
+ }
37
+ })()`);if(!f||f.error)throw Error(f?.error||"qiita stats:evaluate 无返回(浏览器会话异常)");const z=Array.isArray(f.items)?f.items:[];if(z.length===0)throw new I("qiita stats","该 Qiita 账号没有任何公开文章。");return z.map((b)=>({id:b.id||"",type:"article",title:b.title||"",url:b.url||"",published_at:b.created_at||"",views:b.__views??"",likes:b.likes_count??0,comments:b.comments_count??0,collects:b.stocks_count??0,shares:"",tags:Array.isArray(b.tags)?b.tags.map((D)=>D.name||"").filter(Boolean).join("|"):"",extra:JSON.stringify({reactions:b.reactions_count??null,private:b.private??null,updated_at:b.updated_at||"",organization:b.organization_url_name||""})}))}});
@@ -1 +1 @@
1
- import{openSync as $,closeSync as g,writeSync as B,readFileSync as q,unlinkSync as V,statSync as H,mkdirSync as j}from"node:fs";import{join as G}from"node:path";import{homedir as v}from"node:os";const J=G(v(),".opencli"),W=G(J,"browser-adapter-session.lock");function w(x){if(!x)return W;const b=x.replace(/[^A-Za-z0-9_-]/g,"_");return G(J,`browser-adapter-session.${b}.lock`)}const N=180000,X=600000,D=60;function Q(x){return new Promise((b)=>setTimeout(b,x))}function m(x){try{process.kill(x,0);return!0}catch(b){return b?.code==="EPERM"}}function u(x){try{const b=parseInt(q(x,"utf8").trim(),10);return Number.isFinite(b)?b:null}catch{return null}}function Y(x){try{const b=$(x,"wx");try{B(b,String(process.pid))}finally{g(b)}return!0}catch(b){if(b?.code==="EEXIST")return!1;throw b}}function F(x){try{const b=u(x),z=Date.now()-H(x).mtimeMs;if(b!==null&&!m(b)||z>X)V(x)}catch{}}export async function withBrowserSessionLock(x,b){try{j(J,{recursive:!0})}catch{}const z=w(b),Z=Date.now()+N;while(!Y(z)){F(z);if(Y(z))break;if(Date.now()>=Z)throw Error(`browser session lock busy: waited ${Math.round(N/1000)}s for another browser command to finish`);await Q(D)}try{return await x()}finally{try{V(z)}catch{}}}export async function withBrowserSessionLockIf(x,b,z){return x?withBrowserSessionLock(b,z):b()}export const __lockInternals={LOCK_PATH:W,ACQUIRE_TIMEOUT_MS:N,STALE_AFTER_MS:X};
1
+ import{openSync as q,closeSync as H,writeSync as b,readFileSync as j,unlinkSync as Y,statSync as v,mkdirSync as D,utimesSync as Q}from"node:fs";import{join as N}from"node:path";import{homedir as F}from"node:os";const V=N(F(),".opencli"),Z=N(V,"browser-adapter-session.lock");function m(z){if(!z)return Z;const x=z.replace(/[^A-Za-z0-9_-]/g,"_");return N(V,`browser-adapter-session.${x}.lock`)}const W=180000,$=600000,u=60;function w(z){return new Promise((x)=>setTimeout(x,z))}function K(z){try{process.kill(z,0);return!0}catch(x){return x?.code==="EPERM"}}function U(z){try{const x=parseInt(j(z,"utf8").trim(),10);return Number.isFinite(x)?x:null}catch{return null}}function g(z){try{const x=q(z,"wx");try{b(x,String(process.pid))}finally{H(x)}return!0}catch(x){if(x?.code==="EEXIST")return!1;throw x}}function C(z){try{const x=U(z),G=Date.now()-v(z).mtimeMs;if(x!==null&&!K(x)||G>$)Y(z)}catch{}}export async function withBrowserSessionLock(z,x){try{D(V,{recursive:!0})}catch{}const G=m(x),B=Date.now()+W;while(!g(G)){C(G);if(g(G))break;if(Date.now()>=B)throw Error(`browser session lock busy: waited ${Math.round(W/1000)}s for another browser command to finish`);await w(u)}const J=setInterval(()=>{try{const X=new Date;Q(G,X,X)}catch{}},60000);if(typeof J.unref==="function")J.unref();try{return await z()}finally{clearInterval(J);try{Y(G)}catch{}}}export async function withBrowserSessionLockIf(z,x,G){return z?withBrowserSessionLock(x,G):x()}export const __lockInternals={LOCK_PATH:Z,ACQUIRE_TIMEOUT_MS:W,STALE_AFTER_MS:$};
@@ -1,3 +1,3 @@
1
- import{mkdir as q,readFile as N,writeFile as H}from"node:fs/promises";import{homedir as P}from"node:os";import{dirname as b,join as _}from"node:path";import{pathToFileURL as u}from"node:url";import{InvalidArgumentError as I,Option as l}from"commander";import{AuthRequiredError as F,CliError as x,getErrorMessage as v}from"../errors.js";import{executeCommand as Q}from"../execution.js";import{fullName as n,getRegistry as T}from"../registry.js";import{render as C}from"../output.js";const z=1,U=86400000;function j(B,D,G){if(B===void 0||B===null||B==="")return G;const X=Number(B);if(!Number.isInteger(X)||X<=0)throw new I(`${D} must be a positive integer. Received: "${String(B)}"`);return X}function E(B){if(!B||!B.trim())return null;const D=B.split(",").map((G)=>G.trim()).filter(Boolean);return D.length>0?new Set(D):null}function p(){return _(P(),".opencli","auth-refresh.json")}function i(){return{version:z,sites:{}}}async function m(B){try{const D=JSON.parse(await N(B,"utf8"));if(D&&D.version===z&&D.sites&&typeof D.sites==="object")return{version:z,sites:D.sites}}catch(D){if(D.code!=="ENOENT")throw D}return i()}async function c(B,D){await q(b(B),{recursive:!0});await H(B,`${JSON.stringify(D,null,2)}
2
- `,"utf8")}function k(B){if(!B?.last_touched_at)return null;const D=Date.parse(B.last_touched_at);return Number.isFinite(D)?D:null}function a(B,D){const G=k(B);return G!==null&&D.getTime()-G<U}function L(B){const D=k(B);return D===null?"":new Date(D+U).toISOString()}function A(){const B=new Set;return[...T().values()].filter((D)=>{if(B.has(D))return!1;B.add(D);return D.name==="whoami"&&D.access==="read"}).sort((D,G)=>D.site.localeCompare(G.site))}async function V(B){const D=B;if(!D._lazy||!D._modulePath)return B;await import(u(D._modulePath).href);return T().get(n(B))??B}function M(B,D){const G=B.args.some((X)=>X.name==="timeout");return{...B,args:G?B.args:[...B.args,{name:"timeout",type:"int",default:D,help:"Per-site auth command timeout in seconds"}]}}function d(B,D){if(B.browser!==!0||typeof B.authStatus?.quickCheck!=="function")return null;return M({...B,func:B.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},D)}function R(B){if(typeof B==="boolean")return B;if(B&&typeof B==="object"&&!Array.isArray(B)){const D=B.logged_in;if(typeof D==="boolean")return D}return null}function S(B){if(B===void 0||B===null)return"";if(typeof B==="string"||typeof B==="number"||typeof B==="boolean")return String(B);return""}function w(B){if(!B||typeof B!=="object"||Array.isArray(B))return"";const D=B,G=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const X of["username","handle","user_id","id","name","nickname","user_type","url"]){if(G.test(X))continue;const J=S(D[X]);if(J)return J}for(const[X,J]of Object.entries(D)){if(X==="site"||X==="logged_in"||G.test(X))continue;const Y=S(J);if(Y)return Y}return""}function O(B){return B instanceof F||typeof B==="object"&&B!==null&&B.code==="AUTH_REQUIRED"}function f(B,D,G){if(O(G))return{site:B,status:"not_logged_in",logged_in:!1,identity:"",checked:D,error:""};const X=G instanceof x?G.code:"",J=v(G);return{site:B,status:"error",logged_in:"",identity:"",checked:D,error:X?`${X}: ${J}`:J}}function o(B,D){if(B.browser!==!0)return null;let G=B.authStatus?.refresh;if(typeof G!=="function"){const X=B.authStatus?.quickCheck;if(typeof X!=="function"||!B.domain)return null;const J=B.domain.startsWith("http://")||B.domain.startsWith("https://")?B.domain:`https://${B.domain}`;G=async(Y,Z,$)=>{await Y.goto(J);await Y.wait(1);if(R(await X(Y,Z,$))!==!0)throw new F(B.domain??B.site,`Auth refresh quickCheck failed for ${B.site}`);return{status:"touched"}}}return M({...B,func:G,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},D)}function s(B){if(B&&typeof B==="object"&&!Array.isArray(B)){const D=B;if(D.status==="refreshed"||D.refreshed===!0)return"refreshed"}return"touched"}function r(B,D,G){if(O(G))return{site:B,status:"not_logged_in",last_touched_at:D?.last_touched_at??"",next_refresh_at:L(D),error:""};const X=G instanceof x?G.code:"",J=v(G);return{site:B,status:"error",last_touched_at:D?.last_touched_at??"",next_refresh_at:L(D),error:X?`${X}: ${J}`:J}}function y(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}function g(B){return _(P(),".opencli","sites",B,"quickcheck-cache.json")}async function t(B,D,G){try{const J=JSON.parse(await N(g(B),"utf8"))?.[G??"default"];if(!J||J.logged_in!==!0)return!1;const Y=Date.now()-Date.parse(J.checked_at);return Number.isFinite(Y)&&Y>=0&&Y<D*1000}catch{return!1}}async function e(B,D,G){const X=g(B),J=G??"default";let Y={};try{Y=JSON.parse(await N(X,"utf8"))}catch{}if(D)Y[J]={logged_in:!0,checked_at:new Date().toISOString()};else delete Y[J];try{await q(b(X),{recursive:!0});await H(X,`${JSON.stringify(Y,null,2)}
3
- `,"utf8")}catch{}}async function BB(B,D){try{const G=await V(B);if(G.browser!==!0){const $=M(G,D.timeoutSeconds),W=await Q($,{timeout:D.timeoutSeconds},!1,{...D.profile?{profile:D.profile}:{}});return{site:B.site,status:"logged_in",logged_in:!0,identity:w(W),checked:"quick",error:""}}const X=G.authStatus?.quickCheckCacheTtlSeconds;if(typeof X==="number"&&X>0&&await t(B.site,X,D.profile))return{site:B.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};const J=d(G,D.timeoutSeconds);if(!J)return{site:B.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const Y=await Q(J,{timeout:D.timeoutSeconds},!1,{...y(),windowMode:"background",...D.profile?{profile:D.profile}:{}}),Z=R(Y);if(typeof X==="number"&&X>0&&typeof Z==="boolean")await e(B.site,Z,D.profile);if(Z===!0)return{site:B.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(Z===!1)return{site:B.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:B.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(G){return f(B.site,"quick",G)}}async function DB(B,D){try{const G=await V(B),X=M(G,D.timeoutSeconds),J=await Q(X,{timeout:D.timeoutSeconds},!1,{...y(),windowMode:"background",...D.profile?{profile:D.profile}:{}});return{site:B.site,status:"logged_in",logged_in:!0,identity:w(J),checked:"full",error:""}}catch(G){return f(B.site,"full",G)}}function GB(B,D){return D?`${B}@${D}`:B}async function JB(B,D){const G=GB(B.site,D.profile),X=D.state.sites[G];if(!D.force&&a(X,D.now))return{site:B.site,status:"skipped",last_touched_at:X?.last_touched_at??"",next_refresh_at:L(X),error:""};const J=D.now.toISOString();try{const Y=await V(B),Z=o(Y,D.timeoutSeconds);if(!Z){D.state.sites[G]={...X,last_attempt_at:J,last_status:"unsupported"};return{site:B.site,status:"unsupported",last_touched_at:X?.last_touched_at??"",next_refresh_at:L(X),error:"refresh probe is not available for this site"}}const $=await Q(Z,{timeout:D.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...D.profile?{profile:D.profile}:{}}),W=s($);D.state.sites[G]={...X,last_attempt_at:J,last_touched_at:J,last_status:W};return{site:B.site,status:W,last_touched_at:J,next_refresh_at:new Date(D.now.getTime()+U).toISOString(),error:""}}catch(Y){const Z=O(Y)?"not_logged_in":"error";D.state.sites[G]={...X,last_attempt_at:J,last_status:Z};return r(B.site,X,Y)}}async function h(B,D,G){const X=Array(B.length);let J=0;const Y=Array.from({length:Math.min(D,B.length)},async()=>{while(J<B.length){const Z=J++;X[Z]=await G(B[Z])}});await Promise.all(Y);return X}export async function collectAuthStatus(B){const D=E(B.sites),G=B.full?"full":"quick",X=j(B.concurrency,"--concurrency",G==="full"?3:8),J=j(B.timeout,"--timeout",G==="full"?20:8),Y=String(B.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(Y))throw new I("--only must be one of: all, logged-in, not-logged-in, unknown, error");const Z=A().filter((K)=>!D||D.has(K.site)),$=await h(Z,X,(K)=>G==="full"?DB(K,{timeoutSeconds:J,profile:B.profile}):BB(K,{timeoutSeconds:J,profile:B.profile})),W=Y.replace(/-/g,"_");return W==="all"?$:$.filter((K)=>K.status===W)}export async function collectAuthRefresh(B){const D=E(B.sites),G=j(B.concurrency,"--concurrency",3),X=j(B.timeout,"--timeout",20),J=B.statePath??p(),Y=B.now??new Date,Z=await m(J),$=A().filter((K)=>!D||D.has(K.site)),W=await h($,G,(K)=>JB(K,{timeoutSeconds:X,profile:B.profile,now:Y,state:Z,force:B.all===!0}));await c(J,Z);return W}export function registerAuthCommands(B){const D=B.command("auth").description("Inspect website login status"),G=D.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").addOption(new l("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(J)=>{const Y=typeof G.optsWithGlobals==="function"?G.optsWithGlobals():{},Z=await collectAuthStatus({sites:J.site,full:J.full===!0,concurrency:J.concurrency,timeout:J.timeout,only:J.only,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),$=typeof J.format==="string"?J.format:"table";C(Z,{fmt:$,fmtExplicit:G.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"opencli/auth status",source:J.full?"full whoami probe":"quick auth check"})}),X=D.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(J)=>{const Y=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},Z=await collectAuthRefresh({sites:J.site,all:J.all===!0,concurrency:J.concurrency,timeout:J.timeout,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),$=typeof J.format==="string"?J.format:"table";C(Z,{fmt:$,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"opencli/auth refresh",source:J.all?"forced persistent touch":"persistent touch with 24h throttle"})});return D}
1
+ import{mkdir as I,readFile as U,writeFile as _}from"node:fs/promises";import{homedir as F}from"node:os";import{dirname as x,join as b}from"node:path";import{pathToFileURL as p}from"node:url";import{InvalidArgumentError as v,Option as i}from"commander";import{AuthRequiredError as T,CliError as C,getErrorMessage as V}from"../errors.js";import{executeCommand as N}from"../execution.js";import{ensureBrowserBridgeReady as a}from"../browser/daemon-lifecycle.js";import{resolveProfile as m}from"../browser/profile.js";import{isElectronApp as j}from"../electron-apps.js";import{fullName as c,getRegistry as k}from"../registry.js";import{render as A}from"../output.js";const O=1,q=86400000;function z(G,J,X){if(G===void 0||G===null||G==="")return X;const Z=Number(G);if(!Number.isInteger(Z)||Z<=0)throw new v(`${J} must be a positive integer. Received: "${String(G)}"`);return Z}function E(G){if(!G||!G.trim())return null;const J=G.split(",").map((X)=>X.trim()).filter(Boolean);return J.length>0?new Set(J):null}function d(){return b(F(),".opencli","auth-refresh.json")}function o(){return{version:O,sites:{}}}async function s(G){try{const J=JSON.parse(await U(G,"utf8"));if(J&&J.version===O&&J.sites&&typeof J.sites==="object")return{version:O,sites:J.sites}}catch(J){if(J.code!=="ENOENT")throw J}return o()}async function r(G,J){await I(x(G),{recursive:!0});await _(G,`${JSON.stringify(J,null,2)}
2
+ `,"utf8")}function S(G){if(!G?.last_touched_at)return null;const J=Date.parse(G.last_touched_at);return Number.isFinite(J)?J:null}function t(G,J){const X=S(G);return X!==null&&J.getTime()-X<q}function B(G){const J=S(G);return J===null?"":new Date(J+q).toISOString()}function R(){const G=new Set;return[...k().values()].filter((J)=>{if(G.has(J))return!1;G.add(J);return J.name==="whoami"&&J.access==="read"}).sort((J,X)=>J.site.localeCompare(X.site))}async function H(G){const J=G;if(!J._lazy||!J._modulePath)return G;await import(p(J._modulePath).href);return k().get(c(G))??G}function M(G,J){const X=G.args.some((Z)=>Z.name==="timeout");return{...G,args:X?G.args:[...G.args,{name:"timeout",type:"int",default:J,help:"Per-site auth command timeout in seconds"}]}}function e(G,J){if(G.browser!==!0||typeof G.authStatus?.quickCheck!=="function")return null;return M({...G,func:G.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},J)}function w(G){if(typeof G==="boolean")return G;if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G.logged_in;if(typeof J==="boolean")return J}return null}function f(G){if(G===void 0||G===null)return"";if(typeof G==="string"||typeof G==="number"||typeof G==="boolean")return String(G);return""}function y(G){if(!G||typeof G!=="object"||Array.isArray(G))return"";const J=G,X=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const Z of["username","handle","user_id","id","name","nickname","user_type","url"]){if(X.test(Z))continue;const Y=f(J[Z]);if(Y)return Y}for(const[Z,Y]of Object.entries(J)){if(Z==="site"||Z==="logged_in"||X.test(Z))continue;const $=f(Y);if($)return $}return""}function P(G){return G instanceof T||typeof G==="object"&&G!==null&&G.code==="AUTH_REQUIRED"}function g(G,J,X){if(P(X))return{site:G,status:"not_logged_in",logged_in:!1,identity:"",checked:J,error:""};const Z=X instanceof C?X.code:"",Y=V(X);return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:Z?`${Z}: ${Y}`:Y}}function GG(G,J){if(G.browser!==!0)return null;let X=G.authStatus?.refresh;if(typeof X!=="function"){const Z=G.authStatus?.quickCheck;if(typeof Z!=="function"||!G.domain)return null;const Y=G.domain.startsWith("http://")||G.domain.startsWith("https://")?G.domain:`https://${G.domain}`;X=async($,D,K)=>{await $.goto(Y);await $.wait(1);if(w(await Z($,D,K))!==!0)throw new T(G.domain??G.site,`Auth refresh quickCheck failed for ${G.site}`);return{status:"touched"}}}return M({...G,func:X,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},J)}function JG(G){if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G;if(J.status==="refreshed"||J.refreshed===!0)return"refreshed"}return"touched"}function XG(G,J,X){if(P(X))return{site:G,status:"not_logged_in",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:""};const Z=X instanceof C?X.code:"",Y=V(X);return{site:G,status:"error",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:Z?`${Z}: ${Y}`:Y}}function h(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}function u(G){return b(F(),".opencli","sites",G,"quickcheck-cache.json")}async function YG(G,J,X){try{const Y=JSON.parse(await U(u(G),"utf8"))?.[X??"default"];if(!Y||Y.logged_in!==!0)return!1;const $=Date.now()-Date.parse(Y.checked_at);return Number.isFinite($)&&$>=0&&$<J*1000}catch{return!1}}async function ZG(G,J,X){const Z=u(G),Y=X??"default";let $={};try{$=JSON.parse(await U(Z,"utf8"))}catch{}if(J)$[Y]={logged_in:!0,checked_at:new Date().toISOString()};else delete $[Y];try{await I(x(Z),{recursive:!0});await _(Z,`${JSON.stringify($,null,2)}
3
+ `,"utf8")}catch{}}async function $G(G,J){try{const X=await H(G);if(X.browser!==!0){const K=M(X,J.timeoutSeconds),Q=await N(K,{timeout:J.timeoutSeconds},!1,{...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:y(Q),checked:"quick",error:""}}const Z=X.authStatus?.quickCheckCacheTtlSeconds;if(typeof Z==="number"&&Z>0&&await YG(G.site,Z,J.profile))return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};const Y=e(X,J.timeoutSeconds);if(!Y)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const $=await N(Y,{timeout:J.timeoutSeconds},!1,{...h(),windowMode:"background",...J.profile?{profile:J.profile}:{}}),D=w($);if(typeof Z==="number"&&Z>0&&typeof D==="boolean")await ZG(G.site,D,J.profile);if(D===!0)return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(D===!1)return{site:G.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(X){return g(G.site,"quick",X)}}async function DG(G,J){try{const X=await H(G),Z=M(X,J.timeoutSeconds),Y=await N(Z,{timeout:J.timeoutSeconds},!1,{...h(),windowMode:"background",...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:y(Y),checked:"full",error:""}}catch(X){return g(G.site,"full",X)}}function WG(G,J){return J?`${G}@${J}`:G}async function KG(G,J){const X=WG(G.site,J.profile),Z=J.state.sites[X];if(!J.force&&t(Z,J.now))return{site:G.site,status:"skipped",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:""};const Y=J.now.toISOString();try{const $=await H(G),D=GG($,J.timeoutSeconds);if(!D){J.state.sites[X]={...Z,last_attempt_at:Y,last_status:"unsupported"};return{site:G.site,status:"unsupported",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:"refresh probe is not available for this site"}}const K=await N(D,{timeout:J.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...J.profile?{profile:J.profile}:{}}),Q=JG(K);J.state.sites[X]={...Z,last_attempt_at:Y,last_touched_at:Y,last_status:Q};return{site:G.site,status:Q,last_touched_at:Y,next_refresh_at:new Date(J.now.getTime()+q).toISOString(),error:""}}catch($){const D=P($)?"not_logged_in":"error";J.state.sites[X]={...Z,last_attempt_at:Y,last_status:D};return XG(G.site,Z,$)}}async function l(G,J){const X=m(J);if(X.kind!=="extension")return null;if(!G.some((Y)=>Y.browser===!0&&!j(Y.site)))return null;try{await a({timeoutSeconds:15,contextId:X.contextId,verbose:!1});return null}catch(Y){return V(Y)}}function QG(G,J,X){return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:`BROWSER_CONNECT: ${X}`}}async function n(G,J,X){const Z=Array(G.length);let Y=0;const $=Array.from({length:Math.min(J,G.length)},async()=>{while(Y<G.length){const D=Y++;Z[D]=await X(G[D])}});await Promise.all($);return Z}export async function collectAuthStatus(G){const J=E(G.sites),X=G.full?"full":"quick",Z=z(G.concurrency,"--concurrency",X==="full"?3:8),Y=z(G.timeout,"--timeout",X==="full"?20:8),$=String(G.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes($))throw new v("--only must be one of: all, logged-in, not-logged-in, unknown, error");const D=R().filter((W)=>!J||J.has(W.site)),K=await l(D,G.profile),Q=await n(D,Z,(W)=>K&&W.browser===!0&&!j(W.site)?Promise.resolve(QG(W.site,X,K)):X==="full"?DG(W,{timeoutSeconds:Y,profile:G.profile}):$G(W,{timeoutSeconds:Y,profile:G.profile})),L=$.replace(/-/g,"_");return L==="all"?Q:Q.filter((W)=>W.status===L)}export async function collectAuthRefresh(G){const J=E(G.sites),X=z(G.concurrency,"--concurrency",3),Z=z(G.timeout,"--timeout",20),Y=G.statePath??d(),$=G.now??new Date,D=await s(Y),K=R().filter((W)=>!J||J.has(W.site)),Q=await l(K,G.profile),L=await n(K,X,(W)=>Q&&W.browser===!0&&!j(W.site)?Promise.resolve({site:W.site,status:"error",last_touched_at:D.sites[W.site]?.last_touched_at??"",next_refresh_at:B(D.sites[W.site]),error:`BROWSER_CONNECT: ${Q}`}):KG(W,{timeoutSeconds:Z,profile:G.profile,now:$,state:D,force:G.all===!0}));await r(Y,D);return L}export function registerAuthCommands(G){const J=G.command("auth").description("Inspect website login status"),X=J.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").addOption(new i("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(Y)=>{const $=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},D=await collectAuthStatus({sites:Y.site,full:Y.full===!0,concurrency:Y.concurrency,timeout:Y.timeout,only:Y.only,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),K=typeof Y.format==="string"?Y.format:"table";A(D,{fmt:K,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"opencli/auth status",source:Y.full?"full whoami probe":"quick auth check"})}),Z=J.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(Y)=>{const $=typeof Z.optsWithGlobals==="function"?Z.optsWithGlobals():{},D=await collectAuthRefresh({sites:Y.site,all:Y.all===!0,concurrency:Y.concurrency,timeout:Y.timeout,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),K=typeof Y.format==="string"?Y.format:"table";A(D,{fmt:K,fmtExplicit:Z.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"opencli/auth refresh",source:Y.all?"forced persistent touch":"persistent touch with 24h throttle"})});return J}
@@ -1 +1 @@
1
- import{createServer as P}from"node:http";import{WebSocketServer as g,WebSocket as E}from"ws";import{DEFAULT_DAEMON_PORT as u,PUBLISHPORT_DAEMON_PORT as p,unsupportedDaemonPortEnvMessage as l}from"./constants.js";import{EXIT_CODES as S}from"./errors.js";import{log as V}from"./logger.js";import{PKG_VERSION as c}from"./version.js";import{DEFAULT_CONTEXT_ID as d}from"./browser/profile.js";import{recordExtensionVersion as a}from"./update-check.js";import{buildCommandDispatchFailure as i,buildExtensionDisconnectFailure as t,getResponseCorsHeaders as n}from"./daemon-utils.js";import*as L from"./browser/trace.js";const k=u,h=p;if(process.env.OPENCLI_DAEMON_PORT){V.error(l(process.env.OPENCLI_DAEMON_PORT));process.exit(S.USAGE_ERROR)}const W=new Map,N=new Map;let y=0;const s=200,F=[];class v extends Error{errorCode;errorHint;status;constructor(J,K,z,G=400){super(J);this.errorCode=K;this.errorHint=z;this.status=G;this.name="DaemonCommandFailure"}}function o(J){F.push(J);if(F.length>s)F.shift()}function I(){return[...W.values()].filter((J)=>J.ws.readyState===E.OPEN)}function q(J){const K=typeof J==="string"&&J.trim()?J.trim():void 0;if(K){const G=W.get(K);if(G?.ws.readyState===E.OPEN)return{connection:G};return{errorCode:"profile_disconnected",error:`Browser profile "${K}" is not connected.`,errorHint:"Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>."}}const z=I();if(z.length===1)return{connection:z[0]};if(z.length>1)return{errorCode:"profile_required",error:"Multiple Browser Bridge profiles are connected; choose one with --profile.",errorHint:"Run opencli profile list, then use opencli --profile <name> ... or opencli profile use <name>."};return{errorCode:"extension_not_connected",error:"Extension not connected. Please install the opencli Browser Bridge extension."}}function r(J,K){const z=typeof K==="string"&&K.trim()?K.trim():d,G=W.get(z);if(G&&G.ws!==J)G.ws.close();const $=[...W.entries()].find(([,A])=>A.ws===J);if($&&$[0]!==z)W.delete($[0]);const Q=W.get(z),Z={contextId:z,ws:J,extensionVersion:Q?.ws===J?Q.extensionVersion:null,extensionCompatRange:Q?.ws===J?Q.extensionCompatRange:null,lastSeenAt:Date.now()};W.set(z,Z);return Z}function w(J){for(const[K,z]of W.entries()){if(z.ws!==J)continue;W.delete(K);L.traceExtDisconnect(K);for(const[G,$]of N){if($.contextId!==K)continue;clearTimeout($.timer);const Q=t({contextId:K,action:$.action,dispatched:$.dispatched});if(Q.countAsCommandResultUnknown){y++;V.warn(`[daemon] Command result unknown after extension disconnect (id=${G}, action=${$.action}, context=${K})`)}L.traceResult(G,!1,Date.now()-$.dispatchTs,Q.errorCode);$.reject(new v(Q.message,Q.errorCode,Q.errorHint,Q.status));N.delete(G)}}}const x=33554432;class R extends Error{status=413;constructor(){super(`请求体超过 ${x/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function e(J){return new Promise((K,z)=>{const G=[];let $=0,Q=!1;J.on("data",(Z)=>{if(Q)return;$+=Z.length;if($>x){Q=!0;G.length=0;z(new R);return}G.push(Z)});J.on("end",()=>{if(!Q)K(Buffer.concat(G).toString("utf-8"))});J.on("error",(Z)=>{if(!Q)z(Z)})})}function Y(J,K,z,G){J.writeHead(K,{"Content-Type":"application/json",...G});J.end(JSON.stringify(z))}async function f(J,K){const z=J.headers.origin;if(z&&!z.startsWith("chrome-extension://")){Y(K,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(J.method==="OPTIONS"){K.writeHead(204);K.end();return}const G=J.url??"/",$=G.split("?")[0];if(J.method==="GET"&&$==="/ping"){Y(K,200,{ok:!0},n($,z));return}if(!J.headers["x-opencli"]){Y(K,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(J.method==="GET"&&$==="/status"){const Q=process.uptime(),Z=process.memoryUsage(),B=new URL(G,`http://localhost:${k}`).searchParams.get("contextId")?.trim()||void 0,X=q(B),_=I().map((M)=>({contextId:M.contextId,extensionConnected:!0,extensionVersion:M.extensionVersion??void 0,extensionCompatRange:M.extensionCompatRange??void 0,pending:[...N.values()].filter((H)=>H.contextId===M.contextId).length,lastSeenAt:M.lastSeenAt}));Y(K,200,{ok:!0,pid:process.pid,uptime:Q,daemonVersion:c,extensionConnected:!!X.connection,extensionVersion:X.connection?.extensionVersion??void 0,extensionCompatRange:X.connection?.extensionCompatRange??void 0,contextId:X.connection?.contextId??B,profileRequired:X.errorCode==="profile_required",profileDisconnected:X.errorCode==="profile_disconnected",profiles:_,pending:N.size,commandResultUnknown:y,memoryMB:Math.round(Z.rss/1024/1024*10)/10,port:k});return}if(J.method==="GET"&&$==="/logs"){const Z=new URL(G,`http://localhost:${k}`).searchParams.get("level"),A=Z?F.filter((B)=>B.level===Z):F;Y(K,200,{ok:!0,logs:A});return}if(J.method==="DELETE"&&$==="/logs"){F.length=0;Y(K,200,{ok:!0});return}if(J.method==="POST"&&$==="/shutdown"){Y(K,200,{ok:!0,message:"Shutting down"});setTimeout(()=>C(),100);return}if(J.method==="POST"&&G==="/command"){try{const Q=JSON.parse(await e(J));if(!Q.id){Y(K,400,{ok:!1,error:"Missing command id"});return}const Z=q(typeof Q.contextId==="string"?Q.contextId:void 0);if(!Z.connection){Y(K,Z.errorCode==="profile_required"?409:503,{id:Q.id,ok:!1,errorCode:Z.errorCode,error:Z.error,...Z.errorHint?{errorHint:Z.errorHint}:{}});return}const A=typeof Q.timeout==="number"&&Q.timeout>0?Q.timeout*1000:120000;if(N.has(Q.id)){Y(K,409,{id:Q.id,ok:!1,error:"Duplicate command id already pending; retry"});return}const B=await new Promise((X,_)=>{const M=setTimeout(()=>{N.delete(Q.id);L.traceTimeout(Q.id,A,typeof Q.action==="string"?Q.action:void 0);_(Error(`Command timeout (${A/1000}s)`))},A),H={contextId:Z.connection.contextId,action:typeof Q.action==="string"?Q.action:"unknown",dispatched:!1,dispatchTs:Date.now(),resolve:X,reject:_,timer:M};N.set(Q.id,H);const b=(U)=>{if(N.get(Q.id)!==H)return;const D=i(H.contextId);clearTimeout(M);N.delete(Q.id);_(new v(D.message,D.errorCode,D.errorHint,D.status));V.warn(`[daemon] Failed to dispatch command ${Q.id}: ${U instanceof Error?U.message:String(U)}`)};try{Z.connection.ws.send(JSON.stringify(Q),(U)=>{if(U&&!H.dispatched)b(U)});H.dispatched=!0;L.traceDispatch(Q)}catch(U){b(U)}});Y(K,200,B)}catch(Q){const Z=Q instanceof v?Q:null;if(Q instanceof R){Y(K,413,{ok:!1,error:Q.message,errorCode:"body_too_large"});return}Y(K,Z?.status??(Q instanceof Error&&Q.message.includes("timeout")?408:400),{ok:!1,error:Q instanceof Error?Q.message:"Invalid request",...Z?.errorCode?{errorCode:Z.errorCode}:{},...Z?.errorHint?{errorHint:Z.errorHint}:{}})}return}Y(K,404,{error:"Not found"})}const O=P((J,K)=>{f(J,K).catch(()=>{K.writeHead(500);K.end()})}),T=P((J,K)=>{f(J,K).catch(()=>{K.writeHead(500);K.end()})}),j=new g({noServer:!0});function m(J,K,z){if((J.url??"/").split("?")[0]!=="/ext"){K.destroy();return}const $=J.headers.origin;if($&&!$.startsWith("chrome-extension://")){K.destroy();return}j.handleUpgrade(J,K,z,(Q)=>{j.emit("connection",Q,J)})}O.on("upgrade",m);T.on("upgrade",m);j.on("connection",(J)=>{V.info("[daemon] Extension connected");let K=0;const z=setInterval(()=>{if(J.readyState!==E.OPEN){clearInterval(z);return}if(K>=2){V.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(z);J.terminate();return}K++;J.ping()},15000);J.on("pong",()=>{K=0});J.on("message",(G)=>{try{const $=JSON.parse(G.toString());if($.type==="hello"){const Z=r(J,$.contextId);Z.extensionVersion=typeof $.version==="string"?$.version:null;Z.extensionCompatRange=typeof $.compatRange==="string"?$.compatRange:null;Z.lastSeenAt=Date.now();if(Z.extensionVersion)a(Z.extensionVersion);V.info(`[daemon] Extension profile connected: ${Z.contextId}`);L.traceExtConnect(Z.contextId,Z.extensionVersion);return}if($.type==="log"){if($.level==="error")V.error(`[ext] ${$.msg}`);else if($.level==="warn")V.warn(`[ext] ${$.msg}`);else V.info(`[ext] ${$.msg}`);o({level:$.level,msg:$.msg,ts:$.ts??Date.now()});L.traceExtLog($.level,$.msg);return}const Q=N.get($.id);if(Q){clearTimeout(Q.timer);N.delete($.id);L.traceResult($.id,!!$.ok,Date.now()-Q.dispatchTs,typeof $.errorCode==="string"?$.errorCode:void 0,typeof $.page==="string"?$.page:void 0);Q.resolve($)}}catch($){const Q=G.toString().slice(0,200);V.warn(`[daemon] Ignoring malformed WS message from extension: ${$ instanceof Error?$.message:String($)} (first 200 chars: ${JSON.stringify(Q)})`)}});J.on("close",()=>{V.info("[daemon] Extension disconnected");clearInterval(z);w(J)});J.on("error",()=>{clearInterval(z);w(J)})});O.listen(k,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${k}`)});O.on("error",(J)=>{if(J.code==="EADDRINUSE"){V.error(`[daemon] Port ${k} already in use — another daemon is likely running. Exiting.`);process.exit(S.SERVICE_UNAVAIL)}V.error(`[daemon] Server error: ${J.message}`);process.exit(S.GENERIC_ERROR)});T.listen(h,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${h} (PublishPort extension)`)});T.on("error",(J)=>{V.warn(`[daemon] PublishPort port ${h} unavailable (${J.message}) — extension will fall back to ${k}`)});function C(){for(const[,J]of N){clearTimeout(J.timer);J.reject(Error("Daemon shutting down"))}N.clear();for(const J of W.values())J.ws.close();O.close();T.close();process.exit(S.SUCCESS)}process.on("SIGTERM",C);process.on("SIGINT",C);
1
+ import{createServer as b}from"node:http";import{WebSocketServer as g,WebSocket as v}from"ws";import{DEFAULT_DAEMON_PORT as u,PUBLISHPORT_DAEMON_PORT as p,unsupportedDaemonPortEnvMessage as l}from"./constants.js";import{EXIT_CODES as S}from"./errors.js";import{log as V}from"./logger.js";import{PKG_VERSION as c}from"./version.js";import{DEFAULT_CONTEXT_ID as d}from"./browser/profile.js";import{recordExtensionVersion as a}from"./update-check.js";import{buildCommandDispatchFailure as i,buildExtensionDisconnectFailure as t,getResponseCorsHeaders as n}from"./daemon-utils.js";import*as L from"./browser/trace.js";const B=u,h=p;if(process.env.OPENCLI_DAEMON_PORT){V.error(l(process.env.OPENCLI_DAEMON_PORT));process.exit(S.USAGE_ERROR)}const W=new Map,N=new Map;let I=0;const s=200,_=[];class E extends Error{errorCode;errorHint;status;constructor(J,K,z,G=400){super(J);this.errorCode=K;this.errorHint=z;this.status=G;this.name="DaemonCommandFailure"}}function o(J){_.push(J);if(_.length>s)_.shift()}function j(){return[...W.values()].filter((J)=>J.ws.readyState===v.OPEN)}function q(J){const K=typeof J==="string"&&J.trim()?J.trim():void 0;if(K){const G=W.get(K);if(G?.ws.readyState===v.OPEN)return{connection:G};const Q=j();if(Q.length===1){V.warn(`[daemon] Requested profile "${K}" not connected; falling back to the only connected profile "${Q[0].contextId}"`);return{connection:Q[0]}}return{errorCode:"profile_disconnected",error:`Browser profile "${K}" is not connected.`,errorHint:"Open that Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>."}}const z=j();if(z.length===1)return{connection:z[0]};if(z.length>1)return{errorCode:"profile_required",error:"Multiple Browser Bridge profiles are connected; choose one with --profile.",errorHint:"Run opencli profile list, then use opencli --profile <name> ... or opencli profile use <name>."};return{errorCode:"extension_not_connected",error:"Extension not connected. Please install the opencli Browser Bridge extension."}}function r(J,K){const z=typeof K==="string"&&K.trim()?K.trim():d,G=W.get(z);if(G&&G.ws!==J)G.ws.close();const Q=[...W.entries()].find(([,A])=>A.ws===J);if(Q&&Q[0]!==z)W.delete(Q[0]);const $=W.get(z),Z={contextId:z,ws:J,extensionVersion:$?.ws===J?$.extensionVersion:null,extensionCompatRange:$?.ws===J?$.extensionCompatRange:null,lastSeenAt:Date.now()};W.set(z,Z);return Z}function w(J){for(const[K,z]of W.entries()){if(z.ws!==J)continue;W.delete(K);L.traceExtDisconnect(K);for(const[G,Q]of N){if(Q.contextId!==K)continue;clearTimeout(Q.timer);const $=t({contextId:K,action:Q.action,dispatched:Q.dispatched});if($.countAsCommandResultUnknown){I++;V.warn(`[daemon] Command result unknown after extension disconnect (id=${G}, action=${Q.action}, context=${K})`)}L.traceResult(G,!1,Date.now()-Q.dispatchTs,$.errorCode);Q.reject(new E($.message,$.errorCode,$.errorHint,$.status));N.delete(G)}}}const x=33554432;class P extends Error{status=413;constructor(){super(`请求体超过 ${x/1024/1024} MB 上限(正文内联图片过多/过大?可减少单次发布的图片体积)`);this.name="BodyTooLargeError"}}function e(J){return new Promise((K,z)=>{const G=[];let Q=0,$=!1;J.on("data",(Z)=>{if($)return;Q+=Z.length;if(Q>x){$=!0;G.length=0;z(new P);return}G.push(Z)});J.on("end",()=>{if(!$)K(Buffer.concat(G).toString("utf-8"))});J.on("error",(Z)=>{if(!$)z(Z)})})}function Y(J,K,z,G){J.writeHead(K,{"Content-Type":"application/json",...G});J.end(JSON.stringify(z))}async function f(J,K){const z=J.headers.origin;if(z&&!z.startsWith("chrome-extension://")){Y(K,403,{ok:!1,error:"Forbidden: cross-origin request blocked"});return}if(J.method==="OPTIONS"){K.writeHead(204);K.end();return}const G=J.url??"/",Q=G.split("?")[0];if(J.method==="GET"&&Q==="/ping"){Y(K,200,{ok:!0},n(Q,z));return}if(!J.headers["x-opencli"]){Y(K,403,{ok:!1,error:"Forbidden: missing X-OpenCLI header"});return}if(J.method==="GET"&&Q==="/status"){const $=process.uptime(),Z=process.memoryUsage(),F=new URL(G,`http://localhost:${B}`).searchParams.get("contextId")?.trim()||void 0,X=q(F),k=j().map((M)=>({contextId:M.contextId,extensionConnected:!0,extensionVersion:M.extensionVersion??void 0,extensionCompatRange:M.extensionCompatRange??void 0,pending:[...N.values()].filter((H)=>H.contextId===M.contextId).length,lastSeenAt:M.lastSeenAt}));Y(K,200,{ok:!0,pid:process.pid,uptime:$,daemonVersion:c,extensionConnected:!!X.connection,extensionVersion:X.connection?.extensionVersion??void 0,extensionCompatRange:X.connection?.extensionCompatRange??void 0,contextId:X.connection?.contextId??F,profileRequired:X.errorCode==="profile_required",profileDisconnected:X.errorCode==="profile_disconnected",profiles:k,pending:N.size,commandResultUnknown:I,memoryMB:Math.round(Z.rss/1024/1024*10)/10,port:B});return}if(J.method==="GET"&&Q==="/logs"){const Z=new URL(G,`http://localhost:${B}`).searchParams.get("level"),A=Z?_.filter((F)=>F.level===Z):_;Y(K,200,{ok:!0,logs:A});return}if(J.method==="DELETE"&&Q==="/logs"){_.length=0;Y(K,200,{ok:!0});return}if(J.method==="POST"&&Q==="/shutdown"){Y(K,200,{ok:!0,message:"Shutting down"});setTimeout(()=>R(),100);return}if(J.method==="POST"&&G==="/command"){try{const $=JSON.parse(await e(J));if(!$.id){Y(K,400,{ok:!1,error:"Missing command id"});return}const Z=q(typeof $.contextId==="string"?$.contextId:void 0);if(!Z.connection){Y(K,Z.errorCode==="profile_required"?409:503,{id:$.id,ok:!1,errorCode:Z.errorCode,error:Z.error,...Z.errorHint?{errorHint:Z.errorHint}:{}});return}const A=typeof $.timeout==="number"&&$.timeout>0?$.timeout*1000:120000;if(N.has($.id)){Y(K,409,{id:$.id,ok:!1,error:"Duplicate command id already pending; retry"});return}const F=await new Promise((X,k)=>{const M=setTimeout(()=>{N.delete($.id);L.traceTimeout($.id,A,typeof $.action==="string"?$.action:void 0);k(Error(`Command timeout (${A/1000}s)`))},A),H={contextId:Z.connection.contextId,action:typeof $.action==="string"?$.action:"unknown",dispatched:!1,dispatchTs:Date.now(),resolve:X,reject:k,timer:M};N.set($.id,H);const y=(U)=>{if(N.get($.id)!==H)return;const D=i(H.contextId);clearTimeout(M);N.delete($.id);k(new E(D.message,D.errorCode,D.errorHint,D.status));V.warn(`[daemon] Failed to dispatch command ${$.id}: ${U instanceof Error?U.message:String(U)}`)};try{Z.connection.ws.send(JSON.stringify($),(U)=>{if(U&&!H.dispatched)y(U)});H.dispatched=!0;L.traceDispatch($)}catch(U){y(U)}});Y(K,200,F)}catch($){const Z=$ instanceof E?$:null;if($ instanceof P){Y(K,413,{ok:!1,error:$.message,errorCode:"body_too_large"});return}Y(K,Z?.status??($ instanceof Error&&$.message.includes("timeout")?408:400),{ok:!1,error:$ instanceof Error?$.message:"Invalid request",...Z?.errorCode?{errorCode:Z.errorCode}:{},...Z?.errorHint?{errorHint:Z.errorHint}:{}})}return}Y(K,404,{error:"Not found"})}const O=b((J,K)=>{f(J,K).catch(()=>{K.writeHead(500);K.end()})}),T=b((J,K)=>{f(J,K).catch(()=>{K.writeHead(500);K.end()})}),C=new g({noServer:!0});function m(J,K,z){if((J.url??"/").split("?")[0]!=="/ext"){K.destroy();return}const Q=J.headers.origin;if(Q&&!Q.startsWith("chrome-extension://")){K.destroy();return}C.handleUpgrade(J,K,z,($)=>{C.emit("connection",$,J)})}O.on("upgrade",m);T.on("upgrade",m);C.on("connection",(J)=>{V.info("[daemon] Extension connected");let K=0;const z=setInterval(()=>{if(J.readyState!==v.OPEN){clearInterval(z);return}if(K>=2){V.warn("[daemon] Extension heartbeat lost, closing connection");clearInterval(z);J.terminate();return}K++;J.ping()},15000);J.on("pong",()=>{K=0});J.on("message",(G)=>{try{const Q=JSON.parse(G.toString());if(Q.type==="hello"){const Z=r(J,Q.contextId);Z.extensionVersion=typeof Q.version==="string"?Q.version:null;Z.extensionCompatRange=typeof Q.compatRange==="string"?Q.compatRange:null;Z.lastSeenAt=Date.now();if(Z.extensionVersion)a(Z.extensionVersion);V.info(`[daemon] Extension profile connected: ${Z.contextId}`);L.traceExtConnect(Z.contextId,Z.extensionVersion);return}if(Q.type==="log"){if(Q.level==="error")V.error(`[ext] ${Q.msg}`);else if(Q.level==="warn")V.warn(`[ext] ${Q.msg}`);else V.info(`[ext] ${Q.msg}`);o({level:Q.level,msg:Q.msg,ts:Q.ts??Date.now()});L.traceExtLog(Q.level,Q.msg);return}const $=N.get(Q.id);if($){clearTimeout($.timer);N.delete(Q.id);L.traceResult(Q.id,!!Q.ok,Date.now()-$.dispatchTs,typeof Q.errorCode==="string"?Q.errorCode:void 0,typeof Q.page==="string"?Q.page:void 0);$.resolve(Q)}}catch(Q){const $=G.toString().slice(0,200);V.warn(`[daemon] Ignoring malformed WS message from extension: ${Q instanceof Error?Q.message:String(Q)} (first 200 chars: ${JSON.stringify($)})`)}});J.on("close",()=>{V.info("[daemon] Extension disconnected");clearInterval(z);w(J)});J.on("error",()=>{clearInterval(z);w(J)})});O.listen(B,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${B}`)});O.on("error",(J)=>{if(J.code==="EADDRINUSE"){V.error(`[daemon] Port ${B} already in use — another daemon is likely running. Exiting.`);process.exit(S.SERVICE_UNAVAIL)}V.error(`[daemon] Server error: ${J.message}`);process.exit(S.GENERIC_ERROR)});T.listen(h,"127.0.0.1",()=>{V.info(`[daemon] Listening on http://127.0.0.1:${h} (PublishPort extension)`)});T.on("error",(J)=>{V.warn(`[daemon] PublishPort port ${h} unavailable (${J.message}) — extension will fall back to ${B}`)});function R(){for(const[,J]of N){clearTimeout(J.timer);J.reject(Error("Daemon shutting down"))}N.clear();for(const J of W.values())J.ws.close();O.close();T.close();process.exit(S.SUCCESS)}process.on("SIGTERM",R);process.on("SIGINT",R);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.8.5-pp.26",
3
+ "version": "1.8.5-pp.28",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false