shine-code-submit 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/install.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var N=require("node:fs"),X1=require("node:path"),Y1=require("node:child_process");var m=require("node:fs"),h=require("node:os"),y=require("node:path"),v=require("node:child_process"),$1=[1,1,0],G1=300000;function D(){let b=v.spawnSync("bun",["--version"],{shell:process.platform==="win32",encoding:"utf8"});if(b.status===0&&(b.stdout??"").trim())return"bun";let q=h.homedir(),z=process.platform==="win32"?[y.join(q,".bun","bin","bun.exe")]:[y.join(q,".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun"];for(let Q of z)if(m.existsSync(Q))return Q;return null}function H1(b){return b.trim().split(".").map((q)=>parseInt(q,10)||0)}function W1(b,q){let z=H1(b);for(let Q=0;Q<q.length;Q++){let X=z[Q]??0,Y=q[Q]??0;if(X>Y)return!0;if(X<Y)return!1}return!0}function J1(){let b=process.env.npm_config_registry??"";return/npmmirror|taobao/i.test(b)}function M(b){return v.spawnSync(b,{shell:!0,encoding:"utf8",timeout:G1,stdio:"inherit"}).status??1}async function P(){let b=D();if(b){let z=v.spawnSync(b,["--version"],{shell:process.platform==="win32",encoding:"utf8"}).stdout?.trim()??"";if(z&&W1(z,$1))return console.log(`[shine-code-submit] bun ${z} detected`),b}if(console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装..."),J1()){if(console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun"),M("npm install -g bun")===0){let z=D();if(z)return console.log("[shine-code-submit] ✓ bun 安装成功(via npm 镜像)"),z}console.log("[shine-code-submit] npm 镜像方式失败,回退官方脚本")}if(process.platform==="win32")M('powershell -c "irm bun.sh/install.ps1 | iex"');else M("curl -fsSL https://bun.sh/install | bash");let q=D();if(!q)throw console.error("[shine-code-submit] bun 自动安装失败。请手动安装后重试:"),console.error(" Windows: winget install Oven-sh.Bun 或 npm install -g bun"),console.error(" macOS: brew install oven-sh/bun/bun"),console.error(" Linux: curl -fsSL https://bun.sh/install | bash"),Error("bun installation failed");return console.log("[shine-code-submit] ✓ bun 安装成功"),q}var $=require("node:fs"),Z=require("node:path"),g=require("node:child_process");var S=require("node:os"),U=require("node:path");function p(){return process.env.CLAUDE_CONFIG_DIR||U.join(S.homedir(),".claude")}function B(){return U.join(p(),"plugins")}function C(){return U.join(B(),"known_marketplaces.json")}function O(){return U.join(B(),"installed_plugins.json")}function V(){return U.join(p(),"settings.json")}var c={name:"shine-code-submit",version:"1.0.17",private:!1,type:"module",description:"Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",author:"renguifeng",license:"MIT",repository:{type:"git",url:"https://github.com/china-shine/shine-code-submit.git"},homepage:"https://github.com/china-shine/shine-code-submit",bugs:{url:"https://github.com/china-shine/shine-code-submit/issues"},keywords:["claude","claude-code","plugin","hooks","daemon","dashboard"],bin:{"shine-code-submit":"dist/install.cjs"},files:["dist/install.cjs",".claude-plugin","hooks/hooks.json","bin/launcher.cjs","src","ui","package.json","bun.lock","README.md"],engines:{node:">=18"},publishConfig:{access:"public",registry:"https://registry.npmjs.org/"},scripts:{daemon:"bun run src/daemon/main.ts",hook:"bun run src/hook/main.ts",cli:"bun run src/cli/main.ts",build:"bun run scripts/build.ts","build:all":"bun run scripts/build.ts --all","build:install":"bun run scripts/build-install.ts","build:dist":"bun run scripts/build.ts && bun run scripts/build-install.ts",prepublishOnly:"bun run build:dist",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"^1.3.14","@types/react":"^19.2.17","@types/react-dom":"^19.2.3",typescript:"^6.0.3"},dependencies:{marked:"^18.0.5",react:"^19.2.7","react-dom":"^19.2.7"}};var E=require("node:os"),l="shine-code-submit",H=c.version,I1=process.env.SHINE_CODE_SUBMIT_HOST??"0.0.0.0",F1="127.0.0.1",d=36666,L=`http://${F1}:${d}`;function U1(){let b=["vethernet","vmware","virtualbox","docker","veth","br-","virbr","vnet","utun"],q=(z)=>{let Q=z.toLowerCase();return b.some((X)=>Q.includes(X))};try{let z=E.networkInterfaces();for(let Q of Object.keys(z)){if(q(Q))continue;for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}for(let Q of Object.keys(z))for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}catch{}return"localhost"}var w=`http://${U1()}:${d}`;var W="shine-code-submit",I="shine-code-submit";function _(b=H){return Z.join(B(),"cache",W,I,b)}function u1(){let b=process.argv[1],q=process.cwd();if(b)try{q=Z.dirname($.realpathSync(Z.resolve(b)))}catch{q=Z.dirname(Z.resolve(b))}let z=q;for(let Q=0;Q<12;Q++){if($.existsSync(Z.join(z,"package.json"))&&$.existsSync(Z.join(z,".claude-plugin")))return z;let X=Z.dirname(z);if(X===z)break;z=X}return Z.dirname(q)}var x1=[".claude-plugin","hooks","bin","src","ui","package.json","bun.lock","README.md"];function s(b){let q=_();if($.existsSync(q))$.rmSync(q,{recursive:!0,force:!0});$.mkdirSync(q,{recursive:!0});let z=u1();console.log(`[shine-code-submit] 部署源:${z}`);for(let X of x1){let Y=Z.join(z,X);if(!$.existsSync(Y))continue;$.cpSync(Y,Z.join(q,X),{recursive:!0})}console.log("[shine-code-submit] 安装运行时依赖(bun install)...");let Q=g.spawnSync(b,["install","--frozen-lockfile"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status;if(Q!==0){if(console.log("[shine-code-submit] --frozen-lockfile 失败,重试普通 bun install"),Q=g.spawnSync(b,["install"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status,Q!==0)throw Error(`bun install 失败(exit ${Q})。请手动在 ${q} 跑 bun install`)}return $.writeFileSync(Z.join(q,".install-version"),JSON.stringify({version:H,installedAt:Date.now()}),"utf8"),console.log(`[shine-code-submit] 已部署到 ${q}`),q}var G=require("node:fs"),o=require("node:path");function K(b,q){if(!G.existsSync(b))return q;let z;try{z=G.readFileSync(b,"utf8")}catch{return q}try{return JSON.parse(z)}catch{let Q=`${b}.bak-corrupt-${Date.now()}`;try{G.copyFileSync(b,Q),console.error(`[shine-code-submit] WARNING: ${b} JSON 损坏,已备份到 ${Q},用默认值继续`)}catch{}return q}}function F(b,q){let z=`${b}.bak-pre-install`;if(G.existsSync(b)&&!G.existsSync(z))try{G.copyFileSync(b,z)}catch{}G.mkdirSync(o.dirname(b),{recursive:!0});let Q=`${b}.tmp-${process.pid}`;G.writeFileSync(Q,JSON.stringify(q,null,2),"utf8"),G.renameSync(Q,b)}function f(){return`${I}@${W}`}function r(b){let q=C(),z=K(q,{}),Q=z[W];if(Q?.source&&Q.source.source!=="directory")console.log(`[shine-code-submit] WARNING: marketplace "${W}" 已存在(source=${Q.source.source}),将覆盖为 directory 源(原文件已备份)`);z[W]={source:{source:"directory",path:b},installLocation:b,lastUpdated:new Date().toISOString(),autoUpdate:!1},F(q,z),console.log(`[shine-code-submit] marketplace 已注册 → ${q}`)}function i(b){let q=O(),z=K(q,{version:2,plugins:{}});if(!z.version)z.version=2;if(!z.plugins)z.plugins={};let Q=f(),X=new Date().toISOString(),Y=z.plugins[Q]?.[0];z.plugins[Q]=[{scope:"user",installPath:b,version:H,installedAt:Y?.installedAt??X,lastUpdated:X}],F(q,z),console.log(`[shine-code-submit] plugin 已注册 → ${q}`)}function t(b){let q=V(),z=K(q,{});if(!z.enabledPlugins)z.enabledPlugins={};let Q=f();if(z.enabledPlugins[Q]=!0,!z.extraKnownMarketplaces)z.extraKnownMarketplaces={};if(!z.extraKnownMarketplaces[W])z.extraKnownMarketplaces[W]={source:{source:"directory",path:b}};F(q,z),console.log(`[shine-code-submit] 已启用(enabledPlugins)→ ${q}`)}function n(){let b=f(),q=K(C(),{});if(q[W])delete q[W],F(C(),q);let z=K(O(),{plugins:{}});if(z.plugins&&z.plugins[b])delete z.plugins[b],F(O(),z);let Q=K(V(),{}),X=!1;if(Q.enabledPlugins&&Q.enabledPlugins[b])delete Q.enabledPlugins[b],X=!0;if(Q.extraKnownMarketplaces&&Q.extraKnownMarketplaces[W])delete Q.extraKnownMarketplaces[W],X=!0;if(X)F(V(),Q);console.log("[shine-code-submit] 已从三处 JSON 移除注册")}var x=require("node:fs");var J=require("node:path"),a=require("node:fs"),e=require("node:os"),j1=process.env.LOCALAPPDATA??J.join(e.homedir(),".local","share"),u=J.join(j1,"shine-code-submit"),v1=J.join(u,"spool"),b1=J.join(u,"log"),q1=J.join(u,"db"),R=J.join(u,"daemon.pid"),E1=J.join(b1,"daemon.log"),l1=J.join(q1,"events.sqlite");function z1(){for(let b of[u,v1,b1,q1])a.mkdirSync(b,{recursive:!0})}function j(){try{let b=x.readFileSync(R,"utf8");return JSON.parse(b)}catch{return null}}function Q1(){try{x.unlinkSync(R)}catch{}}async function T(b=400){try{let q=await fetch(`${L}/api/health`,{signal:AbortSignal.timeout(b)});if(!q.ok)return{alive:!1};let z=await q.json();if(z?.service!==l)return{alive:!1};return{alive:!0,version:z.version}}catch{return{alive:!1}}}async function k(b=400){return(await T(b)).alive}async function A(){let b=j();if(!b)return;if(await k()){try{await fetch(`${L}/api/shutdown`,{method:"POST",headers:{authorization:`Bearer ${b.token}`}})}catch{}if(await B1(1000),await k())try{process.kill(b.pid)}catch{}}Q1()}function B1(b){return new Promise((q)=>setTimeout(q,b))}var[,,Z1]=process.argv;C1().catch((b)=>{console.error(`[shine-code-submit] ${b instanceof Error?b.message:String(b)}`),process.exit(1)});async function C1(){switch(Z1){case void 0:case"install":await O1();break;case"uninstall":await V1();break;case"status":await w1();break;case"--version":case"-v":console.log(H);break;default:D1()}}async function O1(){console.log(`=== shine-code-submit installer v${H} ===`);let b=await P(),q=s(b);r(q),i(q),t(q),z1(),await k1(b,q),T1(),console.log(""),console.log("✓ 安装完成。"),console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。"),console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。")}async function V1(){console.log("=== shine-code-submit uninstaller ==="),await A(),n();let b=_();if(N.existsSync(b))N.rmSync(b,{recursive:!0,force:!0}),console.log(`[shine-code-submit] 已删除 ${b}`);console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。")}async function w1(){let b=await k(),q=j();if(b&&q)console.log(`daemon: running pid=${q.pid} ${w}`);else console.log("daemon: not running")}async function k1(b,q){let z=await T();if(z.alive&&z.version===H){console.log(`[shine-code-submit] daemon 已是最新 v${H},跳过启动`);return}if(z.alive)console.log(`[shine-code-submit] daemon 旧版 v${z.version} 运行中,重启到 v${H}...`),await A();else console.log("[shine-code-submit] 启动 daemon...");let Q=X1.join(q,"src","daemon","main.ts");try{Y1.spawn(b,["run",Q],{detached:!0,stdio:"ignore",windowsHide:!0,cwd:q,shell:process.platform==="win32"}).unref()}catch(Y){console.error(`[shine-code-submit] 启动 daemon 失败:${Y instanceof Error?Y.message:Y}`),console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");return}let X=Date.now()+1e4;while(Date.now()<X){await N1(200);let Y=await T();if(Y.alive&&Y.version===H){console.log("[shine-code-submit] daemon 已就绪");return}}console.error("[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。")}function T1(){let b=j(),q=b?`${w}/ui?t=${b.token}`:`${w}/ui`;console.log(`[shine-code-submit] Dashboard: ${q}`)}function N1(b){return new Promise((q)=>setTimeout(q,b))}function D1(){console.log(`shine-code-submit <command>
|
|
2
|
+
var N=require("node:fs"),X1=require("node:path"),Y1=require("node:child_process");var m=require("node:fs"),h=require("node:os"),y=require("node:path"),v=require("node:child_process"),$1=[1,1,0],G1=300000;function D(){let b=v.spawnSync("bun",["--version"],{shell:process.platform==="win32",encoding:"utf8"});if(b.status===0&&(b.stdout??"").trim())return"bun";let q=h.homedir(),z=process.platform==="win32"?[y.join(q,".bun","bin","bun.exe")]:[y.join(q,".bun","bin","bun"),"/usr/local/bin/bun","/opt/homebrew/bin/bun"];for(let Q of z)if(m.existsSync(Q))return Q;return null}function H1(b){return b.trim().split(".").map((q)=>parseInt(q,10)||0)}function W1(b,q){let z=H1(b);for(let Q=0;Q<q.length;Q++){let X=z[Q]??0,Y=q[Q]??0;if(X>Y)return!0;if(X<Y)return!1}return!0}function J1(){let b=process.env.npm_config_registry??"";return/npmmirror|taobao/i.test(b)}function M(b){return v.spawnSync(b,{shell:!0,encoding:"utf8",timeout:G1,stdio:"inherit"}).status??1}async function P(){let b=D();if(b){let z=v.spawnSync(b,["--version"],{shell:process.platform==="win32",encoding:"utf8"}).stdout?.trim()??"";if(z&&W1(z,$1))return console.log(`[shine-code-submit] bun ${z} detected`),b}if(console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装..."),J1()){if(console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun"),M("npm install -g bun")===0){let z=D();if(z)return console.log("[shine-code-submit] ✓ bun 安装成功(via npm 镜像)"),z}console.log("[shine-code-submit] npm 镜像方式失败,回退官方脚本")}if(process.platform==="win32")M('powershell -c "irm bun.sh/install.ps1 | iex"');else M("curl -fsSL https://bun.sh/install | bash");let q=D();if(!q)throw console.error("[shine-code-submit] bun 自动安装失败。请手动安装后重试:"),console.error(" Windows: winget install Oven-sh.Bun 或 npm install -g bun"),console.error(" macOS: brew install oven-sh/bun/bun"),console.error(" Linux: curl -fsSL https://bun.sh/install | bash"),Error("bun installation failed");return console.log("[shine-code-submit] ✓ bun 安装成功"),q}var $=require("node:fs"),Z=require("node:path"),g=require("node:child_process");var S=require("node:os"),U=require("node:path");function p(){return process.env.CLAUDE_CONFIG_DIR||U.join(S.homedir(),".claude")}function B(){return U.join(p(),"plugins")}function C(){return U.join(B(),"known_marketplaces.json")}function O(){return U.join(B(),"installed_plugins.json")}function V(){return U.join(p(),"settings.json")}var c={name:"shine-code-submit",version:"1.0.19",private:!1,type:"module",description:"Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",author:"renguifeng",license:"MIT",repository:{type:"git",url:"https://github.com/china-shine/shine-code-submit.git"},homepage:"https://github.com/china-shine/shine-code-submit",bugs:{url:"https://github.com/china-shine/shine-code-submit/issues"},keywords:["claude","claude-code","plugin","hooks","daemon","dashboard"],bin:{"shine-code-submit":"dist/install.cjs"},files:["dist/install.cjs",".claude-plugin","hooks/hooks.json","bin/launcher.cjs","src","ui","package.json","bun.lock","README.md"],engines:{node:">=18"},publishConfig:{access:"public",registry:"https://registry.npmjs.org/"},scripts:{daemon:"bun run src/daemon/main.ts",hook:"bun run src/hook/main.ts",cli:"bun run src/cli/main.ts",build:"bun run scripts/build.ts","build:all":"bun run scripts/build.ts --all","build:install":"bun run scripts/build-install.ts","build:dist":"bun run scripts/build.ts && bun run scripts/build-install.ts",prepublishOnly:"bun run build:dist",typecheck:"tsc --noEmit"},devDependencies:{"@types/bun":"^1.3.14","@types/react":"^19.2.17","@types/react-dom":"^19.2.3",typescript:"^6.0.3"},dependencies:{marked:"^18.0.5",react:"^19.2.7","react-dom":"^19.2.7"}};var E=require("node:os"),l="shine-code-submit",H=c.version,I1=process.env.SHINE_CODE_SUBMIT_HOST??"0.0.0.0",F1="127.0.0.1",d=36666,L=`http://${F1}:${d}`;function U1(){let b=["vethernet","vmware","virtualbox","docker","veth","br-","virbr","vnet","utun"],q=(z)=>{let Q=z.toLowerCase();return b.some((X)=>Q.includes(X))};try{let z=E.networkInterfaces();for(let Q of Object.keys(z)){if(q(Q))continue;for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}for(let Q of Object.keys(z))for(let X of z[Q]??[])if(X.family==="IPv4"&&!X.internal)return X.address}catch{}return"localhost"}var w=`http://${U1()}:${d}`;var W="shine-code-submit",I="shine-code-submit";function _(b=H){return Z.join(B(),"cache",W,I,b)}function u1(){let b=process.argv[1],q=process.cwd();if(b)try{q=Z.dirname($.realpathSync(Z.resolve(b)))}catch{q=Z.dirname(Z.resolve(b))}let z=q;for(let Q=0;Q<12;Q++){if($.existsSync(Z.join(z,"package.json"))&&$.existsSync(Z.join(z,".claude-plugin")))return z;let X=Z.dirname(z);if(X===z)break;z=X}return Z.dirname(q)}var x1=[".claude-plugin","hooks","bin","src","ui","package.json","bun.lock","README.md"];function s(b){let q=_();if($.existsSync(q))$.rmSync(q,{recursive:!0,force:!0});$.mkdirSync(q,{recursive:!0});let z=u1();console.log(`[shine-code-submit] 部署源:${z}`);for(let X of x1){let Y=Z.join(z,X);if(!$.existsSync(Y))continue;$.cpSync(Y,Z.join(q,X),{recursive:!0})}console.log("[shine-code-submit] 安装运行时依赖(bun install)...");let Q=g.spawnSync(b,["install","--frozen-lockfile"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status;if(Q!==0){if(console.log("[shine-code-submit] --frozen-lockfile 失败,重试普通 bun install"),Q=g.spawnSync(b,["install"],{cwd:q,shell:process.platform==="win32",encoding:"utf8",stdio:"inherit"}).status,Q!==0)throw Error(`bun install 失败(exit ${Q})。请手动在 ${q} 跑 bun install`)}return $.writeFileSync(Z.join(q,".install-version"),JSON.stringify({version:H,installedAt:Date.now()}),"utf8"),console.log(`[shine-code-submit] 已部署到 ${q}`),q}var G=require("node:fs"),o=require("node:path");function K(b,q){if(!G.existsSync(b))return q;let z;try{z=G.readFileSync(b,"utf8")}catch{return q}try{return JSON.parse(z)}catch{let Q=`${b}.bak-corrupt-${Date.now()}`;try{G.copyFileSync(b,Q),console.error(`[shine-code-submit] WARNING: ${b} JSON 损坏,已备份到 ${Q},用默认值继续`)}catch{}return q}}function F(b,q){let z=`${b}.bak-pre-install`;if(G.existsSync(b)&&!G.existsSync(z))try{G.copyFileSync(b,z)}catch{}G.mkdirSync(o.dirname(b),{recursive:!0});let Q=`${b}.tmp-${process.pid}`;G.writeFileSync(Q,JSON.stringify(q,null,2),"utf8"),G.renameSync(Q,b)}function f(){return`${I}@${W}`}function r(b){let q=C(),z=K(q,{}),Q=z[W];if(Q?.source&&Q.source.source!=="directory")console.log(`[shine-code-submit] WARNING: marketplace "${W}" 已存在(source=${Q.source.source}),将覆盖为 directory 源(原文件已备份)`);z[W]={source:{source:"directory",path:b},installLocation:b,lastUpdated:new Date().toISOString(),autoUpdate:!1},F(q,z),console.log(`[shine-code-submit] marketplace 已注册 → ${q}`)}function i(b){let q=O(),z=K(q,{version:2,plugins:{}});if(!z.version)z.version=2;if(!z.plugins)z.plugins={};let Q=f(),X=new Date().toISOString(),Y=z.plugins[Q]?.[0];z.plugins[Q]=[{scope:"user",installPath:b,version:H,installedAt:Y?.installedAt??X,lastUpdated:X}],F(q,z),console.log(`[shine-code-submit] plugin 已注册 → ${q}`)}function t(b){let q=V(),z=K(q,{});if(!z.enabledPlugins)z.enabledPlugins={};let Q=f();if(z.enabledPlugins[Q]=!0,!z.extraKnownMarketplaces)z.extraKnownMarketplaces={};if(!z.extraKnownMarketplaces[W])z.extraKnownMarketplaces[W]={source:{source:"directory",path:b}};F(q,z),console.log(`[shine-code-submit] 已启用(enabledPlugins)→ ${q}`)}function n(){let b=f(),q=K(C(),{});if(q[W])delete q[W],F(C(),q);let z=K(O(),{plugins:{}});if(z.plugins&&z.plugins[b])delete z.plugins[b],F(O(),z);let Q=K(V(),{}),X=!1;if(Q.enabledPlugins&&Q.enabledPlugins[b])delete Q.enabledPlugins[b],X=!0;if(Q.extraKnownMarketplaces&&Q.extraKnownMarketplaces[W])delete Q.extraKnownMarketplaces[W],X=!0;if(X)F(V(),Q);console.log("[shine-code-submit] 已从三处 JSON 移除注册")}var x=require("node:fs");var J=require("node:path"),a=require("node:fs"),e=require("node:os"),j1=process.env.LOCALAPPDATA??J.join(e.homedir(),".local","share"),u=J.join(j1,"shine-code-submit"),v1=J.join(u,"spool"),b1=J.join(u,"log"),q1=J.join(u,"db"),R=J.join(u,"daemon.pid"),E1=J.join(b1,"daemon.log"),l1=J.join(q1,"events.sqlite");function z1(){for(let b of[u,v1,b1,q1])a.mkdirSync(b,{recursive:!0})}function j(){try{let b=x.readFileSync(R,"utf8");return JSON.parse(b)}catch{return null}}function Q1(){try{x.unlinkSync(R)}catch{}}async function T(b=400){try{let q=await fetch(`${L}/api/health`,{signal:AbortSignal.timeout(b)});if(!q.ok)return{alive:!1};let z=await q.json();if(z?.service!==l)return{alive:!1};return{alive:!0,version:z.version}}catch{return{alive:!1}}}async function k(b=400){return(await T(b)).alive}async function A(){let b=j();if(!b)return;if(await k()){try{await fetch(`${L}/api/shutdown`,{method:"POST",headers:{authorization:`Bearer ${b.token}`}})}catch{}if(await B1(1000),await k())try{process.kill(b.pid)}catch{}}Q1()}function B1(b){return new Promise((q)=>setTimeout(q,b))}var[,,Z1]=process.argv;C1().catch((b)=>{console.error(`[shine-code-submit] ${b instanceof Error?b.message:String(b)}`),process.exit(1)});async function C1(){switch(Z1){case void 0:case"install":await O1();break;case"uninstall":await V1();break;case"status":await w1();break;case"--version":case"-v":console.log(H);break;default:D1()}}async function O1(){console.log(`=== shine-code-submit installer v${H} ===`);let b=await P(),q=s(b);r(q),i(q),t(q),z1(),await k1(b,q),T1(),console.log(""),console.log("✓ 安装完成。"),console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。"),console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。")}async function V1(){console.log("=== shine-code-submit uninstaller ==="),await A(),n();let b=_();if(N.existsSync(b))N.rmSync(b,{recursive:!0,force:!0}),console.log(`[shine-code-submit] 已删除 ${b}`);console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。")}async function w1(){let b=await k(),q=j();if(b&&q)console.log(`daemon: running pid=${q.pid} ${w}`);else console.log("daemon: not running")}async function k1(b,q){let z=await T();if(z.alive&&z.version===H){console.log(`[shine-code-submit] daemon 已是最新 v${H},跳过启动`);return}if(z.alive)console.log(`[shine-code-submit] daemon 旧版 v${z.version} 运行中,重启到 v${H}...`),await A();else console.log("[shine-code-submit] 启动 daemon...");let Q=X1.join(q,"src","daemon","main.ts");try{Y1.spawn(b,["run",Q],{detached:!0,stdio:"ignore",windowsHide:!0,cwd:q,shell:process.platform==="win32"}).unref()}catch(Y){console.error(`[shine-code-submit] 启动 daemon 失败:${Y instanceof Error?Y.message:Y}`),console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");return}let X=Date.now()+1e4;while(Date.now()<X){await N1(200);let Y=await T();if(Y.alive&&Y.version===H){console.log("[shine-code-submit] daemon 已就绪");return}}console.error("[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。")}function T1(){let b=j(),q=b?`${w}/ui?t=${b.token}`:`${w}/ui`;console.log(`[shine-code-submit] Dashboard: ${q}`)}function N1(b){return new Promise((q)=>setTimeout(q,b))}function D1(){console.log(`shine-code-submit <command>
|
|
3
3
|
|
|
4
4
|
install 安装插件(自动装 bun + 部署 + 注册 + 启 daemon + 开 dashboard)
|
|
5
5
|
uninstall 卸载(停 daemon + 反注册 + 删文件)
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, readdirSync, statSync } from "node:fs";
|
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
|
|
8
|
-
import { getSessionTokenTotal, getSessionTitle } from "./token-cache";
|
|
8
|
+
import { getSessionTokenTotal, getSessionTitle, getSessionActiveMs, getSessionCwd } from "./token-cache";
|
|
9
9
|
import type { TokenUsage } from "../shared/types";
|
|
10
10
|
|
|
11
11
|
export interface ScannedSession {
|
|
@@ -17,8 +17,12 @@ export interface ScannedSession {
|
|
|
17
17
|
/** 父文件 mtime(session 级活跃时间,用于 since 过滤)。 */
|
|
18
18
|
lastActivity: number;
|
|
19
19
|
tokenTotal: TokenUsage;
|
|
20
|
+
/** gap-aware 活跃时长(ms):父+子代理合并,messageId 去重,1h gap 截断。 */
|
|
21
|
+
activeMs: number;
|
|
20
22
|
/** 首条 user 消息(会话标题);读不到为 null。 */
|
|
21
23
|
title: string | null;
|
|
24
|
+
/** 真实 cwd(从 transcript jsonl 首条 cwd 字段读,无编码损失);读不到为 null,消费方回退解码目录名。 */
|
|
25
|
+
cwd: string | null;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const ZERO: TokenUsage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
@@ -136,7 +140,9 @@ function collectScannedSessions(): ScannedSession[] {
|
|
|
136
140
|
transcriptPath: file,
|
|
137
141
|
lastActivity: mtimeMs,
|
|
138
142
|
tokenTotal,
|
|
143
|
+
activeMs: getSessionActiveMs(file),
|
|
139
144
|
title: getSessionTitle(file),
|
|
145
|
+
cwd: getSessionCwd(file),
|
|
140
146
|
});
|
|
141
147
|
}
|
|
142
148
|
}
|
package/src/daemon/server.ts
CHANGED
|
@@ -198,7 +198,7 @@ export function startServer(deps: ServerDeps) {
|
|
|
198
198
|
const h = hookMap.get(sc.sessionId);
|
|
199
199
|
return {
|
|
200
200
|
sessionId: sc.sessionId,
|
|
201
|
-
cwd: h?.cwd ?? decodeProjectCwd(sc.project),
|
|
201
|
+
cwd: h?.cwd ?? sc.cwd ?? decodeProjectCwd(sc.project),
|
|
202
202
|
lastActive: Math.max(sc.lastActivity, h?.lastActive ?? 0),
|
|
203
203
|
eventCount: h?.eventCount ?? 0,
|
|
204
204
|
lastType: h?.lastType ?? null,
|
|
@@ -337,7 +337,7 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
|
|
|
337
337
|
// 同 cwd 的 session 合并到一个项目,避免导航栏因「同一 cwd 对应多个编码项目」而重复。
|
|
338
338
|
const byCwd = new Map<string, ScannedSession[]>();
|
|
339
339
|
for (const s of scanned) {
|
|
340
|
-
const cwd = hookCwd.get(s.sessionId) ?? decodeProjectCwd(s.project);
|
|
340
|
+
const cwd = hookCwd.get(s.sessionId) ?? s.cwd ?? decodeProjectCwd(s.project);
|
|
341
341
|
const arr = byCwd.get(cwd);
|
|
342
342
|
if (arr) arr.push(s);
|
|
343
343
|
else byCwd.set(cwd, [s]);
|
|
@@ -351,6 +351,7 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
|
|
|
351
351
|
sessionId: s.sessionId,
|
|
352
352
|
lastActive: s.lastActivity,
|
|
353
353
|
tokenTotal: s.tokenTotal,
|
|
354
|
+
activeMs: s.activeMs,
|
|
354
355
|
linesTotal: getSessionLines(store, s.sessionId, s.lastActivity),
|
|
355
356
|
title: s.title,
|
|
356
357
|
}));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 任一文件没变就直接返回,变了才重读重算。冷启动逐步填充,稳态全命中。异常返回 null。
|
|
5
5
|
import { statSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { sumSessionUsage, sessionTranscriptFiles, readFirstUserText } from "./transcript";
|
|
7
|
+
import { sumSessionUsage, sessionTranscriptFiles, readFirstUserText, readFirstCwd, sessionActiveMs } from "./transcript";
|
|
8
8
|
import type { TokenUsage } from "../shared/types";
|
|
9
9
|
|
|
10
10
|
interface Entry {
|
|
@@ -66,3 +66,63 @@ export function getSessionTitle(transcriptPath: string): string | null {
|
|
|
66
66
|
titleCache.set(transcriptPath, { mtimeKey, title });
|
|
67
67
|
return title;
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
interface CwdEntry {
|
|
71
|
+
mtimeKey: string;
|
|
72
|
+
cwd: string | null;
|
|
73
|
+
}
|
|
74
|
+
const cwdCache = new Map<string, CwdEntry>();
|
|
75
|
+
|
|
76
|
+
/** 返回某 transcript 父会话的真实 cwd(从 jsonl 首条 cwd 字段读,无编码损失);带复合 mtime 缓存
|
|
77
|
+
* (与 getSessionTitle 同策略,只读父 transcript);读不到返回 null。供扫描替代 decodeProjectCwd 的有损解码。 */
|
|
78
|
+
export function getSessionCwd(transcriptPath: string): string | null {
|
|
79
|
+
const realPath = transcriptPath.replace(/^~/, homedir());
|
|
80
|
+
const files = sessionTranscriptFiles(realPath);
|
|
81
|
+
if (files.length === 0) return null;
|
|
82
|
+
let mtimeKey: string;
|
|
83
|
+
try {
|
|
84
|
+
mtimeKey = files.map((file) => `${file}:${statSync(file).mtimeMs}`).join("|");
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const hit = cwdCache.get(transcriptPath);
|
|
89
|
+
if (hit && hit.mtimeKey === mtimeKey) return hit.cwd;
|
|
90
|
+
let cwd: string | null = null;
|
|
91
|
+
try {
|
|
92
|
+
cwd = readFirstCwd(realPath);
|
|
93
|
+
} catch {
|
|
94
|
+
cwd = null;
|
|
95
|
+
}
|
|
96
|
+
cwdCache.set(transcriptPath, { mtimeKey, cwd });
|
|
97
|
+
return cwd;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ActiveEntry {
|
|
101
|
+
mtimeKey: string;
|
|
102
|
+
activeMs: number;
|
|
103
|
+
}
|
|
104
|
+
const activeCache = new Map<string, ActiveEntry>();
|
|
105
|
+
|
|
106
|
+
/** 返回某 transcript 的会话级 gap-aware 活跃时长(父 + subagents/*.jsonl,对齐 ccusage session 口径);
|
|
107
|
+
* 带复合 mtime 缓存(与 getSessionTokenTotal 同策略);读不到/解析失败返回 0。 */
|
|
108
|
+
export function getSessionActiveMs(transcriptPath: string): number {
|
|
109
|
+
const realPath = transcriptPath.replace(/^~/, homedir());
|
|
110
|
+
const files = sessionTranscriptFiles(realPath);
|
|
111
|
+
if (files.length === 0) return 0;
|
|
112
|
+
let mtimeKey: string;
|
|
113
|
+
try {
|
|
114
|
+
mtimeKey = files.map((file) => `${file}:${statSync(file).mtimeMs}`).join("|");
|
|
115
|
+
} catch {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const hit = activeCache.get(transcriptPath);
|
|
119
|
+
if (hit && hit.mtimeKey === mtimeKey) return hit.activeMs;
|
|
120
|
+
let activeMs = 0;
|
|
121
|
+
try {
|
|
122
|
+
activeMs = sessionActiveMs(realPath);
|
|
123
|
+
} catch {
|
|
124
|
+
activeMs = 0;
|
|
125
|
+
}
|
|
126
|
+
activeCache.set(transcriptPath, { mtimeKey, activeMs });
|
|
127
|
+
return activeMs;
|
|
128
|
+
}
|
package/src/daemon/transcript.ts
CHANGED
|
@@ -132,6 +132,31 @@ export function readFirstUserText(transcriptPath: string): string | null {
|
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/** 读 transcript,返回首条 cwd 字段(Claude Code 写入的真实工作目录,无编码损失)。
|
|
136
|
+
* 只解析前 64 行(cwd 通常在首行 summary);无 cwd 字段返回 null。
|
|
137
|
+
* 供扫描补真实 cwd,替代从项目目录名反推的有损解码(中文/空格/括号被编码成 - 会丢失)。 */
|
|
138
|
+
export function readFirstCwd(transcriptPath: string): string | null {
|
|
139
|
+
const path = transcriptPath.replace(/^~/, homedir());
|
|
140
|
+
if (!existsSync(path)) return null;
|
|
141
|
+
let raw: string;
|
|
142
|
+
try {
|
|
143
|
+
raw = readFileSync(path, "utf8");
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
for (const line of raw.split("\n").slice(0, 64)) {
|
|
148
|
+
if (!line.trim()) continue;
|
|
149
|
+
let obj: Record<string, unknown>;
|
|
150
|
+
try {
|
|
151
|
+
obj = JSON.parse(line);
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (typeof obj.cwd === "string" && obj.cwd) return obj.cwd;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
/** 从 message.usage(Anthropic 扁平四字段)提取 token 用量;无任何数值字段则 undefined。 */
|
|
136
161
|
function readUsage(raw: unknown): TokenUsage | undefined {
|
|
137
162
|
if (!raw || typeof raw !== "object") return undefined;
|
|
@@ -250,6 +275,7 @@ interface UsageDedupeEntry {
|
|
|
250
275
|
output: number;
|
|
251
276
|
cacheCreation: number;
|
|
252
277
|
cacheRead: number;
|
|
278
|
+
ts: number; // 行时间戳(Date.parse(obj.timestamp),line 344 已严格校验);供 gap-aware 时长收集
|
|
253
279
|
}
|
|
254
280
|
|
|
255
281
|
function matchesExactKey(
|
|
@@ -362,6 +388,7 @@ function readUsageEntries(transcriptPath: string): UsageDedupeEntry[] {
|
|
|
362
388
|
output,
|
|
363
389
|
cacheCreation,
|
|
364
390
|
cacheRead,
|
|
391
|
+
ts: Date.parse(obj.timestamp as string),
|
|
365
392
|
});
|
|
366
393
|
}
|
|
367
394
|
return entries;
|
|
@@ -422,3 +449,48 @@ export function sumSessionUsage(parentPath: string): TokenUsage {
|
|
|
422
449
|
}
|
|
423
450
|
return dedupeAndSum(entries);
|
|
424
451
|
}
|
|
452
|
+
|
|
453
|
+
// ---- gap-aware 活跃时长(「对话总时长」KPI 数据源)----
|
|
454
|
+
// transcript 不落盘 duration(cost.total_duration_ms 是运行时字段),用 message timestamp 序列估算:
|
|
455
|
+
// 相邻 gap > GAP_MS 视为用户离开、不计入;每段连续 burst 末尾 +BUFFER_MS 补读/测时间。
|
|
456
|
+
// 复用 readUsageEntries 的 ccusage 严格校验 + pushDedupedEntry 的 messageId 去重,口径与 token 一致。
|
|
457
|
+
const GAP_MS = 3600_000; // 1h:相邻 timestamp 间隔超过此值视为用户离开
|
|
458
|
+
const BUFFER_MS = 600_000; // 10min:每段连续 burst 末尾补的读/测时间(单点 burst 也给 10min,避免「只发一条=0」)
|
|
459
|
+
|
|
460
|
+
/** 对条目集合做 ccusage 全局去重(message.id+requestId,含 sidechain 重放兜底),返回 survivor 条目本身(未求和)。
|
|
461
|
+
* 与 dedupeAndSum 同语义,但保留条目(含 ts),供 sessionActiveMs 收集时间戳。
|
|
462
|
+
* 注意:必须走 messageId 去重而非 Set(ts) —— 同 messageId 的多行可能 ts 不同(重放/重试),Set 去不掉。 */
|
|
463
|
+
function dedupedSurvivors(entries: UsageDedupeEntry[]): UsageDedupeEntry[] {
|
|
464
|
+
const survivors: UsageDedupeEntry[] = [];
|
|
465
|
+
const byExact = new Map<string, number[]>();
|
|
466
|
+
const byMessage = new Map<string, number[]>();
|
|
467
|
+
for (const entry of entries) pushDedupedEntry(entry, survivors, byExact, byMessage);
|
|
468
|
+
return survivors;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** 某 session 的 gap-aware 活跃时长(ms):父 transcript + subagents/*.jsonl 合并(与 token 同口径)。
|
|
472
|
+
* 收集去重后的 timestamp,升序排序后按 GAP_MS 切 burst 累加;空 session 返回 0。 */
|
|
473
|
+
export function sessionActiveMs(parentPath: string): number {
|
|
474
|
+
const entries: UsageDedupeEntry[] = [];
|
|
475
|
+
for (const file of sessionTranscriptFiles(parentPath)) {
|
|
476
|
+
for (const entry of readUsageEntries(file)) entries.push(entry);
|
|
477
|
+
}
|
|
478
|
+
const ts = dedupedSurvivors(entries)
|
|
479
|
+
.map((e) => e.ts)
|
|
480
|
+
.filter((t) => Number.isFinite(t))
|
|
481
|
+
.sort((a, b) => a - b);
|
|
482
|
+
if (ts.length === 0) return 0;
|
|
483
|
+
let active = 0;
|
|
484
|
+
let burstStart = ts[0]!;
|
|
485
|
+
let prev = ts[0]!;
|
|
486
|
+
for (let i = 1; i < ts.length; i++) {
|
|
487
|
+
const cur = ts[i]!;
|
|
488
|
+
if (cur - prev > GAP_MS) {
|
|
489
|
+
active += prev - burstStart + BUFFER_MS;
|
|
490
|
+
burstStart = cur;
|
|
491
|
+
}
|
|
492
|
+
prev = cur;
|
|
493
|
+
}
|
|
494
|
+
active += prev - burstStart + BUFFER_MS; // 末尾 burst(单点时 prev-burstStart=0,得 BUFFER_MS=10min)
|
|
495
|
+
return active;
|
|
496
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -131,6 +131,7 @@ export interface ReportSession {
|
|
|
131
131
|
tokenTotal: TokenUsage | null;
|
|
132
132
|
linesTotal: LinesStat | null;
|
|
133
133
|
title?: string | null; // 首条 user 消息(会话标题),读不到为 null
|
|
134
|
+
activeMs?: number; // gap-aware 活跃时长(ms):messageId 去重 + 1h gap 截断 + 10min buffer;旧 daemon 上报可能缺失
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
/** 报告里单个项目(=cwd)的聚合行。 */
|