shine-code-submit 1.0.1 → 1.0.3
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/.claude-plugin/plugin.json +3 -3
- package/README.md +2 -0
- package/dist/install.cjs +1 -1
- package/package.json +4 -4
- package/src/daemon/git.ts +10 -0
- package/src/daemon/server.ts +30 -26
- package/src/daemon/settings.ts +11 -3
- package/src/daemon/ui-assets.ts +2 -2
- package/src/shared/types.ts +1 -13
- package/ui/.build/app.js +42 -42
- package/ui/components/OverviewModule.tsx +2 -2
- package/ui/components/ReportModule.tsx +60 -25
- package/ui/components/SessionDetail.tsx +3 -3
- package/ui/components/SessionTree.tsx +3 -6
- package/ui/components/StatsModule.tsx +3 -3
- package/ui/components/SummaryView.tsx +3 -3
- package/ui/lib/aggregate.ts +2 -1
- package/ui/lib/util.ts +30 -9
- package/ui/style.css +5 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shine-code-submit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Claude Code Hook → 本地常驻 Daemon 的状态/持久化底座。Hook 只做采集+落盘+转发立即退出,重活交给后台 Daemon 异步处理,不拖慢 Claude Code。",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "renguifeng"
|
|
7
7
|
},
|
|
8
|
-
"homepage": "https://github.com/
|
|
9
|
-
"repository": "https://github.com/
|
|
8
|
+
"homepage": "https://github.com/china-shine/shine-code-submit",
|
|
9
|
+
"repository": "https://github.com/china-shine/shine-code-submit.git"
|
|
10
10
|
}
|
package/README.md
CHANGED
|
@@ -141,6 +141,7 @@ src/ shared/ daemon/ hook/ cli/ install/(多端共用源码)
|
|
|
141
141
|
ui/ 查看页(React/TSX,由 daemon 内嵌 HTTP 服务)
|
|
142
142
|
dist/ install.cjs(npm 发布产物,gitignored)
|
|
143
143
|
scripts/ build.ts、build-install.ts、publish.sh、fix-tarball-mode.py
|
|
144
|
+
tokenserver/ 报表上报接收服务(独立子项目,bun+sqlite+React,可打包 Linux 二进制;见 tokenserver/README.md)
|
|
144
145
|
```
|
|
145
146
|
|
|
146
147
|
## 环境变量
|
|
@@ -161,6 +162,7 @@ daemon.pid pid/port/token/startedAt
|
|
|
161
162
|
spool/*.json 待消费事件(每事件一文件,原子写)
|
|
162
163
|
log/daemon.log 日志(按大小轮转)
|
|
163
164
|
db/events.sqlite 事件库(按 cwd 隔离,幂等去重)
|
|
165
|
+
settings.json 上报配置(reportUrl/reportIntervalMin;默认 http://47.98.221.20:36667/api/report,10 分钟)
|
|
164
166
|
```
|
|
165
167
|
|
|
166
168
|
## 关键设计点
|
package/dist/install.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var D=require("node:fs"),qb=require("node:path"),zb=require("node:child_process");var _=require("node:fs"),m=require("node:os"),y=require("node:path"),C=require("node:child_process"),Yb=[1,1,0],Zb=300000;function T(){let b=C.spawnSync("bun",["--version"],{shell:process.platform==="win32",encoding:"utf8"});if(b.status===0&&(b.stdout??"").trim())return"bun";let q=m.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(_.existsSync(Q))return Q;return null}function $b(b){return b.trim().split(".").map((q)=>parseInt(q,10)||0)}function Gb(b,q){let z=$b(b);for(let Q=0;Q<q.length;Q++){let X=z[Q]??0,J=q[Q]??0;if(X>J)return!0;if(X<J)return!1}return!0}function Hb(){let b=process.env.npm_config_registry??"";return/npmmirror|taobao/i.test(b)}function M(b){return C.spawnSync(b,{shell:!0,encoding:"utf8",timeout:Zb,stdio:"inherit"}).status??1}async function f(){let b=T();if(b){let z=C.spawnSync(b,["--version"],{shell:process.platform==="win32",encoding:"utf8"}).stdout?.trim()??"";if(z&&Gb(z,Yb))return console.log(`[shine-code-submit] bun ${z} detected`),b}if(console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装..."),Hb()){if(console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun"),M("npm install -g bun")===0){let z=T();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=T();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 Z=require("node:fs"),Y=require("node:path"),g=require("node:child_process");var p=require("node:os"),V=require("node:path");function E(){return process.env.CLAUDE_CONFIG_DIR||V.join(p.homedir(),".claude")}function x(){return V.join(E(),"plugins")}function j(){return V.join(x(),"known_marketplaces.json")}function B(){return V.join(x(),"installed_plugins.json")}function O(){return V.join(E(),"settings.json")}var S={name:"shine-code-submit",version:"1.0.1",private:!1,type:"module",description:"Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",author:"renguifeng",license:"MIT",repository:{type:"git",url:"https://github.com/renguifeng/shine-code-submit.git"},homepage:"https://github.com/renguifeng/shine-code-submit",bugs:{url:"https://github.com/renguifeng/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 A=require("node:os"),P="shine-code-submit",W=S.version,yb=process.env.SHINE_CODE_SUBMIT_HOST??"0.0.0.0",Jb="127.0.0.1",h=36666,v=`http://${Jb}:${h}`;function Kb(){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=A.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 N=`http://${Kb()}:${h}`;var G="shine-code-submit",I="shine-code-submit";function L(b=W){return Y.join(x(),"cache",G,I,b)}function Fb(){let b=process.argv[1],q=process.cwd();if(b)try{q=Y.dirname(Z.realpathSync(Y.resolve(b)))}catch{q=Y.dirname(Y.resolve(b))}let z=q;for(let Q=0;Q<12;Q++){if(Z.existsSync(Y.join(z,"package.json"))&&Z.existsSync(Y.join(z,".claude-plugin")))return z;let X=Y.dirname(z);if(X===z)break;z=X}return Y.dirname(q)}var Vb=[".claude-plugin","hooks","bin","src","ui","package.json","bun.lock","README.md"];function c(b){let q=L();if(Z.existsSync(q))Z.rmSync(q,{recursive:!0,force:!0});Z.mkdirSync(q,{recursive:!0});let z=Fb();console.log(`[shine-code-submit] 部署源:${z}`);for(let X of Vb){let J=Y.join(z,X);if(!Z.existsSync(J))continue;Z.cpSync(J,Y.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 Z.writeFileSync(Y.join(q,".install-version"),JSON.stringify({version:W,installedAt:Date.now()}),"utf8"),console.log(`[shine-code-submit] 已部署到 ${q}`),q}var $=require("node:fs"),l=require("node:path");function K(b,q){if(!$.existsSync(b))return q;let z;try{z=$.readFileSync(b,"utf8")}catch{return q}try{return JSON.parse(z)}catch{let Q=`${b}.bak-corrupt-${Date.now()}`;try{$.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($.existsSync(b)&&!$.existsSync(z))try{$.copyFileSync(b,z)}catch{}$.mkdirSync(l.dirname(b),{recursive:!0});let Q=`${b}.tmp-${process.pid}`;$.writeFileSync(Q,JSON.stringify(q,null,2),"utf8"),$.renameSync(Q,b)}function R(){return`${I}@${G}`}function d(b){let q=j(),z=K(q,{}),Q=z[G];if(Q?.source&&Q.source.source!=="directory")console.log(`[shine-code-submit] WARNING: marketplace "${G}" 已存在(source=${Q.source.source}),将覆盖为 directory 源(原文件已备份)`);z[G]={source:{source:"directory",path:b},installLocation:b,lastUpdated:new Date().toISOString(),autoUpdate:!1},F(q,z),console.log(`[shine-code-submit] marketplace 已注册 → ${q}`)}function o(b){let q=B(),z=K(q,{version:2,plugins:{}});if(!z.version)z.version=2;if(!z.plugins)z.plugins={};let Q=R(),X=new Date().toISOString(),J=z.plugins[Q]?.[0];z.plugins[Q]=[{scope:"user",installPath:b,version:W,installedAt:J?.installedAt??X,lastUpdated:X}],F(q,z),console.log(`[shine-code-submit] plugin 已注册 → ${q}`)}function s(b){let q=O(),z=K(q,{});if(!z.enabledPlugins)z.enabledPlugins={};let Q=R();if(z.enabledPlugins[Q]=!0,!z.extraKnownMarketplaces)z.extraKnownMarketplaces={};if(!z.extraKnownMarketplaces[G])z.extraKnownMarketplaces[G]={source:{source:"directory",path:b}};F(q,z),console.log(`[shine-code-submit] 已启用(enabledPlugins)→ ${q}`)}function r(){let b=R(),q=K(j(),{});if(q[G])delete q[G],F(j(),q);let z=K(B(),{plugins:{}});if(z.plugins&&z.plugins[b])delete z.plugins[b],F(B(),z);let Q=K(O(),{}),X=!1;if(Q.enabledPlugins&&Q.enabledPlugins[b])delete Q.enabledPlugins[b],X=!0;if(Q.extraKnownMarketplaces&&Q.extraKnownMarketplaces[G])delete Q.extraKnownMarketplaces[G],X=!0;if(X)F(O(),Q);console.log("[shine-code-submit] 已从三处 JSON 移除注册")}async function U(b=400){try{let q=await fetch(`${v}/api/health`,{signal:AbortSignal.timeout(b)});if(!q.ok)return!1;return(await q.json())?.service===P}catch{return!1}}var H=require("node:path"),i=require("node:fs"),t=require("node:os"),Ub=process.env.LOCALAPPDATA??H.join(t.homedir(),".local","share"),u=H.join(Ub,"shine-code-submit"),ub=H.join(u,"spool"),n=H.join(u,"log"),a=H.join(u,"db"),e=H.join(u,"daemon.pid"),lb=H.join(n,"daemon.log"),db=H.join(a,"events.sqlite");function bb(){for(let b of[u,ub,n,a])i.mkdirSync(b,{recursive:!0})}var w=require("node:fs");function k(){try{let b=w.readFileSync(e,"utf8");return JSON.parse(b)}catch{return null}}var[,,Qb]=process.argv;Cb().catch((b)=>{console.error(`[shine-code-submit] ${b instanceof Error?b.message:String(b)}`),process.exit(1)});async function Cb(){switch(Qb){case void 0:case"install":await xb();break;case"uninstall":await jb();break;case"status":await Bb();break;case"--version":case"-v":console.log(W);break;default:wb()}}async function xb(){console.log(`=== shine-code-submit installer v${W} ===`);let b=await f(),q=c(b);d(q),o(q),s(q),bb(),await Ob(b,q),Nb(),console.log(""),console.log("✓ 安装完成。"),console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。"),console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。")}async function jb(){console.log("=== shine-code-submit uninstaller ==="),await vb(),r();let b=L();if(D.existsSync(b))D.rmSync(b,{recursive:!0,force:!0}),console.log(`[shine-code-submit] 已删除 ${b}`);console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。")}async function Bb(){let b=await U(),q=k();if(b&&q)console.log(`daemon: running pid=${q.pid} ${N}`);else console.log("daemon: not running")}async function Ob(b,q){if(await U()){console.log("[shine-code-submit] daemon 已在运行,跳过启动");return}let z=qb.join(q,"src","daemon","main.ts");console.log("[shine-code-submit] 启动 daemon...");try{zb.spawn(b,["run",z],{detached:!0,stdio:"ignore",windowsHide:!0,cwd:q,shell:process.platform==="win32"}).unref()}catch(X){console.error(`[shine-code-submit] 启动 daemon 失败:${X instanceof Error?X.message:X}`),console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");return}let Q=Date.now()+1e4;while(Date.now()<Q)if(await Xb(200),await U()){console.log("[shine-code-submit] daemon 已就绪");return}console.error("[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。")}async function vb(){let b=k();if(!b){console.log("[shine-code-submit] daemon 未运行(无 pid 文件)");return}if(await U()){try{await fetch(`${v}/api/shutdown`,{method:"POST",headers:{authorization:`Bearer ${b.token}`}})}catch{}if(await Xb(1000),await U())try{process.kill(b.pid)}catch{}}console.log("[shine-code-submit] daemon 已停止")}function Nb(){let b=k(),q=b?`${N}/ui?t=${b.token}`:`${N}/ui`;console.log(`[shine-code-submit] Dashboard: ${q}`)}function Xb(b){return new Promise((q)=>setTimeout(q,b))}function wb(){console.log(`shine-code-submit <command>
|
|
2
|
+
var D=require("node:fs"),qb=require("node:path"),zb=require("node:child_process");var _=require("node:fs"),m=require("node:os"),y=require("node:path"),x=require("node:child_process"),Yb=[1,1,0],Zb=300000;function T(){let b=x.spawnSync("bun",["--version"],{shell:process.platform==="win32",encoding:"utf8"});if(b.status===0&&(b.stdout??"").trim())return"bun";let q=m.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(_.existsSync(Q))return Q;return null}function $b(b){return b.trim().split(".").map((q)=>parseInt(q,10)||0)}function Gb(b,q){let z=$b(b);for(let Q=0;Q<q.length;Q++){let X=z[Q]??0,J=q[Q]??0;if(X>J)return!0;if(X<J)return!1}return!0}function Hb(){let b=process.env.npm_config_registry??"";return/npmmirror|taobao/i.test(b)}function M(b){return x.spawnSync(b,{shell:!0,encoding:"utf8",timeout:Zb,stdio:"inherit"}).status??1}async function f(){let b=T();if(b){let z=x.spawnSync(b,["--version"],{shell:process.platform==="win32",encoding:"utf8"}).stdout?.trim()??"";if(z&&Gb(z,Yb))return console.log(`[shine-code-submit] bun ${z} detected`),b}if(console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装..."),Hb()){if(console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun"),M("npm install -g bun")===0){let z=T();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=T();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 Z=require("node:fs"),Y=require("node:path"),I=require("node:child_process");var p=require("node:os"),V=require("node:path");function E(){return process.env.CLAUDE_CONFIG_DIR||V.join(p.homedir(),".claude")}function j(){return V.join(E(),"plugins")}function u(){return V.join(j(),"known_marketplaces.json")}function B(){return V.join(j(),"installed_plugins.json")}function O(){return V.join(E(),"settings.json")}var S={name:"shine-code-submit",version:"1.0.3",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 h=require("node:os"),A="shine-code-submit",W=S.version,yb=process.env.SHINE_CODE_SUBMIT_HOST??"0.0.0.0",Jb="127.0.0.1",P=36666,v=`http://${Jb}:${P}`;function Kb(){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=h.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 N=`http://${Kb()}:${P}`;var G="shine-code-submit",L="shine-code-submit";function g(b=W){return Y.join(j(),"cache",G,L,b)}function Fb(){let b=process.argv[1],q=process.cwd();if(b)try{q=Y.dirname(Z.realpathSync(Y.resolve(b)))}catch{q=Y.dirname(Y.resolve(b))}let z=q;for(let Q=0;Q<12;Q++){if(Z.existsSync(Y.join(z,"package.json"))&&Z.existsSync(Y.join(z,".claude-plugin")))return z;let X=Y.dirname(z);if(X===z)break;z=X}return Y.dirname(q)}var Vb=[".claude-plugin","hooks","bin","src","ui","package.json","bun.lock","README.md"];function c(b){let q=g();if(Z.existsSync(q))Z.rmSync(q,{recursive:!0,force:!0});Z.mkdirSync(q,{recursive:!0});let z=Fb();console.log(`[shine-code-submit] 部署源:${z}`);for(let X of Vb){let J=Y.join(z,X);if(!Z.existsSync(J))continue;Z.cpSync(J,Y.join(q,X),{recursive:!0})}console.log("[shine-code-submit] 安装运行时依赖(bun install)...");let Q=I.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=I.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 Z.writeFileSync(Y.join(q,".install-version"),JSON.stringify({version:W,installedAt:Date.now()}),"utf8"),console.log(`[shine-code-submit] 已部署到 ${q}`),q}var $=require("node:fs"),l=require("node:path");function K(b,q){if(!$.existsSync(b))return q;let z;try{z=$.readFileSync(b,"utf8")}catch{return q}try{return JSON.parse(z)}catch{let Q=`${b}.bak-corrupt-${Date.now()}`;try{$.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($.existsSync(b)&&!$.existsSync(z))try{$.copyFileSync(b,z)}catch{}$.mkdirSync(l.dirname(b),{recursive:!0});let Q=`${b}.tmp-${process.pid}`;$.writeFileSync(Q,JSON.stringify(q,null,2),"utf8"),$.renameSync(Q,b)}function R(){return`${L}@${G}`}function d(b){let q=u(),z=K(q,{}),Q=z[G];if(Q?.source&&Q.source.source!=="directory")console.log(`[shine-code-submit] WARNING: marketplace "${G}" 已存在(source=${Q.source.source}),将覆盖为 directory 源(原文件已备份)`);z[G]={source:{source:"directory",path:b},installLocation:b,lastUpdated:new Date().toISOString(),autoUpdate:!1},F(q,z),console.log(`[shine-code-submit] marketplace 已注册 → ${q}`)}function o(b){let q=B(),z=K(q,{version:2,plugins:{}});if(!z.version)z.version=2;if(!z.plugins)z.plugins={};let Q=R(),X=new Date().toISOString(),J=z.plugins[Q]?.[0];z.plugins[Q]=[{scope:"user",installPath:b,version:W,installedAt:J?.installedAt??X,lastUpdated:X}],F(q,z),console.log(`[shine-code-submit] plugin 已注册 → ${q}`)}function s(b){let q=O(),z=K(q,{});if(!z.enabledPlugins)z.enabledPlugins={};let Q=R();if(z.enabledPlugins[Q]=!0,!z.extraKnownMarketplaces)z.extraKnownMarketplaces={};if(!z.extraKnownMarketplaces[G])z.extraKnownMarketplaces[G]={source:{source:"directory",path:b}};F(q,z),console.log(`[shine-code-submit] 已启用(enabledPlugins)→ ${q}`)}function r(){let b=R(),q=K(u(),{});if(q[G])delete q[G],F(u(),q);let z=K(B(),{plugins:{}});if(z.plugins&&z.plugins[b])delete z.plugins[b],F(B(),z);let Q=K(O(),{}),X=!1;if(Q.enabledPlugins&&Q.enabledPlugins[b])delete Q.enabledPlugins[b],X=!0;if(Q.extraKnownMarketplaces&&Q.extraKnownMarketplaces[G])delete Q.extraKnownMarketplaces[G],X=!0;if(X)F(O(),Q);console.log("[shine-code-submit] 已从三处 JSON 移除注册")}async function U(b=400){try{let q=await fetch(`${v}/api/health`,{signal:AbortSignal.timeout(b)});if(!q.ok)return!1;return(await q.json())?.service===A}catch{return!1}}var H=require("node:path"),i=require("node:fs"),t=require("node:os"),Ub=process.env.LOCALAPPDATA??H.join(t.homedir(),".local","share"),C=H.join(Ub,"shine-code-submit"),Cb=H.join(C,"spool"),n=H.join(C,"log"),a=H.join(C,"db"),e=H.join(C,"daemon.pid"),lb=H.join(n,"daemon.log"),db=H.join(a,"events.sqlite");function bb(){for(let b of[C,Cb,n,a])i.mkdirSync(b,{recursive:!0})}var w=require("node:fs");function k(){try{let b=w.readFileSync(e,"utf8");return JSON.parse(b)}catch{return null}}var[,,Qb]=process.argv;xb().catch((b)=>{console.error(`[shine-code-submit] ${b instanceof Error?b.message:String(b)}`),process.exit(1)});async function xb(){switch(Qb){case void 0:case"install":await jb();break;case"uninstall":await ub();break;case"status":await Bb();break;case"--version":case"-v":console.log(W);break;default:wb()}}async function jb(){console.log(`=== shine-code-submit installer v${W} ===`);let b=await f(),q=c(b);d(q),o(q),s(q),bb(),await Ob(b,q),Nb(),console.log(""),console.log("✓ 安装完成。"),console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。"),console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。")}async function ub(){console.log("=== shine-code-submit uninstaller ==="),await vb(),r();let b=g();if(D.existsSync(b))D.rmSync(b,{recursive:!0,force:!0}),console.log(`[shine-code-submit] 已删除 ${b}`);console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。")}async function Bb(){let b=await U(),q=k();if(b&&q)console.log(`daemon: running pid=${q.pid} ${N}`);else console.log("daemon: not running")}async function Ob(b,q){if(await U()){console.log("[shine-code-submit] daemon 已在运行,跳过启动");return}let z=qb.join(q,"src","daemon","main.ts");console.log("[shine-code-submit] 启动 daemon...");try{zb.spawn(b,["run",z],{detached:!0,stdio:"ignore",windowsHide:!0,cwd:q,shell:process.platform==="win32"}).unref()}catch(X){console.error(`[shine-code-submit] 启动 daemon 失败:${X instanceof Error?X.message:X}`),console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");return}let Q=Date.now()+1e4;while(Date.now()<Q)if(await Xb(200),await U()){console.log("[shine-code-submit] daemon 已就绪");return}console.error("[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。")}async function vb(){let b=k();if(!b){console.log("[shine-code-submit] daemon 未运行(无 pid 文件)");return}if(await U()){try{await fetch(`${v}/api/shutdown`,{method:"POST",headers:{authorization:`Bearer ${b.token}`}})}catch{}if(await Xb(1000),await U())try{process.kill(b.pid)}catch{}}console.log("[shine-code-submit] daemon 已停止")}function Nb(){let b=k(),q=b?`${N}/ui?t=${b.token}`:`${N}/ui`;console.log(`[shine-code-submit] Dashboard: ${q}`)}function Xb(b){return new Promise((q)=>setTimeout(q,b))}function wb(){console.log(`shine-code-submit <command>
|
|
3
3
|
|
|
4
4
|
install 安装插件(自动装 bun + 部署 + 注册 + 启 daemon + 开 dashboard)
|
|
5
5
|
uninstall 卸载(停 daemon + 反注册 + 删文件)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shine-code-submit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Claude Code Hook -> 本地常驻 Daemon 的状态/持久化底座",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/
|
|
11
|
+
"url": "https://github.com/china-shine/shine-code-submit.git"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.com/
|
|
13
|
+
"homepage": "https://github.com/china-shine/shine-code-submit",
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/
|
|
15
|
+
"url": "https://github.com/china-shine/shine-code-submit/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"claude",
|
package/src/daemon/git.ts
CHANGED
|
@@ -42,6 +42,16 @@ export async function getGitUser(cwd: string): Promise<string | null> {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** 取 cwd 的 git remote origin URL;非 git 仓库 / 无 origin / git 不可用返回 null。 */
|
|
46
|
+
export async function getGitRemote(cwd: string): Promise<string | null> {
|
|
47
|
+
try {
|
|
48
|
+
const url = (await runGit(cwd, ["remote", "get-url", "origin"])).trim();
|
|
49
|
+
return url || null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
/** 跑 git 子进程,超时或非 0 退出 reject;stdout 文本 resolve。 */
|
|
46
56
|
function runGit(cwd: string, args: string[]): Promise<string> {
|
|
47
57
|
return new Promise((resolve, reject) => {
|
package/src/daemon/server.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
HookEventType,
|
|
8
8
|
PidFile,
|
|
9
9
|
ReportProject,
|
|
10
|
-
ReportProjectCommits,
|
|
11
10
|
ReportResponse,
|
|
12
11
|
ReportSession,
|
|
13
12
|
ReportTotals,
|
|
@@ -18,7 +17,7 @@ import { deriveStableEventId } from "../shared/id";
|
|
|
18
17
|
import { checkToken } from "./auth";
|
|
19
18
|
import { parseTranscript, sumUsage } from "./transcript";
|
|
20
19
|
import { getSessionTokenTotal } from "./token-cache";
|
|
21
|
-
import { getCommits, getGitUser } from "./git";
|
|
20
|
+
import { getCommits, getGitUser, getGitRemote } from "./git";
|
|
22
21
|
import { readSettings, writeSettings } from "./settings";
|
|
23
22
|
import type { Store } from "./store";
|
|
24
23
|
import type { EventBus } from "./bus";
|
|
@@ -66,8 +65,8 @@ export function startServer(deps: ServerDeps) {
|
|
|
66
65
|
if (Date.now() - lastReportAt < intervalMin * 60_000) return;
|
|
67
66
|
lastReportAt = Date.now();
|
|
68
67
|
try {
|
|
69
|
-
await uploadReport(store);
|
|
70
|
-
log.info(`auto report uploaded to ${url}`);
|
|
68
|
+
const r = await uploadReport(store);
|
|
69
|
+
log.info(r.uploaded ? `auto report uploaded to ${url}` : `auto report skipped: ${r.reason}`);
|
|
71
70
|
} catch (e) {
|
|
72
71
|
log.info(`auto report upload failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
73
72
|
}
|
|
@@ -199,6 +198,17 @@ export function startServer(deps: ServerDeps) {
|
|
|
199
198
|
return json(await buildReport(store, since));
|
|
200
199
|
}
|
|
201
200
|
|
|
201
|
+
// 手动上报:构建报表并 POST 到 settings.reportUrl(与定时器同一逻辑)。
|
|
202
|
+
if (path === "/api/report/upload" && req.method === "POST") {
|
|
203
|
+
try {
|
|
204
|
+
const r = await uploadReport(store);
|
|
205
|
+
return json(r.uploaded ? { status: "ok" } : { status: "skipped", reason: r.reason });
|
|
206
|
+
} catch (e) {
|
|
207
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
208
|
+
return json({ error: msg }, 500);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
202
212
|
// 用户设置:GET 读、PUT 写(字段级合并)。目前只有 reportUrl(上报地址)。
|
|
203
213
|
if (path === "/api/settings" && req.method === "GET") {
|
|
204
214
|
return json(readSettings());
|
|
@@ -255,7 +265,7 @@ function findTranscriptPath(store: Store, sessionId: string): string | null {
|
|
|
255
265
|
return null;
|
|
256
266
|
}
|
|
257
267
|
|
|
258
|
-
/** 构建 /api/report:按项目(cwd)聚合会话/token
|
|
268
|
+
/** 构建 /api/report:按项目(cwd)聚合会话/token + git 用户 + 仓库地址 + 版本。窗口 since(ms,0=全部)。 */
|
|
259
269
|
async function buildReport(store: Store, since: number): Promise<ReportResponse> {
|
|
260
270
|
const sessions = store.sessions().filter((s) => s.lastActive >= since);
|
|
261
271
|
const byCwd = new Map<string, SessionSummary[]>();
|
|
@@ -269,7 +279,6 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
|
|
|
269
279
|
[...byCwd.keys()].map(
|
|
270
280
|
async (cwd): Promise<ReportProject> => {
|
|
271
281
|
const ss = byCwd.get(cwd) ?? [];
|
|
272
|
-
// 每会话 token:从 transcript 汇总(带 mtime 缓存)。读不到为 null。
|
|
273
282
|
const rs: ReportSession[] = ss.map((s) => {
|
|
274
283
|
const tp = findTranscriptPath(store, s.sessionId);
|
|
275
284
|
return {
|
|
@@ -280,26 +289,16 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
|
|
|
280
289
|
});
|
|
281
290
|
const totalTokens = sumTokens(rs.map((r) => r.tokenTotal));
|
|
282
291
|
|
|
283
|
-
|
|
284
|
-
const [gitUser, commitsResp] = await Promise.all([getGitUser(cwd), getCommits(cwd, 200)]);
|
|
285
|
-
const inRange = (commitsResp.commits ?? []).filter((c) => c.time >= since);
|
|
286
|
-
const commits: ReportProjectCommits = {
|
|
287
|
-
count: inRange.length,
|
|
288
|
-
added: inRange.reduce((a, c) => a + c.added, 0),
|
|
289
|
-
deleted: inRange.reduce((a, c) => a + c.deleted, 0),
|
|
290
|
-
lastTime: inRange.length ? Math.max(...inRange.map((c) => c.time)) : null,
|
|
291
|
-
};
|
|
292
|
+
const [gitUser, gitRemote] = await Promise.all([getGitUser(cwd), getGitRemote(cwd)]);
|
|
292
293
|
|
|
293
294
|
return {
|
|
294
295
|
cwd,
|
|
295
296
|
name: shortName(cwd),
|
|
296
297
|
gitUser,
|
|
298
|
+
gitRemote,
|
|
297
299
|
sessionCount: ss.length,
|
|
298
300
|
sessions: rs,
|
|
299
301
|
totalTokens,
|
|
300
|
-
commits,
|
|
301
|
-
recentCommits: inRange.slice(0, 5),
|
|
302
|
-
...(commitsResp.error ? { gitError: commitsResp.error } : {}),
|
|
303
302
|
};
|
|
304
303
|
},
|
|
305
304
|
),
|
|
@@ -315,9 +314,6 @@ async function buildReport(store: Store, since: number): Promise<ReportResponse>
|
|
|
315
314
|
projects: projects.length,
|
|
316
315
|
sessions: sessions.length,
|
|
317
316
|
tokens: sumTokens(projects.map((p) => p.totalTokens)),
|
|
318
|
-
commitCount: projects.reduce((a, p) => a + p.commits.count, 0),
|
|
319
|
-
added: projects.reduce((a, p) => a + p.commits.added, 0),
|
|
320
|
-
deleted: projects.reduce((a, p) => a + p.commits.deleted, 0),
|
|
321
317
|
};
|
|
322
318
|
|
|
323
319
|
return {
|
|
@@ -352,18 +348,26 @@ function shortName(p: string): string {
|
|
|
352
348
|
return i >= 0 ? t.slice(i + 1) : t;
|
|
353
349
|
}
|
|
354
350
|
|
|
355
|
-
/**
|
|
356
|
-
|
|
351
|
+
/** 上报结果:uploaded=true 已 POST;false=主动跳过(附 reason);抛错=网络/服务端失败。 */
|
|
352
|
+
type UploadOutcome = { uploaded: boolean; reason?: string };
|
|
353
|
+
|
|
354
|
+
/** 构建 report 并 POST 到 settings.reportUrl(自动/手动上报共用)。
|
|
355
|
+
* 无 reportUrl,或采集不到 git user.name(上报身份缺失,tokenserver 只会落「未知用户」) 则跳过不报,返回原因由调用方记日志/回前端;失败抛错。 */
|
|
356
|
+
async function uploadReport(store: Store): Promise<UploadOutcome> {
|
|
357
357
|
const s = readSettings();
|
|
358
358
|
const url = s.reportUrl;
|
|
359
|
-
if (!url) return;
|
|
360
|
-
const
|
|
359
|
+
if (!url) return { uploaded: false, reason: "reportUrl 未配置" };
|
|
360
|
+
const report = await buildReport(store, 0);
|
|
361
|
+
if (!report.gitUser) {
|
|
362
|
+
return { uploaded: false, reason: "未采集到 git user.name,跳过上报(无上报身份)" };
|
|
363
|
+
}
|
|
361
364
|
await fetch(url, {
|
|
362
365
|
method: "POST",
|
|
363
366
|
headers: { "content-type": "application/json" },
|
|
364
|
-
body,
|
|
367
|
+
body: JSON.stringify(report),
|
|
365
368
|
signal: AbortSignal.timeout(15000),
|
|
366
369
|
});
|
|
370
|
+
return { uploaded: true };
|
|
367
371
|
}
|
|
368
372
|
|
|
369
373
|
function normalizeEvent(type: HookEventType, body: unknown): HookEvent | null {
|
package/src/daemon/settings.ts
CHANGED
|
@@ -11,13 +11,21 @@ export interface Settings {
|
|
|
11
11
|
reportIntervalMin?: number | null; // 自动上报间隔(分钟);>0 启用,空/0=不自动上报
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/** 默认设置:上报到 tokenserver 公网地址,每 10 分钟一次。 */
|
|
15
|
+
const DEFAULTS: Settings = {
|
|
16
|
+
reportUrl: "http://47.98.221.20:36667/api/report",
|
|
17
|
+
reportIntervalMin: 10,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** 读设置;文件不存在/损坏返回默认值,已存字段覆盖默认(含 null)。 */
|
|
15
21
|
export function readSettings(): Settings {
|
|
22
|
+
let s: Settings;
|
|
16
23
|
try {
|
|
17
|
-
|
|
24
|
+
s = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as Settings;
|
|
18
25
|
} catch {
|
|
19
|
-
|
|
26
|
+
s = {};
|
|
20
27
|
}
|
|
28
|
+
return { ...DEFAULTS, ...s };
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
/** 写设置(整体覆盖)。写失败静默——GET 仍返回上次成功写入的值。 */
|