File size: 4,660 Bytes
06a5304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
 * Proxy self-update — checks for new commits on GitHub and applies via git pull.
 * Only works in CLI mode (where .git exists). Docker/Electron show manual instructions.
 */

import { execFile, execFileSync } from "child_process";
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { promisify } from "util";
import { isEmbedded } from "./paths.js";

const execFileAsync = promisify(execFile);

export interface ProxyInfo {
  version: string;
  commit: string | null;
}

export interface ProxySelfUpdateResult {
  commitsBehind: number;
  currentCommit: string | null;
  latestCommit: string | null;
}

let _proxyUpdateInProgress = false;
let _gitAvailable: boolean | null = null;

/** Read proxy version from package.json + current git commit hash. */
export function getProxyInfo(): ProxyInfo {
  let version = "unknown";
  try {
    const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
    version = pkg.version ?? "unknown";
  } catch { /* ignore */ }

  let commit: string | null = null;
  if (canSelfUpdate()) {
    try {
      const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
        cwd: process.cwd(),
        encoding: "utf-8",
        timeout: 5000,
      });
      commit = out.trim() || null;
    } catch { /* ignore */ }
  }

  return { version, commit };
}

/** Whether this environment supports git-based self-update. */
export function canSelfUpdate(): boolean {
  if (isEmbedded()) return false;
  if (_gitAvailable !== null) return _gitAvailable;

  // Check .git directory exists
  if (!existsSync(resolve(process.cwd(), ".git"))) {
    _gitAvailable = false;
    return false;
  }

  // Check git command is available
  try {
    execFileSync("git", ["--version"], {
      cwd: process.cwd(),
      timeout: 5000,
      stdio: "ignore",
    });
    _gitAvailable = true;
  } catch {
    _gitAvailable = false;
  }

  return _gitAvailable;
}

/** Whether a proxy self-update is currently in progress. */
export function isProxyUpdateInProgress(): boolean {
  return _proxyUpdateInProgress;
}

/** Fetch latest from origin and check how many commits behind. */
export async function checkProxySelfUpdate(): Promise<ProxySelfUpdateResult> {
  const cwd = process.cwd();

  // Get current commit
  let currentCommit: string | null = null;
  try {
    const { stdout } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd, timeout: 5000 });
    currentCommit = stdout.trim() || null;
  } catch { /* ignore */ }

  // Fetch latest
  try {
    await execFileAsync("git", ["fetch", "origin", "master", "--quiet"], { cwd, timeout: 30000 });
  } catch (err) {
    console.warn("[SelfUpdate] git fetch failed:", err instanceof Error ? err.message : err);
    return { commitsBehind: 0, currentCommit, latestCommit: currentCommit };
  }

  // Count commits behind
  let commitsBehind = 0;
  let latestCommit: string | null = null;
  try {
    const { stdout: countOut } = await execFileAsync(
      "git", ["rev-list", "HEAD..origin/master", "--count"], { cwd, timeout: 5000 },
    );
    commitsBehind = parseInt(countOut.trim(), 10) || 0;

    const { stdout: latestOut } = await execFileAsync(
      "git", ["rev-parse", "--short", "origin/master"], { cwd, timeout: 5000 },
    );
    latestCommit = latestOut.trim() || null;
  } catch { /* ignore */ }

  return { commitsBehind, currentCommit, latestCommit };
}

/**
 * Apply proxy self-update: git pull + npm install + npm run build.
 * Runs in background. Returns immediately; check isProxyUpdateInProgress() for status.
 */
export async function applyProxySelfUpdate(): Promise<{ started: boolean; error?: string }> {
  if (_proxyUpdateInProgress) {
    return { started: false, error: "Update already in progress" };
  }

  _proxyUpdateInProgress = true;
  const cwd = process.cwd();

  try {
    console.log("[SelfUpdate] Pulling latest code...");
    await execFileAsync("git", ["pull", "origin", "master"], { cwd, timeout: 60000 });

    console.log("[SelfUpdate] Installing dependencies...");
    await execFileAsync("npm", ["install"], { cwd, timeout: 120000, shell: true });

    console.log("[SelfUpdate] Building...");
    await execFileAsync("npm", ["run", "build"], { cwd, timeout: 120000, shell: true });

    console.log("[SelfUpdate] Update complete. Server restart required.");
    _proxyUpdateInProgress = false;
    return { started: true };
  } catch (err) {
    _proxyUpdateInProgress = false;
    const msg = err instanceof Error ? err.message : String(err);
    console.error("[SelfUpdate] Update failed:", msg);
    return { started: false, error: msg };
  }
}