shine-code-submit 1.0.4 → 1.0.6
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 +24 -45
- package/src/daemon/server.ts +33 -1
- package/src/daemon/settings.ts +6 -0
- package/src/daemon/ui-assets.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/src/shared/updater.ts +73 -0
- package/ui/.build/app.js +49 -49
- package/ui/components/SettingsModule.tsx +70 -6
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
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// 自动更新:查 npm registry 最新版本,有新版 spawn detached `npx shine-code-submit@latest install` 升级。
|
|
2
|
+
// daemon 启动时 + 定时调 autoUpdateIfNeeded。全程静默(try/catch),绝不影响 daemon。
|
|
3
|
+
//
|
|
4
|
+
// 升级链路:npx 拉 latest 包 → install CLI 部署新版到 cache + 注册 → startDaemonWithBun(1.0.5)
|
|
5
|
+
// 检测旧 daemon 版本不匹配 → stopDaemon 停旧 → 启新 daemon。daemon 退出不影响 npx install(detached)。
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { SERVICE_NAME, SERVICE_VERSION } from "./config";
|
|
8
|
+
import { readSettings, writeSettings } from "../daemon/settings";
|
|
9
|
+
|
|
10
|
+
const REGISTRY_LATEST = `https://registry.npmjs.org/${SERVICE_NAME}/latest`;
|
|
11
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
12
|
+
|
|
13
|
+
/** semver 大于:a > b 才 true(相等 false)。只升级不降级,避免本地比 npm 新(如发版前 build)时误降级。 */
|
|
14
|
+
function versionGt(a: string, b: string): boolean {
|
|
15
|
+
const pa = a.split(".").map((x) => parseInt(x, 10) || 0);
|
|
16
|
+
const pb = b.split(".").map((x) => parseInt(x, 10) || 0);
|
|
17
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
18
|
+
const x = pa[i] ?? 0;
|
|
19
|
+
const y = pb[i] ?? 0;
|
|
20
|
+
if (x > y) return true;
|
|
21
|
+
if (x < y) return false;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UpdateCheck {
|
|
27
|
+
latest?: string;
|
|
28
|
+
current: string;
|
|
29
|
+
hasUpdate: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 查 npm registry 最新版本。失败返回 hasUpdate:false(不抛)。 */
|
|
33
|
+
export async function checkForUpdate(): Promise<UpdateCheck> {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(REGISTRY_LATEST, { signal: AbortSignal.timeout(5000) });
|
|
36
|
+
if (!res.ok) return { current: SERVICE_VERSION, hasUpdate: false };
|
|
37
|
+
const data = (await res.json()) as { version?: string };
|
|
38
|
+
const latest = data.version;
|
|
39
|
+
if (!latest) return { current: SERVICE_VERSION, hasUpdate: false };
|
|
40
|
+
return { latest, current: SERVICE_VERSION, hasUpdate: versionGt(latest, SERVICE_VERSION) };
|
|
41
|
+
} catch {
|
|
42
|
+
return { current: SERVICE_VERSION, hasUpdate: false };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 自动更新:读 settings(autoUpdate 开关),有新版 spawn detached npx install。
|
|
48
|
+
* 返回 {updated, latest}。全程 try/catch 静默——绝不影响 daemon。
|
|
49
|
+
* - autoUpdate===false → 跳过。
|
|
50
|
+
* - 有新版 → spawn `npx --yes --registry=官方 shine-code-submit@latest install`(detached,不阻塞)。
|
|
51
|
+
* - 无论是否升级,都缓存 latestVersion 到 settings(dashboard 显示用)。
|
|
52
|
+
*/
|
|
53
|
+
export async function autoUpdateIfNeeded(force = false): Promise<{ updated: boolean; latest?: string }> {
|
|
54
|
+
try {
|
|
55
|
+
const s = readSettings();
|
|
56
|
+
if (!force && s.autoUpdate === false) return { updated: false };
|
|
57
|
+
const check = await checkForUpdate();
|
|
58
|
+
// 缓存 latestVersion(dashboard 显示当前/最新版本)
|
|
59
|
+
if (check.latest && check.latest !== s.latestVersion) {
|
|
60
|
+
writeSettings({ ...s, latestVersion: check.latest });
|
|
61
|
+
}
|
|
62
|
+
if (!check.hasUpdate) return { updated: false, latest: check.latest };
|
|
63
|
+
// 有新版:spawn detached npx install(官方 registry 确保 latest,npmmirror 有同步延迟)
|
|
64
|
+
spawn(
|
|
65
|
+
"npx",
|
|
66
|
+
["--yes", `--registry=${NPM_REGISTRY}`, `${SERVICE_NAME}@latest`, "install"],
|
|
67
|
+
{ detached: true, stdio: "ignore", windowsHide: true, shell: true },
|
|
68
|
+
).unref();
|
|
69
|
+
return { updated: true, latest: check.latest };
|
|
70
|
+
} catch {
|
|
71
|
+
return { updated: false };
|
|
72
|
+
}
|
|
73
|
+
}
|