shine-code-submit 1.0.4 → 1.0.5
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 +1 -1
- package/dist/install.cjs +2 -2
- package/package.json +1 -1
- package/src/cli/main.ts +9 -45
- package/src/daemon/server.ts +1 -1
- package/src/hook/main.ts +17 -8
- package/src/install/main.ts +15 -34
- package/src/shared/daemonctl.ts +45 -10
package/dist/install.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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"),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.4",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>
|
|
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.5",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 + 反注册 + 删文件)
|
|
6
6
|
status 显示 daemon 状态
|
|
7
7
|
|
|
8
|
-
通常通过 npx 跑:npx shine-code-submit install`),process.exit(
|
|
8
|
+
通常通过 npx 跑:npx shine-code-submit install`),process.exit(Z1?1:0)}
|
package/package.json
CHANGED
package/src/cli/main.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// shine-code-submit CLI:status / start / stop / restart / ui。
|
|
2
2
|
// 用户侧管理命令。token 从 pid 文件读取。
|
|
3
3
|
import { BASE_URL, PUBLIC_BASE_URL } from "../shared/config";
|
|
4
|
-
import { readPidFile
|
|
5
|
-
import { ensureDaemon, isOursAlive, openBrowser, spawnDaemon } from "../shared/daemonctl";
|
|
4
|
+
import { readPidFile } from "../shared/pidfile";
|
|
5
|
+
import { ensureDaemon, isOursAlive, openBrowser, spawnDaemon, stopDaemon } from "../shared/daemonctl";
|
|
6
6
|
|
|
7
7
|
const [cmd] = process.argv.slice(2);
|
|
8
8
|
|
|
@@ -38,51 +38,19 @@ async function cmdStatus(): Promise<void> {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async function cmdStart(): Promise<void> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
spawnDaemon();
|
|
46
|
-
const ok = await waitReady();
|
|
47
|
-
console.log(ok ? "daemon: started" : "daemon: start failed (check %LOCALAPPDATA%/shine-code-submit/log/daemon.log)");
|
|
41
|
+
// ensureDaemon 版本感知:已是最新复用,旧版在跑则停旧启新,没跑则拉起
|
|
42
|
+
const ok = await ensureDaemon();
|
|
43
|
+
console.log(ok ? "daemon: running" : "daemon: start failed (check %LOCALAPPDATA%/shine-code-submit/log/daemon.log)");
|
|
48
44
|
}
|
|
49
45
|
|
|
50
46
|
async function cmdStop(): Promise<void> {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (!(await isOursAlive())) {
|
|
57
|
-
console.log("daemon: not running (stale pid file)");
|
|
58
|
-
removePidFile();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
// 优雅停止:调用 /api/shutdown
|
|
62
|
-
try {
|
|
63
|
-
await fetch(`${BASE_URL}/api/shutdown`, {
|
|
64
|
-
method: "POST",
|
|
65
|
-
headers: { authorization: `Bearer ${pid.token}` },
|
|
66
|
-
});
|
|
67
|
-
} catch {
|
|
68
|
-
/* ignore */
|
|
69
|
-
}
|
|
70
|
-
await sleep(1000);
|
|
71
|
-
if (await isOursAlive()) {
|
|
72
|
-
try {
|
|
73
|
-
process.kill(pid.pid); // 兜底强杀
|
|
74
|
-
} catch {
|
|
75
|
-
/* ignore */
|
|
76
|
-
}
|
|
77
|
-
removePidFile();
|
|
78
|
-
console.log("daemon: force-stopped");
|
|
79
|
-
} else {
|
|
80
|
-
console.log("daemon: stopped");
|
|
81
|
-
}
|
|
47
|
+
const wasAlive = await isOursAlive();
|
|
48
|
+
await stopDaemon();
|
|
49
|
+
console.log(wasAlive ? "daemon: stopped" : "daemon: not running");
|
|
82
50
|
}
|
|
83
51
|
|
|
84
52
|
async function cmdRestart(): Promise<void> {
|
|
85
|
-
await
|
|
53
|
+
await stopDaemon();
|
|
86
54
|
spawnDaemon();
|
|
87
55
|
const ok = await waitReady();
|
|
88
56
|
console.log(ok ? "daemon: restarted" : "daemon: restart failed");
|
|
@@ -108,10 +76,6 @@ async function waitReady(): Promise<boolean> {
|
|
|
108
76
|
return ensureDaemon();
|
|
109
77
|
}
|
|
110
78
|
|
|
111
|
-
function sleep(ms: number): Promise<void> {
|
|
112
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
79
|
function printHelp(): void {
|
|
116
80
|
console.log(`shine-code-submit <command>
|
|
117
81
|
|
package/src/daemon/server.ts
CHANGED
|
@@ -126,7 +126,7 @@ export function startServer(deps: ServerDeps) {
|
|
|
126
126
|
stats.recordEvent();
|
|
127
127
|
log.info(`ingest http ${event.type}`);
|
|
128
128
|
}
|
|
129
|
-
return json({ status: "ok", inserted });
|
|
129
|
+
return json({ status: "ok", inserted, version: SERVICE_VERSION });
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (path === "/api/stats" && req.method === "GET") {
|
package/src/hook/main.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { ensureDirs } from "../shared/paths";
|
|
7
7
|
import { readToken } from "../shared/pidfile";
|
|
8
8
|
import { writeSpoolFile } from "../shared/spool";
|
|
9
|
-
import { BASE_URL, PUBLIC_BASE_URL, HOOK_POST_TIMEOUT_MS } from "../shared/config";
|
|
10
|
-
import { ensureDaemon, openBrowser } from "../shared/daemonctl";
|
|
9
|
+
import { BASE_URL, PUBLIC_BASE_URL, HOOK_POST_TIMEOUT_MS, SERVICE_VERSION } from "../shared/config";
|
|
10
|
+
import { ensureDaemon, openBrowser, spawnDaemon, stopDaemon } from "../shared/daemonctl";
|
|
11
11
|
import type { HookEvent, HookEventType } from "../shared/types";
|
|
12
12
|
|
|
13
13
|
const VALID_TYPES: HookEventType[] = [
|
|
@@ -122,16 +122,24 @@ async function readStdin(): Promise<unknown> {
|
|
|
122
122
|
async function forward(event: HookEvent): Promise<void> {
|
|
123
123
|
const token = readToken();
|
|
124
124
|
const url = `${BASE_URL}/api/hook/${event.type}`;
|
|
125
|
-
|
|
125
|
+
const r = await postOnce(url, event, token);
|
|
126
|
+
if (r.ok) {
|
|
127
|
+
// 升级检测:daemon 版本旧 → 停旧启新(不等 ready;本次事件已入库 + 已落 spool,新 daemon 起来后回捞后续)
|
|
128
|
+
if (r.version && r.version !== SERVICE_VERSION) {
|
|
129
|
+
await stopDaemon();
|
|
130
|
+
spawnDaemon();
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
126
134
|
// 热转发失败 → 故障路径
|
|
127
135
|
await ensureDaemon();
|
|
128
|
-
// 重读 token
|
|
136
|
+
// 重读 token(拉起后 pid 文件已更新)
|
|
129
137
|
await postOnce(url, event, readToken() ?? token);
|
|
130
138
|
}
|
|
131
139
|
|
|
132
|
-
async function postOnce(url: string, event: HookEvent, token: string | null): Promise<boolean> {
|
|
140
|
+
async function postOnce(url: string, event: HookEvent, token: string | null): Promise<{ ok: boolean; version?: string }> {
|
|
133
141
|
try {
|
|
134
|
-
await fetch(url, {
|
|
142
|
+
const res = await fetch(url, {
|
|
135
143
|
method: "POST",
|
|
136
144
|
headers: {
|
|
137
145
|
"content-type": "application/json",
|
|
@@ -140,9 +148,10 @@ async function postOnce(url: string, event: HookEvent, token: string | null): Pr
|
|
|
140
148
|
body: JSON.stringify(event),
|
|
141
149
|
signal: AbortSignal.timeout(HOOK_POST_TIMEOUT_MS),
|
|
142
150
|
});
|
|
143
|
-
|
|
151
|
+
const data = (await res.json().catch(() => ({}))) as { version?: string };
|
|
152
|
+
return { ok: true, version: data.version }; // 到达 daemon 即视为成功(事件也已落盘,回捞兜底);顺带读 version
|
|
144
153
|
} catch {
|
|
145
|
-
return false;
|
|
154
|
+
return { ok: false };
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
package/src/install/main.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { spawn } from "node:child_process";
|
|
|
7
7
|
import { ensureBun } from "./bun";
|
|
8
8
|
import { cacheDir, deployPlugin } from "./deploy";
|
|
9
9
|
import { enablePlugin, registerMarketplace, registerPlugin, unregisterAll } from "./register";
|
|
10
|
-
import {
|
|
11
|
-
import { isOursAlive, openBrowser } from "../shared/daemonctl";
|
|
10
|
+
import { PUBLIC_BASE_URL, SERVICE_VERSION } from "../shared/config";
|
|
11
|
+
import { isOursAlive, openBrowser, probeDaemon, stopDaemon } from "../shared/daemonctl";
|
|
12
12
|
import { ensureDirs } from "../shared/paths";
|
|
13
13
|
import { readPidFile } from "../shared/pidfile";
|
|
14
14
|
|
|
@@ -78,14 +78,21 @@ async function runStatus(): Promise<void> {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** 用显式 bunPath 拉 daemon。不调 daemonctl.spawnDaemon——它用 process.execPath,install 场景是 node 会出错。
|
|
81
|
+
/** 用显式 bunPath 拉 daemon。不调 daemonctl.spawnDaemon——它用 process.execPath,install 场景是 node 会出错。
|
|
82
|
+
* 版本感知:已是当前 SERVICE_VERSION 则复用;旧版在跑则停旧启新(daemon 自守,必须先停后启);没跑则直接启。 */
|
|
82
83
|
async function startDaemonWithBun(bunPath: string, cachePath: string): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const probe = await probeDaemon();
|
|
85
|
+
if (probe.alive && probe.version === SERVICE_VERSION) {
|
|
86
|
+
console.log(`[shine-code-submit] daemon 已是最新 v${SERVICE_VERSION},跳过启动`);
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
89
|
+
if (probe.alive) {
|
|
90
|
+
console.log(`[shine-code-submit] daemon 旧版 v${probe.version} 运行中,重启到 v${SERVICE_VERSION}...`);
|
|
91
|
+
await stopDaemon();
|
|
92
|
+
} else {
|
|
93
|
+
console.log("[shine-code-submit] 启动 daemon...");
|
|
94
|
+
}
|
|
87
95
|
const daemonSrc = join(cachePath, "src", "daemon", "main.ts");
|
|
88
|
-
console.log("[shine-code-submit] 启动 daemon...");
|
|
89
96
|
try {
|
|
90
97
|
const child = spawn(bunPath, ["run", daemonSrc], {
|
|
91
98
|
detached: true,
|
|
@@ -103,7 +110,8 @@ async function startDaemonWithBun(bunPath: string, cachePath: string): Promise<v
|
|
|
103
110
|
const deadline = Date.now() + 10_000;
|
|
104
111
|
while (Date.now() < deadline) {
|
|
105
112
|
await sleep(200);
|
|
106
|
-
|
|
113
|
+
const p = await probeDaemon();
|
|
114
|
+
if (p.alive && p.version === SERVICE_VERSION) {
|
|
107
115
|
console.log("[shine-code-submit] daemon 已就绪");
|
|
108
116
|
return;
|
|
109
117
|
}
|
|
@@ -113,33 +121,6 @@ async function startDaemonWithBun(bunPath: string, cachePath: string): Promise<v
|
|
|
113
121
|
);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
async function stopDaemon(): Promise<void> {
|
|
117
|
-
const pid = readPidFile();
|
|
118
|
-
if (!pid) {
|
|
119
|
-
console.log("[shine-code-submit] daemon 未运行(无 pid 文件)");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (await isOursAlive()) {
|
|
123
|
-
try {
|
|
124
|
-
await fetch(`${BASE_URL}/api/shutdown`, {
|
|
125
|
-
method: "POST",
|
|
126
|
-
headers: { authorization: `Bearer ${pid.token}` },
|
|
127
|
-
});
|
|
128
|
-
} catch {
|
|
129
|
-
/* ignore */
|
|
130
|
-
}
|
|
131
|
-
await sleep(1000);
|
|
132
|
-
if (await isOursAlive()) {
|
|
133
|
-
try {
|
|
134
|
-
process.kill(pid.pid);
|
|
135
|
-
} catch {
|
|
136
|
-
/* ignore */
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
console.log("[shine-code-submit] daemon 已停止");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
124
|
function openDashboard(): void {
|
|
144
125
|
const pid = readPidFile();
|
|
145
126
|
const url = pid ? `${PUBLIC_BASE_URL}/ui?t=${pid.token}` : `${PUBLIC_BASE_URL}/ui`;
|
package/src/shared/daemonctl.ts
CHANGED
|
@@ -4,20 +4,27 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { BASE_URL, HEALTH_POLL_TIMEOUT_MS, HEALTH_POLL_INTERVAL_MS, SERVICE_NAME } from "./config";
|
|
7
|
+
import { BASE_URL, HEALTH_POLL_TIMEOUT_MS, HEALTH_POLL_INTERVAL_MS, SERVICE_NAME, SERVICE_VERSION } from "./config";
|
|
8
|
+
import { readPidFile, removePidFile } from "./pidfile";
|
|
8
9
|
|
|
9
|
-
/** 探活 +
|
|
10
|
-
export async function
|
|
10
|
+
/** 探活 + 认自己人 + 取版本:service 必须匹配(防端口被无关程序占用误判),顺便读 version。 */
|
|
11
|
+
export async function probeDaemon(timeoutMs = 400): Promise<{ alive: boolean; version?: string }> {
|
|
11
12
|
try {
|
|
12
13
|
const res = await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
13
|
-
if (!res.ok) return false;
|
|
14
|
-
const data = (await res.json()) as { service?: string };
|
|
15
|
-
|
|
14
|
+
if (!res.ok) return { alive: false };
|
|
15
|
+
const data = (await res.json()) as { service?: string; version?: string };
|
|
16
|
+
if (data?.service !== SERVICE_NAME) return { alive: false };
|
|
17
|
+
return { alive: true, version: data.version };
|
|
16
18
|
} catch {
|
|
17
|
-
return false;
|
|
19
|
+
return { alive: false };
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/** 探活 + 认自己人(只看存活,不比版本)。向后兼容;需比版本用 probeDaemon。 */
|
|
24
|
+
export async function isOursAlive(timeoutMs = 400): Promise<boolean> {
|
|
25
|
+
return (await probeDaemon(timeoutMs)).alive;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* detached 拉起 daemon。
|
|
23
30
|
* 优先级:env 覆盖 > 同目录 daemon 二进制(二进制模式)> bun run src/daemon/main.ts(源码模式)。
|
|
@@ -55,14 +62,42 @@ export function spawnDaemon(): void {
|
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
/**
|
|
65
|
+
/** 停 daemon:POST /api/shutdown(优雅) → sleep 1s → 仍活强杀 → 清 stale pid 文件。 */
|
|
66
|
+
export async function stopDaemon(): Promise<void> {
|
|
67
|
+
const pid = readPidFile();
|
|
68
|
+
if (!pid) return;
|
|
69
|
+
if (await isOursAlive()) {
|
|
70
|
+
try {
|
|
71
|
+
await fetch(`${BASE_URL}/api/shutdown`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { authorization: `Bearer ${pid.token}` },
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
await sleep(1000);
|
|
79
|
+
if (await isOursAlive()) {
|
|
80
|
+
try {
|
|
81
|
+
process.kill(pid.pid);
|
|
82
|
+
} catch {
|
|
83
|
+
/* ignore */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
removePidFile();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 确保 daemon 就绪且版本 == 当前 SERVICE_VERSION:没跑则拉起;旧版在跑则停旧启新(daemon 自守,必须先停后启)。 */
|
|
59
91
|
export async function ensureDaemon(): Promise<boolean> {
|
|
60
|
-
|
|
92
|
+
const probe = await probeDaemon();
|
|
93
|
+
if (probe.alive && probe.version === SERVICE_VERSION) return true;
|
|
94
|
+
if (probe.alive) await stopDaemon(); // 旧版:先停,新的才起得来
|
|
61
95
|
spawnDaemon();
|
|
62
96
|
const deadline = Date.now() + HEALTH_POLL_TIMEOUT_MS;
|
|
63
97
|
while (Date.now() < deadline) {
|
|
64
98
|
await sleep(HEALTH_POLL_INTERVAL_MS);
|
|
65
|
-
|
|
99
|
+
const p = await probeDaemon();
|
|
100
|
+
if (p.alive && p.version === SERVICE_VERSION) return true;
|
|
66
101
|
}
|
|
67
102
|
return false;
|
|
68
103
|
}
|