shine-code-submit 1.0.11 → 1.0.12
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 +1 -1
- package/package.json +1 -1
- package/src/daemon/claude-scan.ts +128 -0
- package/src/daemon/server.ts +38 -24
- package/src/daemon/ui-assets.ts +1 -1
- package/ui/.build/app.js +31 -31
- package/ui/components/OverviewModule.tsx +21 -8
- package/ui/components/ReportModule.tsx +5 -5
- package/ui/components/SessionDetail.tsx +2 -2
- package/ui/components/SessionTree.tsx +3 -3
- package/ui/components/StatsModule.tsx +3 -3
- package/ui/components/SummaryView.tsx +3 -3
- package/ui/lib/aggregate.ts +2 -4
- package/ui/lib/util.ts +11 -12
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.11",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.12",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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// ccusage 式 transcript 扫描:直接读 ~/.claude/projects 下所有 jsonl,按 session 归组,
|
|
2
|
+
// 使 daemon 的 token 总量与 `ccusage claude session` 完全一致(不依赖 hook 是否抓到)。
|
|
3
|
+
// 逻辑照搬 ccusage rust/crates/ccusage/src/adapter/claude/paths.rs 的 claude_paths + session 归组。
|
|
4
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { getSessionTokenTotal } from "./token-cache";
|
|
9
|
+
import type { TokenUsage } from "../shared/types";
|
|
10
|
+
|
|
11
|
+
export interface ScannedSession {
|
|
12
|
+
/** 编码后的项目目录名(projects/<project>,如 C--Users-ren-Desktop-workspace-ccusage)。 */
|
|
13
|
+
project: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
/** 父 transcript 绝对路径(subagents 由 getSessionTokenTotal 自动并入)。 */
|
|
16
|
+
transcriptPath: string;
|
|
17
|
+
/** 父文件 mtime(session 级活跃时间,用于 since 过滤)。 */
|
|
18
|
+
lastActivity: number;
|
|
19
|
+
tokenTotal: TokenUsage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ZERO: TokenUsage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
|
|
23
|
+
|
|
24
|
+
/** 展开 ~ 为 home。 */
|
|
25
|
+
function expandHome(p: string): string {
|
|
26
|
+
return p.replace(/^~/, homedir());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 解析 Claude 配置目录(含 projects/ 子目录的),等价 ccusage claude_paths。
|
|
30
|
+
* CLAUDE_CONFIG_DIR(逗号分隔,可指向目录本身或其 projects/)|$XDG_CONFIG_HOME/claude|~/.claude。 */
|
|
31
|
+
export function claudeProjectsRoots(): string[] {
|
|
32
|
+
const roots: string[] = [];
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const addIfHasProjects = (p: string): void => {
|
|
35
|
+
const dir = p.trim();
|
|
36
|
+
if (!dir) return;
|
|
37
|
+
if (existsSync(join(dir, "projects")) && !seen.has(dir)) {
|
|
38
|
+
seen.add(dir);
|
|
39
|
+
roots.push(dir);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
|
44
|
+
if (envDir) {
|
|
45
|
+
for (const raw of envDir.split(",")) {
|
|
46
|
+
let p = expandHome(raw.trim());
|
|
47
|
+
if (basename(p) === "projects") p = dirname(p); // 指向 projects/ 本身则取父
|
|
48
|
+
addIfHasProjects(p);
|
|
49
|
+
}
|
|
50
|
+
if (roots.length) return roots;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const home = homedir();
|
|
54
|
+
const xdg = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
55
|
+
addIfHasProjects(join(xdg, "claude"));
|
|
56
|
+
addIfHasProjects(join(home, ".claude"));
|
|
57
|
+
return roots;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 递归收集 dir 下所有 *.jsonl 绝对路径。 */
|
|
61
|
+
function collectJsonl(dir: string, out: string[]): void {
|
|
62
|
+
let names: string[];
|
|
63
|
+
try {
|
|
64
|
+
names = readdirSync(dir);
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
const p = join(dir, name);
|
|
70
|
+
let st;
|
|
71
|
+
try {
|
|
72
|
+
st = statSync(p);
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (st.isDirectory()) collectJsonl(p, out);
|
|
77
|
+
else if (name.endsWith(".jsonl")) out.push(p);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** projects/<project>/<session>.jsonl 的相对段(去掉 .jsonl)取 sessionId;project 取 projects 后第一段。
|
|
82
|
+
* 跳过 subagents/ 下的文件(由父 session 的 getSessionTokenTotal 自动并入),跳过非标准嵌套。 */
|
|
83
|
+
function parentSessionInfo(
|
|
84
|
+
pathParts: string[],
|
|
85
|
+
projectsIndex: number,
|
|
86
|
+
): { project: string; sessionId: string } | null {
|
|
87
|
+
const rel = pathParts.slice(projectsIndex + 1); // projects/ 之后的段
|
|
88
|
+
if (rel.length < 2) return null;
|
|
89
|
+
if (rel.some((seg) => seg === "subagents")) return null; // 子代理,跳过
|
|
90
|
+
if (rel.length !== 2) return null; // 非标准父(如 projects/<proj>/<x>/foo.jsonl),跳过避免误归
|
|
91
|
+
const project = rel[0];
|
|
92
|
+
const sessionId = (rel[1] ?? "").replace(/\.jsonl$/, "");
|
|
93
|
+
if (!project || !sessionId) return null;
|
|
94
|
+
return { project, sessionId };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 扫描所有 Claude transcript,按 session 归组并算 token(ccusage 口径,含子代理)。
|
|
98
|
+
* 贵的汇总走 getSessionTokenTotal 的 mtime 缓存,遍历只列文件。 */
|
|
99
|
+
export function scanSessions(): ScannedSession[] {
|
|
100
|
+
const out: ScannedSession[] = [];
|
|
101
|
+
for (const root of claudeProjectsRoots()) {
|
|
102
|
+
const projectsDir = join(root, "projects");
|
|
103
|
+
const files: string[] = [];
|
|
104
|
+
collectJsonl(projectsDir, files);
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const parts = file.split(/[/\\]/);
|
|
107
|
+
const projectsIndex = parts.lastIndexOf("projects");
|
|
108
|
+
if (projectsIndex < 0) continue;
|
|
109
|
+
const info = parentSessionInfo(parts, projectsIndex);
|
|
110
|
+
if (!info) continue;
|
|
111
|
+
let mtimeMs: number;
|
|
112
|
+
try {
|
|
113
|
+
mtimeMs = statSync(file).mtimeMs;
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const tokenTotal = getSessionTokenTotal(file) ?? ZERO;
|
|
118
|
+
out.push({
|
|
119
|
+
project: info.project,
|
|
120
|
+
sessionId: info.sessionId,
|
|
121
|
+
transcriptPath: file,
|
|
122
|
+
lastActivity: mtimeMs,
|
|
123
|
+
tokenTotal,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
package/src/daemon/server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { deriveStableEventId } from "../shared/id";
|
|
|
17
17
|
import { checkToken } from "./auth";
|
|
18
18
|
import { parseTranscript, sumUsage } from "./transcript";
|
|
19
19
|
import { getSessionTokenTotal } from "./token-cache";
|
|
20
|
+
import { scanSessions, type ScannedSession } from "./claude-scan";
|
|
20
21
|
import { getCommits, getGitUser, getGitRemote } from "./git";
|
|
21
22
|
import { getSessionLines, sumLines } from "./lines";
|
|
22
23
|
import { readSettings, writeSettings } from "./settings";
|
|
@@ -298,35 +299,48 @@ function findTranscriptPath(store: Store, sessionId: string): string | null {
|
|
|
298
299
|
return null;
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
/**
|
|
302
|
+
/** 解码 Claude 项目目录名 → 真实 cwd:':' '\' '/' 都被编码成 '-'。
|
|
303
|
+
* Windows 盘符 C--… → C:\…(其余 - → \);无盘符时原样返回(posix 情形,少見)。 */
|
|
304
|
+
function decodeProjectCwd(project: string): string {
|
|
305
|
+
const m = /^([A-Za-z])--(.*)$/.exec(project);
|
|
306
|
+
if (m && m[1] && m[2]) return `${m[1]}:\\${m[2].replace(/-/g, "\\")}`;
|
|
307
|
+
return project;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** 构建 /api/report:token 直接扫所有 transcript(ccusage 口径,与 `ccusage claude session` 的 totals 逐字段相等),
|
|
311
|
+
* 按项目(projects/<project>)聚合;cwd/git/代码行数尽量按 sessionId 匹配 hook session 取真实值,匹配不上则解码项目名。
|
|
312
|
+
* 窗口 since(ms,0=全部);since>0 时按 session 文件 mtime(session 级)过滤。 */
|
|
302
313
|
async function buildReport(store: Store, since: number): Promise<ReportResponse> {
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
const ex = seen.get(s.sessionId);
|
|
308
|
-
if (!ex || s.lastActive > ex.lastActive) seen.set(s.sessionId, s);
|
|
314
|
+
// hook session 的 sessionId → 真实 cwd,用于给 scan session 补 cwd/git/行数
|
|
315
|
+
const hookCwd = new Map<string, string>();
|
|
316
|
+
for (const s of store.sessions()) {
|
|
317
|
+
if (!hookCwd.has(s.sessionId)) hookCwd.set(s.sessionId, s.cwd);
|
|
309
318
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
319
|
+
|
|
320
|
+
// 扫描所有 transcript(ccusage 口径,含子代理),按 since 过滤
|
|
321
|
+
const scanned = scanSessions().filter((s) => since <= 0 || s.lastActivity >= since);
|
|
322
|
+
|
|
323
|
+
// 按编码项目名分组
|
|
324
|
+
const byProject = new Map<string, ScannedSession[]>();
|
|
325
|
+
for (const s of scanned) {
|
|
326
|
+
const arr = byProject.get(s.project);
|
|
313
327
|
if (arr) arr.push(s);
|
|
314
|
-
else
|
|
328
|
+
else byProject.set(s.project, [s]);
|
|
315
329
|
}
|
|
316
330
|
|
|
317
331
|
const projects = await Promise.all(
|
|
318
|
-
[...
|
|
319
|
-
async (
|
|
320
|
-
const ss =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
});
|
|
332
|
+
[...byProject.keys()].map(
|
|
333
|
+
async (project): Promise<ReportProject> => {
|
|
334
|
+
const ss = byProject.get(project) ?? [];
|
|
335
|
+
// cwd:优先 hook 匹配的真实 cwd,否则解码项目名
|
|
336
|
+
const cwd =
|
|
337
|
+
ss.map((s) => hookCwd.get(s.sessionId)).find((c) => !!c) ?? decodeProjectCwd(project);
|
|
338
|
+
const rs: ReportSession[] = ss.map((s) => ({
|
|
339
|
+
sessionId: s.sessionId,
|
|
340
|
+
lastActive: s.lastActivity,
|
|
341
|
+
tokenTotal: s.tokenTotal,
|
|
342
|
+
linesTotal: getSessionLines(store, s.sessionId, s.lastActivity),
|
|
343
|
+
}));
|
|
330
344
|
const totalTokens = sumTokens(rs.map((r) => r.tokenTotal));
|
|
331
345
|
const totalLines = sumLines(rs.map((r) => r.linesTotal));
|
|
332
346
|
|
|
@@ -354,7 +368,7 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
|
|
|
354
368
|
|
|
355
369
|
const totals: ReportTotals = {
|
|
356
370
|
projects: projects.length,
|
|
357
|
-
sessions:
|
|
371
|
+
sessions: scanned.length,
|
|
358
372
|
tokens: sumTokens(projects.map((p) => p.totalTokens)),
|
|
359
373
|
lines: sumLines(projects.map((p) => p.totalLines)),
|
|
360
374
|
};
|