feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
This commit is contained in:
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
# Generate icon.icns + tray-icon-Template.png{,@2x.png} from the SVG sources.
|
||||
#
|
||||
# Uses macOS-built-in tools only — no Homebrew or npm dependencies:
|
||||
# * qlmanage : SVG → PNG via Quick Look (always present on macOS)
|
||||
# * sips : PNG resize/format
|
||||
# * iconutil : .iconset directory → .icns
|
||||
#
|
||||
# This script is idempotent. Run from desktop/ or from anywhere.
|
||||
# Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ASSETS="$(cd "$HERE/../assets" && pwd)"
|
||||
|
||||
require() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
echo "error: required tool '$1' not found. This script only runs on macOS." >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
require qlmanage
|
||||
require sips
|
||||
require iconutil
|
||||
|
||||
cd "$ASSETS"
|
||||
|
||||
echo ">>> rendering icon.svg → icon.png (1024)"
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
qlmanage -t -s 1024 -o "$TMP" icon.svg >/dev/null 2>&1
|
||||
mv "$TMP/icon.svg.png" icon.png
|
||||
|
||||
echo ">>> building icon.iconset"
|
||||
rm -rf icon.iconset
|
||||
mkdir -p icon.iconset
|
||||
for s in 16 32 64 128 256 512 1024; do
|
||||
sips -z "$s" "$s" icon.png --out "icon.iconset/icon_${s}x${s}.png" >/dev/null
|
||||
done
|
||||
# Apple's @2x naming convention.
|
||||
cp icon.iconset/icon_32x32.png icon.iconset/icon_16x16@2x.png
|
||||
cp icon.iconset/icon_64x64.png icon.iconset/icon_32x32@2x.png
|
||||
cp icon.iconset/icon_256x256.png icon.iconset/icon_128x128@2x.png
|
||||
cp icon.iconset/icon_512x512.png icon.iconset/icon_256x256@2x.png
|
||||
cp icon.iconset/icon_1024x1024.png icon.iconset/icon_512x512@2x.png
|
||||
# Drop the 64-only file; iconutil dislikes unknown sizes.
|
||||
rm -f icon.iconset/icon_64x64.png
|
||||
|
||||
echo ">>> compiling icon.icns"
|
||||
iconutil -c icns icon.iconset -o icon.icns
|
||||
rm -rf icon.iconset
|
||||
|
||||
echo ">>> rendering tray-icon-Template.png{,@2x.png} via Python"
|
||||
# qlmanage flattens SVG against an opaque white background — the tray PNG
|
||||
# ends up with alpha=255 everywhere and macOS template tinting turns the
|
||||
# whole 22x22 bounding box white in the menu bar. Generate the RGBA PNG
|
||||
# pixel-by-pixel instead. Geometry mirrors tray-icon.svg (22-unit viewBox).
|
||||
require python3
|
||||
python3 - <<'PY'
|
||||
import struct, zlib
|
||||
|
||||
def make_png(width, height, pixels):
|
||||
def chunk(tag, data):
|
||||
return struct.pack('>I', len(data)) + tag + data + struct.pack('>I', zlib.crc32(tag + data))
|
||||
sig = b'\x89PNG\r\n\x1a\n'
|
||||
ihdr = struct.pack('>IIBBBBB', width, height, 8, 6, 0, 0, 0) # 8-bit RGBA
|
||||
raw = bytearray()
|
||||
for y in range(height):
|
||||
raw.append(0)
|
||||
raw.extend(pixels[y*width*4:(y+1)*width*4])
|
||||
return sig + chunk(b'IHDR', ihdr) + chunk(b'IDAT', zlib.compress(bytes(raw), 9)) + chunk(b'IEND', b'')
|
||||
|
||||
def draw(w, h, s):
|
||||
px = bytearray(w * h * 4) # alpha=0 -> transparent
|
||||
def rect(x, y, rw, rh):
|
||||
for j in range(y, min(y+rh, h)):
|
||||
for i in range(x, min(x+rw, w)):
|
||||
o = (j*w + i) * 4
|
||||
px[o:o+4] = b'\x00\x00\x00\xff' # opaque black
|
||||
rect(2*s, 14*s, 4*s, 7*s)
|
||||
rect(9*s, 11*s, 4*s, 10*s)
|
||||
rect(16*s, 5*s, 4*s, 16*s)
|
||||
return px
|
||||
|
||||
with open('tray-icon-Template.png', 'wb') as f: f.write(make_png(22, 22, draw(22, 22, 1)))
|
||||
with open('tray-icon-Template@2x.png', 'wb') as f: f.write(make_png(44, 44, draw(44, 44, 2)))
|
||||
PY
|
||||
|
||||
echo ">>> done."
|
||||
ls -la icon.icns tray-icon-Template.png tray-icon-Template@2x.png
|
||||
@@ -0,0 +1,121 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generate assets/icon.ico from assets/icon.png — the Windows counterpart to
|
||||
scripts/build-icons.sh (which produces icon.icns + the macOS tray PNGs).
|
||||
|
||||
.DESCRIPTION
|
||||
Uses only the .NET Framework's System.Drawing (always present on Windows) —
|
||||
no ImageMagick, no npm dependency. icon.png is the 1024x1024 raster already
|
||||
rendered from assets/icon.svg by the macOS icon pipeline; this script
|
||||
downscales it to the standard Windows icon sizes and packs them into a
|
||||
classic, maximally-compatible BMP-based .ico (32bpp BGRA + AND mask). That
|
||||
format is what electron-builder embeds in the .exe and what NSIS uses for
|
||||
the installer icon, and it renders correctly on Windows 7 through 11.
|
||||
|
||||
Idempotent. Run from anywhere:
|
||||
powershell -ExecutionPolicy Bypass -File desktop/scripts/build-win-icon.ps1
|
||||
|
||||
.NOTES
|
||||
Author: Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
#>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$assets = Join-Path (Split-Path -Parent $here) 'assets'
|
||||
$srcPng = Join-Path $assets 'icon.png'
|
||||
$outIco = Join-Path $assets 'icon.ico'
|
||||
|
||||
if (-not (Test-Path $srcPng)) {
|
||||
throw "icon.png not found at $srcPng. Generate it first (scripts/build-icons.sh renders it from icon.svg)."
|
||||
}
|
||||
|
||||
# Standard Windows icon ladder. 256 is required by electron-builder; the small
|
||||
# sizes keep the taskbar / Alt-Tab / tray crisp.
|
||||
$sizes = 16, 24, 32, 48, 64, 128, 256
|
||||
|
||||
$src = [System.Drawing.Image]::FromFile($srcPng)
|
||||
$entries = New-Object System.Collections.ArrayList
|
||||
|
||||
try {
|
||||
foreach ($s in $sizes) {
|
||||
$bmp = New-Object System.Drawing.Bitmap($s, $s, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
||||
$g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
||||
$g.Clear([System.Drawing.Color]::Transparent)
|
||||
$g.DrawImage($src, 0, 0, $s, $s)
|
||||
$g.Dispose()
|
||||
|
||||
# Pull raw pixels: Format32bppArgb is stored little-endian as B,G,R,A —
|
||||
# exactly the byte order a 32bpp DIB wants. Rows are top-down here.
|
||||
$rect = New-Object System.Drawing.Rectangle(0, 0, $s, $s)
|
||||
$data = $bmp.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||
$stride = $data.Stride
|
||||
$buf = New-Object byte[] ($stride * $s)
|
||||
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $buf, 0, $buf.Length)
|
||||
$bmp.UnlockBits($data)
|
||||
$bmp.Dispose()
|
||||
|
||||
# Build the DIB: BITMAPINFOHEADER(40) + XOR bitmap (bottom-up BGRA) +
|
||||
# 1bpp AND mask (bottom-up, all zeros — alpha channel does the masking).
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$bw = New-Object System.IO.BinaryWriter($ms)
|
||||
$bw.Write([int]40) # biSize
|
||||
$bw.Write([int]$s) # biWidth
|
||||
$bw.Write([int]($s * 2)) # biHeight = XOR height + AND height
|
||||
$bw.Write([int16]1) # biPlanes
|
||||
$bw.Write([int16]32) # biBitCount
|
||||
$bw.Write([int]0) # biCompression = BI_RGB
|
||||
$bw.Write([int]0) # biSizeImage
|
||||
$bw.Write([int]0) # biXPelsPerMeter
|
||||
$bw.Write([int]0) # biYPelsPerMeter
|
||||
$bw.Write([int]0) # biClrUsed
|
||||
$bw.Write([int]0) # biClrImportant
|
||||
|
||||
# XOR pixels, bottom-up.
|
||||
for ($y = $s - 1; $y -ge 0; $y--) {
|
||||
$bw.Write($buf, $y * $stride, 4 * $s)
|
||||
}
|
||||
# AND mask: 1 bit/pixel, each row padded to a 4-byte boundary, all zero.
|
||||
$maskRow = [int]([math]::Floor((($s + 31) / 32)) * 4)
|
||||
$zeros = New-Object byte[] ($maskRow)
|
||||
for ($y = 0; $y -lt $s; $y++) { $bw.Write($zeros, 0, $maskRow) }
|
||||
|
||||
$bw.Flush()
|
||||
[void]$entries.Add([pscustomobject]@{ Size = $s; Data = $ms.ToArray() })
|
||||
$bw.Dispose(); $ms.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$src.Dispose()
|
||||
}
|
||||
|
||||
# Assemble the .ico: ICONDIR header, then one ICONDIRENTRY per image, then data.
|
||||
$out = New-Object System.IO.MemoryStream
|
||||
$w = New-Object System.IO.BinaryWriter($out)
|
||||
$w.Write([int16]0) # reserved
|
||||
$w.Write([int16]1) # type = icon
|
||||
$w.Write([int16]$entries.Count) # image count
|
||||
|
||||
$offset = 6 + 16 * $entries.Count
|
||||
foreach ($e in $entries) {
|
||||
$dim = if ($e.Size -ge 256) { 0 } else { $e.Size } # 0 means 256 in the dir
|
||||
$w.Write([byte]$dim) # width
|
||||
$w.Write([byte]$dim) # height
|
||||
$w.Write([byte]0) # palette color count
|
||||
$w.Write([byte]0) # reserved
|
||||
$w.Write([int16]1) # color planes
|
||||
$w.Write([int16]32) # bits per pixel
|
||||
$w.Write([int]$e.Data.Length) # size of image data
|
||||
$w.Write([int]$offset) # offset of image data
|
||||
$offset += $e.Data.Length
|
||||
}
|
||||
foreach ($e in $entries) { $w.Write($e.Data, 0, $e.Data.Length) }
|
||||
$w.Flush()
|
||||
[System.IO.File]::WriteAllBytes($outIco, $out.ToArray())
|
||||
$w.Dispose(); $out.Dispose()
|
||||
|
||||
Write-Output ("Wrote {0} ({1:N0} bytes, sizes: {2})" -f $outIco, (Get-Item $outIco).Length, ($sizes -join ', '))
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file Desktop dependency installer with actionable failure help.
|
||||
*
|
||||
* Thin wrapper around `npm install` (which still runs the `postinstall`
|
||||
* `electron-builder install-app-deps` to rebuild native modules for Electron).
|
||||
* On success it behaves exactly like a bare `npm install`. On failure — almost
|
||||
* always the `better-sqlite3` native build — it prints the prerequisite
|
||||
* guidance + the no-toolchain alternative commands, then exits non-zero so the
|
||||
* normal command still fails loudly rather than silently leaving a half-set-up
|
||||
* `node_modules`.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const path = require("node:path");
|
||||
const { printNativeDepHelp, hasBetterSqliteBinary } = require("./preflight");
|
||||
|
||||
const desktopRoot = path.resolve(__dirname, "..");
|
||||
|
||||
// On Windows `npm` is a `.cmd` shim that `spawnSync` can only launch via a
|
||||
// shell; without this it fails with ENOENT. POSIX is unaffected.
|
||||
const result = spawnSync("npm", ["install"], {
|
||||
cwd: desktopRoot,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
// `npm install` failed outright (e.g. node-gyp could not find a compiler), or
|
||||
// it "succeeded" but the native binary never landed (a prebuilt download was
|
||||
// skipped). Either way the desktop app cannot boot — surface the fix and fail.
|
||||
if (result.status !== 0) {
|
||||
printNativeDepHelp("`npm install` failed while building the native better-sqlite3 module.");
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
|
||||
if (!hasBetterSqliteBinary()) {
|
||||
printNativeDepHelp("Dependencies installed, but the better-sqlite3 native binary is missing.");
|
||||
process.exit(1);
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file electron-builder afterSign hook for Apple notarization.
|
||||
*
|
||||
* This is opt-in: it only does anything when all three Apple credentials
|
||||
* are present as environment variables. In every other case (local builds,
|
||||
* fork CI without secrets) the hook is a no-op. That keeps the default
|
||||
* `npm run dmg` working for contributors without an Apple Developer
|
||||
* account while letting the project maintainer flip a switch later.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
exports.default = async function notarizeIfConfigured(context) {
|
||||
const { electronPlatformName, appOutDir, packager } = context;
|
||||
if (electronPlatformName !== "darwin") return;
|
||||
|
||||
const { APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD } = process.env;
|
||||
if (!APPLE_ID || !APPLE_TEAM_ID || !APPLE_APP_SPECIFIC_PASSWORD) {
|
||||
console.log("[notarize] Apple credentials not set — skipping notarization (ad-hoc only).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Lazy-require: @electron/notarize is only needed when we actually notarize,
|
||||
// so contributors without Apple credentials don't have to install it.
|
||||
let notarize;
|
||||
try {
|
||||
({ notarize } = require("@electron/notarize"));
|
||||
} catch {
|
||||
console.log(
|
||||
"[notarize] Apple credentials present but @electron/notarize is not installed. Run `npm install --save-dev @electron/notarize` in desktop/."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const appName = packager.appInfo.productFilename;
|
||||
const appPath = `${appOutDir}/${appName}.app`;
|
||||
console.log(`[notarize] notarizing ${appPath}`);
|
||||
|
||||
await notarize({
|
||||
tool: "notarytool",
|
||||
appBundleId: packager.appInfo.id,
|
||||
appPath,
|
||||
appleId: APPLE_ID,
|
||||
appleIdPassword: APPLE_APP_SPECIFIC_PASSWORD,
|
||||
teamId: APPLE_TEAM_ID,
|
||||
});
|
||||
|
||||
console.log("[notarize] done");
|
||||
};
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file Pre-build guard.
|
||||
*
|
||||
* Ensures the desktop bundle has everything it needs before TypeScript
|
||||
* compiles. Specifically:
|
||||
* 1. The root repo's node_modules exists (Express + friends).
|
||||
* 2. The client has been built (client/dist exists). In production mode the
|
||||
* Express server serves the SPA from client/dist; if it's missing the
|
||||
* DMG would ship a 404-only dashboard.
|
||||
* 3. Asset PNGs exist (or we leave a clear warning — icons can be
|
||||
* regenerated via scripts/build-icons.sh).
|
||||
* 4. The desktop-local better-sqlite3 native binary matches this machine's
|
||||
* CPU architecture. A prior `electron-builder --mac --x64/--arm64` build
|
||||
* rebuilds it for the target arch; left mismatched it breaks `desktop:dev`
|
||||
* and `desktop:test` with ERR_DLOPEN_FAILED. We rebuild it if so.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { hasBetterSqliteBinary, printNativeDepHelp } = require("./preflight");
|
||||
|
||||
const desktopRoot = path.resolve(__dirname, "..");
|
||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||
const clientDist = path.join(repoRoot, "client", "dist");
|
||||
const rootNodeModules = path.join(repoRoot, "node_modules");
|
||||
const assets = path.join(desktopRoot, "assets");
|
||||
|
||||
function run(cmd, args, opts = {}) {
|
||||
// On Windows `npm`/`npx` are `.cmd` shims that `spawnSync` can only launch
|
||||
// through a shell; without this it fails with ENOENT. POSIX is unaffected.
|
||||
const result = spawnSync(cmd, args, {
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
...opts,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${cmd} ${args.join(" ")} failed with exit ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(rootNodeModules)) {
|
||||
console.log("[prebuild] installing root dependencies…");
|
||||
run("npm", ["ci"], { cwd: repoRoot });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(clientDist) || !fs.existsSync(path.join(clientDist, "index.html"))) {
|
||||
console.log("[prebuild] building client (client/dist missing)…");
|
||||
run("npm", ["ci"], { cwd: path.join(repoRoot, "client") });
|
||||
run("npm", ["run", "build"], { cwd: repoRoot });
|
||||
}
|
||||
|
||||
const trayIcon = path.join(assets, "tray-icon-Template.png");
|
||||
if (!fs.existsSync(trayIcon)) {
|
||||
console.warn(
|
||||
"[prebuild] WARN: tray-icon-Template.png missing. Run `npm run build:icons` to regenerate from assets/icon.svg."
|
||||
);
|
||||
}
|
||||
|
||||
// Heal a better-sqlite3 native binary left built for the wrong CPU arch by a
|
||||
// prior `electron-builder --mac --x64/--arm64` run. Without this, `desktop:dev`
|
||||
// and `desktop:test` fail to load the module (ERR_DLOPEN_FAILED) until the
|
||||
// contributor manually re-runs `npm run desktop:install`.
|
||||
if (process.platform === "darwin") {
|
||||
const bsNode = path.join(
|
||||
desktopRoot,
|
||||
"node_modules",
|
||||
"better-sqlite3",
|
||||
"build",
|
||||
"Release",
|
||||
"better_sqlite3.node"
|
||||
);
|
||||
if (fs.existsSync(bsNode)) {
|
||||
const desc = spawnSync("file", ["-b", bsNode], { encoding: "utf8" }).stdout || "";
|
||||
// A universal binary works on both arches; only act on a clear mismatch.
|
||||
const universal = /universal/i.test(desc);
|
||||
const wrongArch =
|
||||
!universal &&
|
||||
((process.arch === "arm64" && !/arm64/.test(desc)) ||
|
||||
(process.arch === "x64" && !/x86_64/.test(desc)));
|
||||
if (wrongArch) {
|
||||
console.log(
|
||||
"[prebuild] better-sqlite3 is built for the wrong CPU arch (a prior DMG build left it that way) — rebuilding for this machine…"
|
||||
);
|
||||
run("npx", ["electron-builder", "install-app-deps"], { cwd: desktopRoot });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The embedded server `require`s better-sqlite3 at boot; without its native
|
||||
// binary the desktop app dies with a fatal dialog after compiling cleanly.
|
||||
// Catch it here (a build-time, copy-pasteable failure) rather than at runtime.
|
||||
if (!hasBetterSqliteBinary()) {
|
||||
printNativeDepHelp("The desktop-local better-sqlite3 native binary is missing.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("[prebuild] ok");
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @file Shared native-dependency preflight checks + actionable failure help.
|
||||
*
|
||||
* The desktop shell embeds the dashboard server in-process, which `require`s
|
||||
* the native `better-sqlite3` module rebuilt against Electron's Node ABI. That
|
||||
* build is the single most common setup failure: it needs either a C++ toolchain
|
||||
* (to compile from source) or a Node version new enough to have a prebuilt
|
||||
* binary. When it's missing we want a clear, copy-pasteable message instead of a
|
||||
* raw node-gyp stack trace or a runtime "Cannot find module" deep inside boot.
|
||||
*
|
||||
* This module is shared by `install.js` (wraps the dependency install) and
|
||||
* `prebuild.js` (gates every `desktop:*` build/dev script) so both surfaces
|
||||
* print the same guidance.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const desktopRoot = path.resolve(__dirname, "..");
|
||||
|
||||
/** Absolute path to the compiled/prebuilt better-sqlite3 native binary. */
|
||||
function betterSqliteBinary() {
|
||||
return path.join(
|
||||
desktopRoot,
|
||||
"node_modules",
|
||||
"better-sqlite3",
|
||||
"build",
|
||||
"Release",
|
||||
"better_sqlite3.node"
|
||||
);
|
||||
}
|
||||
|
||||
/** True when the Electron-ABI better-sqlite3 binary is present on disk. */
|
||||
function hasBetterSqliteBinary() {
|
||||
return fs.existsSync(betterSqliteBinary());
|
||||
}
|
||||
|
||||
/**
|
||||
* Print prerequisite guidance and the no-toolchain alternative commands to
|
||||
* stderr. Callers should `process.exit(1)` after this so the failing npm
|
||||
* command exits non-zero (never leave the user thinking setup succeeded).
|
||||
*/
|
||||
function printNativeDepHelp(reason) {
|
||||
const line = "─".repeat(74);
|
||||
const out = (s) => process.stderr.write(s + "\n");
|
||||
out("");
|
||||
out(line);
|
||||
out(" Claude Code Monitor — desktop native dependency setup did not complete");
|
||||
out(line);
|
||||
if (reason) {
|
||||
out(` ${reason}`);
|
||||
out("");
|
||||
}
|
||||
out(" The desktop app embeds the dashboard server, which needs the native");
|
||||
out(" 'better-sqlite3' module built for Electron's Node ABI. This typically");
|
||||
out(" fails for one of two reasons:");
|
||||
out("");
|
||||
out(" 1. No C++ build toolchain, so the module can't compile from source:");
|
||||
out(' • Windows: install "Visual Studio Build Tools" with the');
|
||||
out(' "Desktop development with C++" workload.');
|
||||
out(" • macOS: xcode-select --install");
|
||||
out(" • Linux: install build-essential + python3.");
|
||||
out("");
|
||||
out(" 2. Your Node.js is newer than any published better-sqlite3 prebuilt");
|
||||
out(` binary (you are on Node ${process.version}). A Node LTS (20 or 22)`);
|
||||
out(" ships prebuilt binaries and avoids the compile entirely.");
|
||||
out("");
|
||||
out(" Or skip the source build and fetch Electron's prebuilt binary directly");
|
||||
out(" (no C++ toolchain needed):");
|
||||
out("");
|
||||
out(" cd desktop");
|
||||
out(" npm install --ignore-scripts");
|
||||
out(" node node_modules/electron/install.js");
|
||||
out(" npx electron-builder install-app-deps");
|
||||
out("");
|
||||
out(line);
|
||||
out("");
|
||||
}
|
||||
|
||||
module.exports = { betterSqliteBinary, hasBetterSqliteBinary, printNativeDepHelp };
|
||||
Reference in New Issue
Block a user