shine-code-submit 0.2.0

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.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/LICENSE +21 -0
  4. package/README.md +217 -0
  5. package/bin/launcher.cjs +35 -0
  6. package/bun.lock +45 -0
  7. package/dist/install.cjs +8 -0
  8. package/hooks/hooks.json +81 -0
  9. package/package.json +68 -0
  10. package/src/cli/main.ts +128 -0
  11. package/src/daemon/auth.ts +7 -0
  12. package/src/daemon/bus.ts +25 -0
  13. package/src/daemon/git.ts +118 -0
  14. package/src/daemon/logger.ts +66 -0
  15. package/src/daemon/main.ts +95 -0
  16. package/src/daemon/server.ts +208 -0
  17. package/src/daemon/spool-consumer.ts +52 -0
  18. package/src/daemon/stats.ts +33 -0
  19. package/src/daemon/store.ts +147 -0
  20. package/src/daemon/token-cache.ts +35 -0
  21. package/src/daemon/transcript.ts +130 -0
  22. package/src/daemon/ui-assets.ts +4 -0
  23. package/src/daemon/ui.ts +34 -0
  24. package/src/daemon/ws.ts +54 -0
  25. package/src/hook/main.ts +154 -0
  26. package/src/install/bun.ts +97 -0
  27. package/src/install/deploy.ts +78 -0
  28. package/src/install/json-safe.ts +43 -0
  29. package/src/install/main.ts +167 -0
  30. package/src/install/paths.ts +28 -0
  31. package/src/install/register.ts +108 -0
  32. package/src/shared/config.ts +77 -0
  33. package/src/shared/daemonctl.ts +110 -0
  34. package/src/shared/id.ts +19 -0
  35. package/src/shared/paths.ts +23 -0
  36. package/src/shared/pidfile.ts +32 -0
  37. package/src/shared/spool.ts +60 -0
  38. package/src/shared/types.ts +116 -0
  39. package/ui/.build/app.js +76 -0
  40. package/ui/app.tsx +31 -0
  41. package/ui/components/App.tsx +55 -0
  42. package/ui/components/CommitsModule.tsx +25 -0
  43. package/ui/components/CommitsView.tsx +84 -0
  44. package/ui/components/Conversation.tsx +101 -0
  45. package/ui/components/DiffBlock.tsx +64 -0
  46. package/ui/components/EventDetail.tsx +8 -0
  47. package/ui/components/EventItem.tsx +24 -0
  48. package/ui/components/EventList.tsx +22 -0
  49. package/ui/components/EventsModule.tsx +123 -0
  50. package/ui/components/Header.tsx +15 -0
  51. package/ui/components/Icon.tsx +114 -0
  52. package/ui/components/Markdown.tsx +13 -0
  53. package/ui/components/Message.tsx +60 -0
  54. package/ui/components/OverviewModule.tsx +86 -0
  55. package/ui/components/SessionDetail.tsx +51 -0
  56. package/ui/components/SessionTree.tsx +94 -0
  57. package/ui/components/SessionsModule.tsx +39 -0
  58. package/ui/components/SessionsPanel.tsx +15 -0
  59. package/ui/components/SideNav.tsx +46 -0
  60. package/ui/components/Splitter.tsx +23 -0
  61. package/ui/components/StatsModule.tsx +133 -0
  62. package/ui/components/Status.tsx +66 -0
  63. package/ui/components/SummaryView.tsx +83 -0
  64. package/ui/components/SystemModule.tsx +38 -0
  65. package/ui/components/ToolCard.tsx +57 -0
  66. package/ui/hooks/useAllCommits.ts +59 -0
  67. package/ui/hooks/useApi.ts +13 -0
  68. package/ui/hooks/useCommits.ts +45 -0
  69. package/ui/hooks/useConversation.ts +43 -0
  70. package/ui/hooks/useEvents.ts +45 -0
  71. package/ui/hooks/useSplitter.ts +43 -0
  72. package/ui/hooks/useStatsPolling.ts +35 -0
  73. package/ui/hooks/useWebSocket.ts +38 -0
  74. package/ui/index.html +13 -0
  75. package/ui/lib/aggregate.ts +81 -0
  76. package/ui/lib/diff.ts +45 -0
  77. package/ui/lib/export.ts +45 -0
  78. package/ui/lib/format.ts +100 -0
  79. package/ui/lib/util.ts +106 -0
  80. package/ui/state/AppContext.tsx +65 -0
  81. package/ui/style.css +849 -0
  82. package/ui/types.ts +46 -0
@@ -0,0 +1,34 @@
1
+ // 查看页静态资源:编译期嵌入二进制。
2
+ // ui-assets.ts 由 scripts/build.ts 从 ui/* 生成为字符串常量,bun build --compile 时随 daemon 嵌入。
3
+ // (不走 Bun import attribute:tsc 在 bundler 模式会把 .html/.css 当 bun-types 的 HTMLBundle/CSSBundle、
4
+ // 把 app.js 当真实模块解析,类型与解析都报错;生成字符串模块最稳。)
5
+ import { INDEX_HTML, APP_JS, STYLE_CSS } from "./ui-assets";
6
+
7
+ const CONTENT_TYPES: Record<string, string> = {
8
+ ".html": "text/html; charset=utf-8",
9
+ ".js": "application/javascript; charset=utf-8",
10
+ ".css": "text/css; charset=utf-8",
11
+ ".svg": "image/svg+xml",
12
+ };
13
+
14
+ // 路径 → 嵌入内容(内存取,天然无路径穿越风险)
15
+ const ASSETS: Record<string, { body: string; ext: string }> = {
16
+ "/": { body: INDEX_HTML, ext: ".html" },
17
+ "/ui": { body: INDEX_HTML, ext: ".html" },
18
+ "/ui/": { body: INDEX_HTML, ext: ".html" },
19
+ "/ui/index.html": { body: INDEX_HTML, ext: ".html" },
20
+ "/ui/app.js": { body: APP_JS, ext: ".js" },
21
+ "/ui/style.css": { body: STYLE_CSS, ext: ".css" },
22
+ };
23
+
24
+ export function serveUi(_req: Request, url: URL): Response {
25
+ const asset = ASSETS[url.pathname];
26
+ if (!asset) return new Response("not found", { status: 404 });
27
+ return new Response(asset.body, {
28
+ headers: {
29
+ "content-type": CONTENT_TYPES[asset.ext] ?? "application/octet-stream",
30
+ // 开发期 UI 常更新,URL 无 hash,禁止缓存避免改了不生效
31
+ "cache-control": "no-store, must-revalidate",
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,54 @@
1
+ // WS 连接池:订阅 EventBus 广播新事件给查看页。鉴权在 server 升级前完成。
2
+ import type { ServerWebSocket } from "bun";
3
+ import type { EventBus } from "./bus";
4
+ import type { Stats } from "./stats";
5
+ import type { HookEvent } from "../shared/types";
6
+ import { SERVICE_NAME, SERVICE_VERSION } from "../shared/config";
7
+
8
+ export class WebSocketPool {
9
+ private sockets = new Set<ServerWebSocket<unknown>>();
10
+ private off?: () => void;
11
+
12
+ constructor(private bus: EventBus, private stats: Stats) {}
13
+
14
+ attach(): void {
15
+ this.off = this.bus.on((event) => this.broadcast(event));
16
+ }
17
+
18
+ add(ws: ServerWebSocket<unknown>): void {
19
+ this.sockets.add(ws);
20
+ // 连接即推送一次当前状态快照
21
+ ws.send(
22
+ JSON.stringify({
23
+ kind: "snapshot",
24
+ stats: {
25
+ service: SERVICE_NAME,
26
+ version: SERVICE_VERSION,
27
+ spoolBacklog: this.stats.backlog(),
28
+ eventsPerSec: this.stats.rate(),
29
+ totalEvents: this.stats.total,
30
+ },
31
+ }),
32
+ );
33
+ }
34
+
35
+ remove(ws: ServerWebSocket<unknown>): void {
36
+ this.sockets.delete(ws);
37
+ }
38
+
39
+ private broadcast(event: HookEvent): void {
40
+ const msg = JSON.stringify({ kind: "event", event });
41
+ for (const ws of this.sockets) {
42
+ try {
43
+ ws.send(msg);
44
+ } catch {
45
+ /* 连接已断,忽略 */
46
+ }
47
+ }
48
+ }
49
+
50
+ dispose(): void {
51
+ this.off?.();
52
+ this.sockets.clear();
53
+ }
54
+ }
@@ -0,0 +1,154 @@
1
+ // Hook 入口(短命进程):
2
+ // 1. 采集 env + stdin,补 cwd/sessionId/pid/eventId/timestamp/type
3
+ // 2. 原子落盘 spool(tmp+rename)—— 唯一必成功环节
4
+ // 3. 热转发 POST;连接失败才走故障路径(ensureDaemon:探测→认自己人→拉起→轮询 ready)→ 重读 token → 重试
5
+ // 全程失败静默,退出码恒为 0(绝不影响 Claude Code)。
6
+ import { ensureDirs } from "../shared/paths";
7
+ import { readToken } from "../shared/pidfile";
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";
11
+ import type { HookEvent, HookEventType } from "../shared/types";
12
+
13
+ const VALID_TYPES: HookEventType[] = [
14
+ "SessionStart",
15
+ "UserPromptSubmit",
16
+ "PreToolUse",
17
+ "PostToolUse",
18
+ "Stop",
19
+ "SubagentStop",
20
+ "PreCompact",
21
+ "SessionEnd",
22
+ ];
23
+
24
+ main().catch(() => process.exit(0));
25
+
26
+ async function main(): Promise<void> {
27
+ const event = await collect();
28
+ if (!event) {
29
+ process.stderr.write("[shine-code-submit-hook] collect failed: missing cwd/sessionId\n");
30
+ return process.exit(0);
31
+ }
32
+
33
+ // 1. 落盘(必成功环节)。失败时写 stderr 告警(可被发现),仍退出码 0。
34
+ try {
35
+ ensureDirs();
36
+ writeSpoolFile(event);
37
+ } catch (err) {
38
+ process.stderr.write(
39
+ `[shine-code-submit-hook] spool write failed: ${safeMsg(err)}; event=${truncate(JSON.stringify(event))}\n`,
40
+ );
41
+ return process.exit(0);
42
+ }
43
+
44
+ // 2. 热转发 + 故障路径(永不抛出)
45
+ try {
46
+ await forward(event);
47
+ } catch (err) {
48
+ process.stderr.write(`[shine-code-submit-hook] forward failed: ${safeMsg(err)}\n`);
49
+ }
50
+
51
+ // 3. SessionStart(真·新开会话)时给用户打印 UI 入口:stdout 输出 JSON,
52
+ // Claude Code 解析 systemMessage 字段直接显示给用户(裸 stdout 只注入
53
+ // assistant 当 context,用户不可见)。仅 source=startup 打印,避免
54
+ // resume/clear/compact 刷屏;daemon 未就绪读不到 token 则静默跳过。
55
+ if (event.type === "SessionStart") {
56
+ const source = (event.payload as Record<string, unknown> | null | undefined)?.source;
57
+ if (source === "startup") {
58
+ const token = readToken();
59
+ if (token) {
60
+ const url = `${PUBLIC_BASE_URL}/ui?t=${token}`; // 网卡 IP:显示与打开浏览器用同一地址,局域网通用
61
+ process.stdout.write(JSON.stringify({ systemMessage: `Shine Dashboard: ${url}` }));
62
+ openBrowser(url);
63
+ }
64
+ }
65
+ }
66
+ process.exit(0);
67
+ }
68
+
69
+ /** 采集:argv[1] 或 stdin.hook_event_name 作为 type;cwd=process.cwd();sessionId 取 stdin.session_id。 */
70
+ async function collect(): Promise<HookEvent | null> {
71
+ // 扫描 argv 找有效事件名:兼容「直接调 exe (argv[1])」与「bun run script.ts X (argv[2])」两种形式
72
+ const typeArg = process.argv.slice(1).find((a) => VALID_TYPES.includes(a as HookEventType)) as
73
+ | HookEventType
74
+ | undefined;
75
+ const payload = await readStdin();
76
+ const obj = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
77
+
78
+ const type: HookEventType =
79
+ typeArg && VALID_TYPES.includes(typeArg)
80
+ ? typeArg
81
+ : typeof obj.hook_event_name === "string" && VALID_TYPES.includes(obj.hook_event_name as HookEventType)
82
+ ? (obj.hook_event_name as HookEventType)
83
+ : "PostToolUse";
84
+
85
+ const sessionId =
86
+ (typeof obj.session_id === "string" && obj.session_id) ||
87
+ process.env.CLAUDE_SESSION_ID ||
88
+ "unknown";
89
+ const cwd = process.cwd();
90
+ if (!cwd) return null;
91
+
92
+ return {
93
+ eventId: crypto.randomUUID(),
94
+ type,
95
+ timestamp: Date.now(),
96
+ cwd,
97
+ sessionId,
98
+ pid: process.pid,
99
+ payload: obj,
100
+ };
101
+ }
102
+
103
+ /** 读 stdin(带超时兜底,防止无管道时阻塞)。解析失败则保留原始文本。 */
104
+ async function readStdin(): Promise<unknown> {
105
+ if (process.stdin.isTTY) return null;
106
+ try {
107
+ const text = await Promise.race([
108
+ Bun.stdin.text(),
109
+ new Promise<string>((r) => setTimeout(() => r(""), 800)),
110
+ ]);
111
+ if (!text.trim()) return null;
112
+ try {
113
+ return JSON.parse(text);
114
+ } catch {
115
+ return { _raw: text };
116
+ }
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ async function forward(event: HookEvent): Promise<void> {
123
+ const token = readToken();
124
+ const url = `${BASE_URL}/api/hook/${event.type}`;
125
+ if (await postOnce(url, event, token)) return;
126
+ // 热转发失败 → 故障路径
127
+ await ensureDaemon();
128
+ // 重读 token(拉起后 pid 文件已更新)
129
+ await postOnce(url, event, readToken() ?? token);
130
+ }
131
+
132
+ async function postOnce(url: string, event: HookEvent, token: string | null): Promise<boolean> {
133
+ try {
134
+ await fetch(url, {
135
+ method: "POST",
136
+ headers: {
137
+ "content-type": "application/json",
138
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
139
+ },
140
+ body: JSON.stringify(event),
141
+ signal: AbortSignal.timeout(HOOK_POST_TIMEOUT_MS),
142
+ });
143
+ return true; // 到达 daemon 即视为成功(事件也已落盘,回捞兜底)
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ function safeMsg(v: unknown): string {
150
+ return v instanceof Error ? `${v.name}: ${v.message}` : String(v);
151
+ }
152
+ function truncate(s: string): string {
153
+ return s.length > 500 ? `${s.slice(0, 500)}...` : s;
154
+ }
@@ -0,0 +1,97 @@
1
+ // bun 检测 + 自动安装。install CLI 由 node 跑,跑 install 时 bun 可能还没装。
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+
7
+ const MIN_BUN = [1, 1, 0];
8
+ const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
9
+
10
+ /** 检测 bun:先 PATH(which),再常见安装位置(不依赖 PATH 刚刷新)。 */
11
+ export function getBunPath(): string | null {
12
+ const r = spawnSync("bun", ["--version"], { shell: process.platform === "win32", encoding: "utf8" });
13
+ if (r.status === 0 && (r.stdout ?? "").trim()) return "bun";
14
+ const home = homedir();
15
+ const candidates =
16
+ process.platform === "win32"
17
+ ? [join(home, ".bun", "bin", "bun.exe")]
18
+ : [join(home, ".bun", "bin", "bun"), "/usr/local/bin/bun", "/opt/homebrew/bin/bun"];
19
+ for (const c of candidates) {
20
+ if (existsSync(c)) return c;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function parseVersion(v: string): number[] {
26
+ return v.trim().split(".").map((x) => parseInt(x, 10) || 0);
27
+ }
28
+
29
+ function versionGte(v: string, min: number[]): boolean {
30
+ const parts = parseVersion(v);
31
+ for (let i = 0; i < min.length; i++) {
32
+ const p = parts[i] ?? 0;
33
+ const m = min[i] ?? 0;
34
+ if (p > m) return true;
35
+ if (p < m) return false;
36
+ }
37
+ return true;
38
+ }
39
+
40
+ /** 当前 npm registry 是否国内镜像(npmmirror/taobao)——是则优先 npm i -g bun 走镜像。 */
41
+ function isCnRegistry(): boolean {
42
+ const reg = process.env.npm_config_registry ?? "";
43
+ return /npmmirror|taobao/i.test(reg);
44
+ }
45
+
46
+ function runShell(cmd: string): number {
47
+ return spawnSync(cmd, { shell: true, encoding: "utf8", timeout: INSTALL_TIMEOUT_MS, stdio: "inherit" }).status ?? 1;
48
+ }
49
+
50
+ /**
51
+ * 确保 bun 可用:已装且版本够返回路径;否则自动装。
52
+ * 返回 bun 可执行路径("bun" 或绝对路径)。
53
+ */
54
+ export async function ensureBun(): Promise<string> {
55
+ const existing = getBunPath();
56
+ if (existing) {
57
+ const v =
58
+ spawnSync(existing, ["--version"], { shell: process.platform === "win32", encoding: "utf8" }).stdout?.trim() ?? "";
59
+ if (v && versionGte(v, MIN_BUN)) {
60
+ console.log(`[shine-code-submit] bun ${v} detected`);
61
+ return existing;
62
+ }
63
+ }
64
+
65
+ console.log("[shine-code-submit] bun 未找到或版本过低,开始自动安装...");
66
+
67
+ // 国内镜像优先:npm i -g bun(走 npmmirror,比官方脚本快且稳)
68
+ if (isCnRegistry()) {
69
+ console.log("[shine-code-submit] 检测到国内 npm 镜像,先尝试 npm install -g bun");
70
+ if (runShell("npm install -g bun") === 0) {
71
+ const p = getBunPath();
72
+ if (p) {
73
+ console.log("[shine-code-submit] ✓ bun 安装成功(via npm 镜像)");
74
+ return p;
75
+ }
76
+ }
77
+ console.log("[shine-code-submit] npm 镜像方式失败,回退官方脚本");
78
+ }
79
+
80
+ // 官方脚本
81
+ if (process.platform === "win32") {
82
+ runShell('powershell -c "irm bun.sh/install.ps1 | iex"');
83
+ } else {
84
+ runShell("curl -fsSL https://bun.sh/install | bash");
85
+ }
86
+
87
+ const p = getBunPath();
88
+ if (!p) {
89
+ console.error("[shine-code-submit] bun 自动安装失败。请手动安装后重试:");
90
+ console.error(" Windows: winget install Oven-sh.Bun 或 npm install -g bun");
91
+ console.error(" macOS: brew install oven-sh/bun/bun");
92
+ console.error(" Linux: curl -fsSL https://bun.sh/install | bash");
93
+ throw new Error("bun installation failed");
94
+ }
95
+ console.log("[shine-code-submit] ✓ bun 安装成功");
96
+ return p;
97
+ }
@@ -0,0 +1,78 @@
1
+ // 部署 plugin 文件到 claude cache 目录,并跑 bun install 装运行时依赖(marked/react)。
2
+ import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawnSync } from "node:child_process";
6
+ import { pluginsRoot } from "./paths";
7
+ import { SERVICE_VERSION } from "../shared/config";
8
+
9
+ export const MARKETPLACE_NAME = "shine-code-submit";
10
+ export const PLUGIN_NAME = "shine-code-submit";
11
+
12
+ /** 部署目标版本目录:~/.claude/plugins/cache/shine-code-submit/shine-code-submit/<version>/ */
13
+ export function cacheDir(version: string = SERVICE_VERSION): string {
14
+ return join(pluginsRoot(), "cache", MARKETPLACE_NAME, PLUGIN_NAME, version);
15
+ }
16
+
17
+ /** 找 npm 包根:从本文件(dist/install.cjs)上溯到含 package.json + .claude-plugin 的目录。 */
18
+ function findPackageRoot(): string {
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ let dir = here;
21
+ for (let i = 0; i < 10; i++) {
22
+ if (existsSync(join(dir, "package.json")) && existsSync(join(dir, ".claude-plugin"))) return dir;
23
+ const parent = dirname(dir);
24
+ if (parent === dir) break;
25
+ dir = parent;
26
+ }
27
+ return here;
28
+ }
29
+
30
+ /** 要部署的文件/目录白名单(plugin 运行必需;不含 dist/install.cjs——install CLI 本身不进 plugin)。 */
31
+ const WHITELIST = [".claude-plugin", "hooks", "bin", "src", "ui", "package.json", "bun.lock", "README.md"];
32
+
33
+ /**
34
+ * 部署 plugin:清同版本目录 → 拷白名单 → bun install 装依赖 → 写版本标记。
35
+ * 返回 cache 目录绝对路径。
36
+ */
37
+ export function deployPlugin(bunPath: string): string {
38
+ const target = cacheDir();
39
+ if (existsSync(target)) rmSync(target, { recursive: true, force: true });
40
+ mkdirSync(target, { recursive: true });
41
+
42
+ const srcRoot = findPackageRoot();
43
+ console.log(`[shine-code-submit] 部署源:${srcRoot}`);
44
+ for (const item of WHITELIST) {
45
+ const from = join(srcRoot, item);
46
+ if (!existsSync(from)) continue; // 缺(如 bun.lock 未入库)跳过
47
+ cpSync(from, join(target, item), { recursive: true });
48
+ }
49
+
50
+ // bun install 装运行时依赖
51
+ console.log("[shine-code-submit] 安装运行时依赖(bun install)...");
52
+ let status = spawnSync(bunPath, ["install", "--frozen-lockfile"], {
53
+ cwd: target,
54
+ shell: process.platform === "win32",
55
+ encoding: "utf8",
56
+ stdio: "inherit",
57
+ }).status;
58
+ if (status !== 0) {
59
+ console.log("[shine-code-submit] --frozen-lockfile 失败,重试普通 bun install");
60
+ status = spawnSync(bunPath, ["install"], {
61
+ cwd: target,
62
+ shell: process.platform === "win32",
63
+ encoding: "utf8",
64
+ stdio: "inherit",
65
+ }).status;
66
+ if (status !== 0) {
67
+ throw new Error(`bun install 失败(exit ${status})。请手动在 ${target} 跑 bun install`);
68
+ }
69
+ }
70
+
71
+ writeFileSync(
72
+ join(target, ".install-version"),
73
+ JSON.stringify({ version: SERVICE_VERSION, installedAt: Date.now() }),
74
+ "utf8",
75
+ );
76
+ console.log(`[shine-code-submit] 已部署到 ${target}`);
77
+ return target;
78
+ }
@@ -0,0 +1,43 @@
1
+ // JSON 安全读写:读损坏时备份后用默认值(绝不覆盖);写时 tmp+rename 原子,首次写备份。
2
+ // 改用户 ~/.claude 下的 JSON 全走这套,防止 install 脚本 bug 损坏 claude 配置。
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
+ import { dirname } from "node:path";
5
+
6
+ /** 读 JSON;文件不存在/读失败返回默认;JSON 损坏则备份后返回默认(绝不覆盖原文件)。 */
7
+ export function readJsonDefault<T>(file: string, def: T): T {
8
+ if (!existsSync(file)) return def;
9
+ let raw: string;
10
+ try {
11
+ raw = readFileSync(file, "utf8");
12
+ } catch {
13
+ return def;
14
+ }
15
+ try {
16
+ return JSON.parse(raw) as T;
17
+ } catch {
18
+ const bak = `${file}.bak-corrupt-${Date.now()}`;
19
+ try {
20
+ copyFileSync(file, bak);
21
+ console.error(`[shine-code-submit] WARNING: ${file} JSON 损坏,已备份到 ${bak},用默认值继续`);
22
+ } catch {
23
+ /* 备份失败也继续 */
24
+ }
25
+ return def;
26
+ }
27
+ }
28
+
29
+ /** 原子写 JSON:首次写时把原文件备份到 .bak-pre-install,写 .tmp 再 rename(同卷 rename 原子)。 */
30
+ export function writeJsonAtomicWithBackup(file: string, data: unknown): void {
31
+ const bak = `${file}.bak-pre-install`;
32
+ if (existsSync(file) && !existsSync(bak)) {
33
+ try {
34
+ copyFileSync(file, bak);
35
+ } catch {
36
+ /* 备份失败不阻塞写 */
37
+ }
38
+ }
39
+ mkdirSync(dirname(file), { recursive: true });
40
+ const tmp = `${file}.tmp-${process.pid}`;
41
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
42
+ renameSync(tmp, file);
43
+ }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // install CLI 入口:install / uninstall / status。由 node 跑(编译成 dist/install.cjs)。
3
+ // npx shine-code-submit install → 自动装 bun + 部署 plugin + 注册 + 启 daemon + 开 dashboard。
4
+ import { existsSync, rmSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { ensureBun } from "./bun";
8
+ import { cacheDir, deployPlugin } from "./deploy";
9
+ import { enablePlugin, registerMarketplace, registerPlugin, unregisterAll } from "./register";
10
+ import { BASE_URL, PUBLIC_BASE_URL, SERVICE_VERSION } from "../shared/config";
11
+ import { isOursAlive, openBrowser } from "../shared/daemonctl";
12
+ import { ensureDirs } from "../shared/paths";
13
+ import { readPidFile } from "../shared/pidfile";
14
+
15
+ const [, , cmd] = process.argv;
16
+
17
+ main().catch((err) => {
18
+ console.error(`[shine-code-submit] ${err instanceof Error ? err.message : String(err)}`);
19
+ process.exit(1);
20
+ });
21
+
22
+ async function main(): Promise<void> {
23
+ switch (cmd) {
24
+ case undefined:
25
+ case "install":
26
+ await runInstall();
27
+ break;
28
+ case "uninstall":
29
+ await runUninstall();
30
+ break;
31
+ case "status":
32
+ await runStatus();
33
+ break;
34
+ case "--version":
35
+ case "-v":
36
+ console.log(SERVICE_VERSION);
37
+ break;
38
+ default:
39
+ printHelp();
40
+ }
41
+ }
42
+
43
+ async function runInstall(): Promise<void> {
44
+ console.log(`=== shine-code-submit installer v${SERVICE_VERSION} ===`);
45
+ const bunPath = await ensureBun();
46
+ const cachePath = deployPlugin(bunPath);
47
+ registerMarketplace(cachePath);
48
+ registerPlugin(cachePath);
49
+ enablePlugin(cachePath);
50
+ ensureDirs();
51
+ await startDaemonWithBun(bunPath, cachePath);
52
+ openDashboard();
53
+ console.log("");
54
+ console.log("✓ 安装完成。");
55
+ console.log(" · 重启 Claude Code 后,/plugin 列表会显示 shine-code-submit(已启用)。");
56
+ console.log(" · 开新会话即触发 SessionStart hook,事件出现在 dashboard。");
57
+ }
58
+
59
+ async function runUninstall(): Promise<void> {
60
+ console.log("=== shine-code-submit uninstaller ===");
61
+ await stopDaemon();
62
+ unregisterAll();
63
+ const target = cacheDir();
64
+ if (existsSync(target)) {
65
+ rmSync(target, { recursive: true, force: true });
66
+ console.log(`[shine-code-submit] 已删除 ${target}`);
67
+ }
68
+ console.log("✓ 已卸载。重启 Claude Code 后 /plugin 不再显示。");
69
+ }
70
+
71
+ async function runStatus(): Promise<void> {
72
+ const alive = await isOursAlive();
73
+ const pid = readPidFile();
74
+ if (alive && pid) {
75
+ console.log(`daemon: running pid=${pid.pid} ${PUBLIC_BASE_URL}`);
76
+ } else {
77
+ console.log("daemon: not running");
78
+ }
79
+ }
80
+
81
+ /** 用显式 bunPath 拉 daemon。不调 daemonctl.spawnDaemon——它用 process.execPath,install 场景是 node 会出错。 */
82
+ async function startDaemonWithBun(bunPath: string, cachePath: string): Promise<void> {
83
+ if (await isOursAlive()) {
84
+ console.log("[shine-code-submit] daemon 已在运行,跳过启动");
85
+ return;
86
+ }
87
+ const daemonSrc = join(cachePath, "src", "daemon", "main.ts");
88
+ console.log("[shine-code-submit] 启动 daemon...");
89
+ try {
90
+ const child = spawn(bunPath, ["run", daemonSrc], {
91
+ detached: true,
92
+ stdio: "ignore",
93
+ windowsHide: true,
94
+ cwd: cachePath,
95
+ shell: process.platform === "win32",
96
+ });
97
+ child.unref();
98
+ } catch (err) {
99
+ console.error(`[shine-code-submit] 启动 daemon 失败:${err instanceof Error ? err.message : err}`);
100
+ console.error(" plugin 已注册,Claude Code 重启后 hook 会自动拉起 daemon");
101
+ return;
102
+ }
103
+ const deadline = Date.now() + 10_000;
104
+ while (Date.now() < deadline) {
105
+ await sleep(200);
106
+ if (await isOursAlive()) {
107
+ console.log("[shine-code-submit] daemon 已就绪");
108
+ return;
109
+ }
110
+ }
111
+ console.error(
112
+ "[shine-code-submit] daemon 启动超时(10s)。plugin 已注册,可稍后手动 `shine-code-submit start` 或重启 claude。",
113
+ );
114
+ }
115
+
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
+ function openDashboard(): void {
144
+ const pid = readPidFile();
145
+ const url = pid ? `${PUBLIC_BASE_URL}/ui?t=${pid.token}` : `${PUBLIC_BASE_URL}/ui`;
146
+ console.log(`[shine-code-submit] Dashboard: ${url}`);
147
+ try {
148
+ openBrowser(url);
149
+ } catch {
150
+ /* 打开失败不阻塞 */
151
+ }
152
+ }
153
+
154
+ function sleep(ms: number): Promise<void> {
155
+ return new Promise((r) => setTimeout(r, ms));
156
+ }
157
+
158
+ function printHelp(): void {
159
+ console.log(`shine-code-submit <command>
160
+
161
+ install 安装插件(自动装 bun + 部署 + 注册 + 启 daemon + 开 dashboard)
162
+ uninstall 卸载(停 daemon + 反注册 + 删文件)
163
+ status 显示 daemon 状态
164
+
165
+ 通常通过 npx 跑:npx shine-code-submit install`);
166
+ process.exit(cmd ? 1 : 0);
167
+ }
@@ -0,0 +1,28 @@
1
+ // claude 配置根解析:CLAUDE_CONFIG_DIR 优先,回退 ~/.claude。与 claude-mem 一致。
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ /** claude 配置根目录(~/.claude 或 $CLAUDE_CONFIG_DIR)。 */
6
+ export function claudeRoot(): string {
7
+ return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
8
+ }
9
+
10
+ /** plugin 根目录(~/.claude/plugins)。 */
11
+ export function pluginsRoot(): string {
12
+ return join(claudeRoot(), "plugins");
13
+ }
14
+
15
+ /** 已知 marketplace 注册表路径。 */
16
+ export function knownMarketplacesPath(): string {
17
+ return join(pluginsRoot(), "known_marketplaces.json");
18
+ }
19
+
20
+ /** 已安装 plugin 注册表路径。 */
21
+ export function installedPluginsPath(): string {
22
+ return join(pluginsRoot(), "installed_plugins.json");
23
+ }
24
+
25
+ /** 用户 settings.json 路径(~/.claude/settings.json)。 */
26
+ export function settingsPath(): string {
27
+ return join(claudeRoot(), "settings.json");
28
+ }