@mingxy/opencode-mascot 0.1.2 → 0.2.1

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.
@@ -0,0 +1,109 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { createRequire } from "node:module";
4
+ import { join, dirname } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { openSync, closeSync, unlinkSync, statSync, writeSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const require = createRequire(import.meta.url);
10
+
11
+ const PKG_NAME = "@mingxy/opencode-mascot";
12
+ const LOCK_FILE = join(tmpdir(), "mascot-update.lock");
13
+ const STALE_LOCK_MS = 5 * 60 * 1000;
14
+ let lockFd: number | null = null;
15
+
16
+ async function getLatestVersion(): Promise<string | null> {
17
+ try {
18
+ const { stdout } = await execFileAsync("npm", ["view", PKG_NAME, "version"], { timeout: 10000 });
19
+ return stdout.trim();
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function compareVersions(a: string, b: string): number {
26
+ const pa = a.replace(/^v/, "").split(".").map(Number);
27
+ const pb = b.replace(/^v/, "").split(".").map(Number);
28
+ for (let i = 0; i < 3; i++) {
29
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return 1;
30
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return -1;
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ function getInstallDir(): string {
36
+ try {
37
+ const pkgPath = require.resolve(`${PKG_NAME}/package.json`);
38
+ return dirname(pkgPath);
39
+ } catch {
40
+ return join(homedir(), ".cache", "opencode", "packages", "@mingxy", "opencode-mascot");
41
+ }
42
+ }
43
+
44
+ async function installUpdate(targetDir: string): Promise<boolean> {
45
+ const tmpDir = mkdtempSync(join(tmpdir(), "mascot-update-"));
46
+ try {
47
+ await execFileAsync("npm", ["pack", `${PKG_NAME}@latest`, "--pack-destination", tmpDir], { timeout: 60000 });
48
+ const files = readdirSync(tmpDir);
49
+ const tgz = files.find(f => f.endsWith(".tgz"));
50
+ if (!tgz) return false;
51
+ await execFileAsync(
52
+ "tar",
53
+ ["-xzf", join(tmpDir, tgz), "-C", targetDir, "--strip-components=1", "--no-same-owner", "--no-same-permissions"],
54
+ { timeout: 30000 },
55
+ );
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ } finally {
60
+ try { rmSync(tmpDir, { recursive: true }); } catch {}
61
+ }
62
+ }
63
+
64
+ function acquireLock(): boolean {
65
+ try {
66
+ try {
67
+ const s = statSync(LOCK_FILE);
68
+ if (Date.now() - s.mtimeMs > STALE_LOCK_MS) unlinkSync(LOCK_FILE);
69
+ } catch {}
70
+ lockFd = openSync(LOCK_FILE, "wx");
71
+ writeSync(lockFd, String(process.pid));
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function releaseLock(): void {
79
+ if (lockFd !== null) {
80
+ try { closeSync(lockFd); } catch {}
81
+ lockFd = null;
82
+ try { unlinkSync(LOCK_FILE); } catch {}
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 更新成功后的回调:把新版本号交给调用方,由其触发吉祥物庆祝动画
88
+ */
89
+ export async function checkAndUpdate(
90
+ currentVersion: string,
91
+ onSuccess: (newVersion: string) => void,
92
+ ): Promise<void> {
93
+ const latest = await getLatestVersion();
94
+ if (!latest) return;
95
+
96
+ if (compareVersions(latest, currentVersion) <= 0) return;
97
+
98
+ if (!acquireLock()) return;
99
+
100
+ try {
101
+ const targetDir = getInstallDir();
102
+ const success = await installUpdate(targetDir);
103
+ if (success) {
104
+ onSuccess(latest);
105
+ }
106
+ } finally {
107
+ releaseLock();
108
+ }
109
+ }
package/tui.tsx CHANGED
@@ -1,27 +1,42 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
- import { loadMascot } from "./src/core/mascot-loader"
3
+ import { readFileSync } from "node:fs"
4
+ import { join, dirname } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+ import { loadAllMascots } from "./src/core/mascot-loader"
4
7
  import { SidebarMascot } from "./src/components/sidebar-mascot"
5
- import { createAnimatedRenderer } from "./src/core/ascii-renderer"
8
+ import { HomeMascot } from "./src/components/home-mascot"
9
+ import { checkAndUpdate } from "./src/core/updater"
10
+ import { emitCelebrate } from "./src/core/celebration-bus"
6
11
 
7
- const tui: TuiPlugin = async (api, options) => {
8
- const mascot = await loadMascot(options)
9
- const homeRenderer = createAnimatedRenderer(mascot)
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ let pluginVersion = "unknown";
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
18
+ if (pkg?.version && typeof pkg.version === "string") {
19
+ pluginVersion = pkg.version;
20
+ }
21
+ } catch {}
22
+
23
+ const tui: TuiPlugin = async (api, _options) => {
24
+ const mascots = loadAllMascots()
10
25
 
11
26
  api.slots.register({
12
27
  slots: {
13
28
  sidebar_content() {
14
- return <SidebarMascot mascot={mascot} api={api} />
29
+ return <SidebarMascot mascots={mascots} api={api} />
15
30
  },
16
31
  home_bottom() {
17
- return (
18
- <box flexDirection="column" alignItems="center">
19
- {homeRenderer.element()}
20
- </box>
21
- )
32
+ return <HomeMascot mascots={mascots} api={api} />
22
33
  }
23
34
  }
24
35
  })
36
+
37
+ checkAndUpdate(pluginVersion, (newVersion) => {
38
+ emitCelebrate(newVersion);
39
+ }).catch(() => {});
25
40
  }
26
41
 
27
42
  const plugin: TuiPluginModule = {