fix: clear stale Next.js cache on version upgrade (#1022)
* fix: clear stale Next.js cache on version upgrade (#986) After upgrading @composio/ao via npm, `ao start` served the old UI because Next.js runtime cache (.next/cache) persisted from the previous version. Adds a hybrid fix: - Postinstall hook clears .next/cache and writes a version stamp - Runtime guard in `ao start` and `ao dashboard` compares stamp against package version; on mismatch, clears .next/cache and restamps - Build-time script writes stamp after `next build` (monorepo path) Only .next/cache is deleted — shipped build artifacts (.next/server, .next/static, BUILD_ID) are never touched, keeping npm installs intact. Closes #986 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(lint): add Node.js globals for package-level scripts The ESLint config only covered root-level scripts/, not packages/*/scripts/. This caused `no-undef` errors for `console` and `process` in packages/web/scripts/stamp-version.js. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: ensure postinstall cache clearing runs on all platforms Restructure postinstall.js so the node-pty chmod fix is wrapped in a conditional block instead of using early process.exit(0). The previous exits on Windows, missing node-pty, or missing spawn-helper prevented the cache-clearing code from ever running on those systems. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review comments on stamp-version ordering and cache catch logging - Move stamp-version.js after tsc in web build script so a tsc failure does not leave a fresh stamp paired with a stale server bundle. - Log skipped cache version checks via console.debug instead of swallowing silently, to aid debugging without blocking dashboard startup. Addresses review feedback from @illegalcall on PR #1022. * fix: resolve ao-web in postinstall cache cleanup --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64f67f3425
commit
2e4583b7bd
|
|
@ -101,7 +101,7 @@ export default tseslint.config(
|
|||
|
||||
// Scripts directory - Node.js environment
|
||||
{
|
||||
files: ["scripts/**/*.js", "scripts/**/*.mjs"],
|
||||
files: ["scripts/**/*.js", "scripts/**/*.mjs", "packages/*/scripts/**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
console: "readonly",
|
||||
|
|
|
|||
|
|
@ -10,16 +10,17 @@
|
|||
* 2. Verifies the prebuilt binary is compatible with the current Node.js version.
|
||||
* If not (common with nvm/fnm/volta), rebuilds from source via npx node-gyp.
|
||||
* See: https://github.com/ComposioHQ/agent-orchestrator/issues/987
|
||||
*
|
||||
* 3. Clears stale Next.js runtime cache (.next/cache) from @composio/ao-web
|
||||
* after a version upgrade, so `ao start` serves fresh dashboard assets.
|
||||
* Writes a version stamp (.next/AO_VERSION) to skip cleanup on subsequent runs.
|
||||
*/
|
||||
|
||||
import { chmodSync, existsSync } from "node:fs";
|
||||
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// No-op on Windows — different PTY mechanism
|
||||
if (process.platform === "win32") process.exit(0);
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function findPackageUp(startDir, ...segments) {
|
||||
|
|
@ -34,50 +35,92 @@ function findPackageUp(startDir, ...segments) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const nodePtyDir = findPackageUp(__dirname, "node-pty");
|
||||
if (!nodePtyDir) process.exit(0);
|
||||
function resolveNodeModulesPackage(fromDir, ...segments) {
|
||||
const packageDir = resolve(fromDir, "node_modules", ...segments);
|
||||
return existsSync(resolve(packageDir, "package.json")) ? packageDir : null;
|
||||
}
|
||||
|
||||
// Step 1: Fix spawn-helper permissions
|
||||
const spawnHelper = resolve(
|
||||
nodePtyDir,
|
||||
"prebuilds",
|
||||
`${process.platform}-${process.arch}`,
|
||||
"spawn-helper",
|
||||
);
|
||||
function findWebDir() {
|
||||
const directWebDir = findPackageUp(__dirname, "@aoagents", "ao-web");
|
||||
if (directWebDir) return directWebDir;
|
||||
|
||||
if (existsSync(spawnHelper)) {
|
||||
try {
|
||||
chmodSync(spawnHelper, 0o755);
|
||||
console.log("\u2713 node-pty spawn-helper permissions set");
|
||||
} catch {
|
||||
console.warn("\u26a0\ufe0f Could not set spawn-helper permissions (non-critical)");
|
||||
const cliDir = findPackageUp(__dirname, "@aoagents", "ao-cli");
|
||||
if (!cliDir) return null;
|
||||
|
||||
return resolveNodeModulesPackage(cliDir, "@aoagents", "ao-web");
|
||||
}
|
||||
|
||||
// --- 1 & 2. Fix node-pty spawn-helper permissions and verify ABI (non-Windows only) ---
|
||||
if (process.platform !== "win32") {
|
||||
const nodePtyDir = findPackageUp(__dirname, "node-pty");
|
||||
if (nodePtyDir) {
|
||||
const spawnHelper = resolve(
|
||||
nodePtyDir,
|
||||
"prebuilds",
|
||||
`${process.platform}-${process.arch}`,
|
||||
"spawn-helper",
|
||||
);
|
||||
|
||||
if (existsSync(spawnHelper)) {
|
||||
try {
|
||||
chmodSync(spawnHelper, 0o755);
|
||||
console.log("✓ node-pty spawn-helper permissions set");
|
||||
} catch {
|
||||
console.warn("⚠️ Could not set spawn-helper permissions (non-critical)");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the prebuilt binary actually works with this Node.js version.
|
||||
// If it doesn't (ABI mismatch from nvm/fnm/volta version switching), rebuild.
|
||||
// We exercise pty.spawn() — not just require() — because the posix_spawnp
|
||||
// failure only surfaces when the helper binary is actually executed.
|
||||
try {
|
||||
execSync(
|
||||
"node -e \"var p=require('node-pty');var t=p.spawn('/bin/sh',['-c','exit 0'],{});t.kill();process.exit(0);\"",
|
||||
{
|
||||
cwd: resolve(nodePtyDir, ".."),
|
||||
stdio: "ignore",
|
||||
timeout: 10000,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
console.log("⚠️ node-pty prebuilt binary incompatible with Node.js " + process.version + ", rebuilding...");
|
||||
try {
|
||||
execSync("npx --yes node-gyp rebuild", {
|
||||
cwd: nodePtyDir,
|
||||
stdio: "inherit",
|
||||
timeout: 120000,
|
||||
});
|
||||
console.log("✓ node-pty rebuilt successfully");
|
||||
} catch {
|
||||
console.warn("⚠️ node-pty rebuild failed — web terminal may not work");
|
||||
console.warn(" Manual fix: cd " + nodePtyDir + " && npx node-gyp rebuild");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Verify the prebuilt binary actually works with this Node.js version.
|
||||
// If it doesn't (ABI mismatch from nvm/fnm/volta version switching), rebuild.
|
||||
// We exercise pty.spawn() — not just require() — because the posix_spawnp
|
||||
// failure only surfaces when the helper binary is actually executed.
|
||||
// --- 3. Clear stale Next.js runtime cache after version upgrade ---
|
||||
try {
|
||||
execSync(
|
||||
"node -e \"var p=require('node-pty');var t=p.spawn('/bin/sh',['-c','exit 0'],{});t.kill();process.exit(0);\"",
|
||||
{
|
||||
cwd: resolve(nodePtyDir, ".."),
|
||||
stdio: "ignore",
|
||||
timeout: 10000,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
console.log("\u26a0\ufe0f node-pty prebuilt binary incompatible with Node.js " + process.version + ", rebuilding...");
|
||||
try {
|
||||
execSync("npx --yes node-gyp rebuild", {
|
||||
cwd: nodePtyDir,
|
||||
stdio: "inherit",
|
||||
timeout: 120000,
|
||||
});
|
||||
console.log("\u2713 node-pty rebuilt successfully");
|
||||
} catch {
|
||||
console.warn("\u26a0\ufe0f node-pty rebuild failed — web terminal may not work");
|
||||
console.warn(" Manual fix: cd " + nodePtyDir + " && npx node-gyp rebuild");
|
||||
const webDir = findWebDir();
|
||||
if (webDir) {
|
||||
const pkgPath = resolve(webDir, "package.json");
|
||||
if (existsSync(pkgPath)) {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
||||
const version = pkg.version;
|
||||
const cacheDir = resolve(webDir, ".next", "cache");
|
||||
const stampPath = resolve(webDir, ".next", "AO_VERSION");
|
||||
|
||||
if (existsSync(cacheDir)) {
|
||||
rmSync(cacheDir, { recursive: true, force: true });
|
||||
console.log("✓ Cleared stale .next/cache");
|
||||
}
|
||||
if (existsSync(resolve(webDir, ".next"))) {
|
||||
writeFileSync(stampPath, version, "utf8");
|
||||
console.log(`✓ Dashboard version stamp set to ${version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Could not clear dashboard cache (non-critical): ${err.message}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,64 @@ describe("rebuildDashboardProductionArtifacts", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("clearStaleCacheIfNeeded", () => {
|
||||
it("clears .next/cache and writes stamp when version differs", async () => {
|
||||
const webDir = join(tmpDir, "web");
|
||||
mkdirSync(join(webDir, ".next", "cache", "webpack"), { recursive: true });
|
||||
writeFileSync(join(webDir, ".next", "AO_VERSION"), "0.1.0");
|
||||
writeFileSync(join(webDir, "package.json"), JSON.stringify({ version: "0.2.0" }));
|
||||
|
||||
const { clearStaleCacheIfNeeded } = await import("../../src/lib/dashboard-rebuild.js");
|
||||
await clearStaleCacheIfNeeded(webDir);
|
||||
|
||||
expect(existsSync(join(webDir, ".next", "cache"))).toBe(false);
|
||||
expect(existsSync(join(webDir, ".next", "AO_VERSION"))).toBe(true);
|
||||
const { readFileSync } = await import("node:fs");
|
||||
expect(readFileSync(join(webDir, ".next", "AO_VERSION"), "utf8")).toBe("0.2.0");
|
||||
});
|
||||
|
||||
it("clears cache when stamp is missing (upgrade from old version)", async () => {
|
||||
const webDir = join(tmpDir, "web");
|
||||
mkdirSync(join(webDir, ".next", "cache"), { recursive: true });
|
||||
writeFileSync(join(webDir, "package.json"), JSON.stringify({ version: "0.2.0" }));
|
||||
|
||||
const { clearStaleCacheIfNeeded } = await import("../../src/lib/dashboard-rebuild.js");
|
||||
await clearStaleCacheIfNeeded(webDir);
|
||||
|
||||
expect(existsSync(join(webDir, ".next", "cache"))).toBe(false);
|
||||
expect(existsSync(join(webDir, ".next", "AO_VERSION"))).toBe(true);
|
||||
});
|
||||
|
||||
it("is a no-op when version matches", async () => {
|
||||
const webDir = join(tmpDir, "web");
|
||||
mkdirSync(join(webDir, ".next", "cache", "webpack"), { recursive: true });
|
||||
writeFileSync(join(webDir, ".next", "AO_VERSION"), "0.2.0");
|
||||
writeFileSync(join(webDir, "package.json"), JSON.stringify({ version: "0.2.0" }));
|
||||
|
||||
const { clearStaleCacheIfNeeded } = await import("../../src/lib/dashboard-rebuild.js");
|
||||
await clearStaleCacheIfNeeded(webDir);
|
||||
|
||||
// cache should still exist
|
||||
expect(existsSync(join(webDir, ".next", "cache", "webpack"))).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves .next/server and .next/static intact", async () => {
|
||||
const webDir = join(tmpDir, "web");
|
||||
mkdirSync(join(webDir, ".next", "cache"), { recursive: true });
|
||||
mkdirSync(join(webDir, ".next", "server"), { recursive: true });
|
||||
mkdirSync(join(webDir, ".next", "static"), { recursive: true });
|
||||
writeFileSync(join(webDir, ".next", "AO_VERSION"), "0.1.0");
|
||||
writeFileSync(join(webDir, "package.json"), JSON.stringify({ version: "0.2.0" }));
|
||||
|
||||
const { clearStaleCacheIfNeeded } = await import("../../src/lib/dashboard-rebuild.js");
|
||||
await clearStaleCacheIfNeeded(webDir);
|
||||
|
||||
expect(existsSync(join(webDir, ".next", "cache"))).toBe(false);
|
||||
expect(existsSync(join(webDir, ".next", "server"))).toBe(true);
|
||||
expect(existsSync(join(webDir, ".next", "static"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("looksLikeStaleBuild pattern matching", () => {
|
||||
// We can't import the private function directly, so we replicate the patterns
|
||||
// to ensure the detection logic catches the actual error messages seen in production.
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ vi.mock("../../src/lib/web-dir.js", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("../../src/lib/dashboard-rebuild.js", () => ({
|
||||
clearStaleCacheIfNeeded: vi.fn().mockResolvedValue(undefined),
|
||||
findRunningDashboardPid: vi.fn().mockResolvedValue(null),
|
||||
rebuildDashboardProductionArtifacts: vi.fn().mockResolvedValue(undefined),
|
||||
waitForPortFree: vi.fn(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const { mockExistsSync, mockReadFileSync, mockRmSync, mockWriteFileSync } = vi.hoisted(() => ({
|
||||
mockExistsSync: vi.fn(),
|
||||
mockReadFileSync: vi.fn(),
|
||||
mockRmSync: vi.fn(),
|
||||
mockWriteFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
existsSync: mockExistsSync,
|
||||
readFileSync: mockReadFileSync,
|
||||
rmSync: mockRmSync,
|
||||
writeFileSync: mockWriteFileSync,
|
||||
}));
|
||||
|
||||
vi.mock("ora", () => ({
|
||||
default: () => ({
|
||||
start: vi.fn().mockReturnThis(),
|
||||
succeed: vi.fn().mockReturnThis(),
|
||||
fail: vi.fn().mockReturnThis(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../src/lib/shell.js", () => ({
|
||||
exec: vi.fn(),
|
||||
execSilent: vi.fn(),
|
||||
}));
|
||||
|
||||
import { clearStaleCacheIfNeeded, isInstalledUnderNodeModules } from "../../src/lib/dashboard-rebuild.js";
|
||||
|
||||
beforeEach(() => {
|
||||
mockExistsSync.mockReset();
|
||||
mockReadFileSync.mockReset();
|
||||
mockRmSync.mockReset();
|
||||
mockWriteFileSync.mockReset();
|
||||
});
|
||||
|
||||
describe("isInstalledUnderNodeModules", () => {
|
||||
it("returns true for paths with node_modules segment", () => {
|
||||
expect(isInstalledUnderNodeModules("/usr/local/lib/node_modules/@composio/ao-web")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for monorepo paths", () => {
|
||||
expect(isInstalledUnderNodeModules("/home/user/agent-orchestrator/packages/web")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearStaleCacheIfNeeded", () => {
|
||||
it("does nothing when package.json does not exist", async () => {
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
await clearStaleCacheIfNeeded("/web");
|
||||
expect(mockRmSync).not.toHaveBeenCalled();
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing when stamp matches current version", async () => {
|
||||
// existsSync: package.json → true, AO_VERSION → true
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("package.json")) return JSON.stringify({ version: "0.2.2" });
|
||||
if (path.includes("AO_VERSION")) return "0.2.2";
|
||||
return "";
|
||||
});
|
||||
|
||||
await clearStaleCacheIfNeeded("/web");
|
||||
expect(mockRmSync).not.toHaveBeenCalled();
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears cache and updates stamp when version differs", async () => {
|
||||
// existsSync: package.json, AO_VERSION, .next/cache, .next → all true
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("package.json")) return JSON.stringify({ version: "0.3.0" });
|
||||
if (path.includes("AO_VERSION")) return "0.2.2";
|
||||
return "";
|
||||
});
|
||||
|
||||
await clearStaleCacheIfNeeded("/web");
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("cache"),
|
||||
{ recursive: true, force: true },
|
||||
);
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("AO_VERSION"),
|
||||
"0.3.0",
|
||||
"utf8",
|
||||
);
|
||||
});
|
||||
|
||||
it("clears cache when stamp file is missing (upgrade from old version)", async () => {
|
||||
mockExistsSync.mockImplementation((path: string) => {
|
||||
if (path.includes("AO_VERSION")) return false;
|
||||
return true; // package.json, .next/cache, .next all exist
|
||||
});
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("package.json")) return JSON.stringify({ version: "0.3.0" });
|
||||
return "";
|
||||
});
|
||||
|
||||
await clearStaleCacheIfNeeded("/web");
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("cache"),
|
||||
{ recursive: true, force: true },
|
||||
);
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("AO_VERSION"),
|
||||
"0.3.0",
|
||||
"utf8",
|
||||
);
|
||||
});
|
||||
|
||||
it("writes stamp but skips rmSync when no cache dir exists", async () => {
|
||||
mockExistsSync.mockImplementation((path: string) => {
|
||||
if (path.includes("cache")) return false;
|
||||
if (path.includes("AO_VERSION")) return false;
|
||||
return true; // package.json, .next exist
|
||||
});
|
||||
mockReadFileSync.mockImplementation((path: string) => {
|
||||
if (path.includes("package.json")) return JSON.stringify({ version: "0.3.0" });
|
||||
return "";
|
||||
});
|
||||
|
||||
await clearStaleCacheIfNeeded("/web");
|
||||
expect(mockRmSync).not.toHaveBeenCalled();
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
expect.stringContaining("AO_VERSION"),
|
||||
"0.3.0",
|
||||
"utf8",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import type { Command } from "commander";
|
|||
import { loadConfig } from "@aoagents/ao-core";
|
||||
import { findWebDir, buildDashboardEnv, waitForPortAndOpen } from "../lib/web-dir.js";
|
||||
import {
|
||||
clearStaleCacheIfNeeded,
|
||||
findRunningDashboardPid,
|
||||
isInstalledUnderNodeModules,
|
||||
rebuildDashboardProductionArtifacts,
|
||||
|
|
@ -54,6 +55,7 @@ export function registerDashboard(program: Command): void {
|
|||
// Fall through to start the dashboard on this port.
|
||||
} else {
|
||||
await preflight.checkBuilt(localWebDir);
|
||||
await clearStaleCacheIfNeeded(localWebDir);
|
||||
}
|
||||
|
||||
const webDir = localWebDir;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,10 @@ import {
|
|||
findFreePort,
|
||||
MAX_PORT_SCAN,
|
||||
} from "../lib/web-dir.js";
|
||||
import { rebuildDashboardProductionArtifacts } from "../lib/dashboard-rebuild.js";
|
||||
import {
|
||||
clearStaleCacheIfNeeded,
|
||||
rebuildDashboardProductionArtifacts,
|
||||
} from "../lib/dashboard-rebuild.js";
|
||||
import { preflight } from "../lib/preflight.js";
|
||||
import {
|
||||
register,
|
||||
|
|
@ -1258,6 +1261,7 @@ async function runStartup(
|
|||
await rebuildDashboardProductionArtifacts(webDir);
|
||||
} else if (!willUseDevServer) {
|
||||
await preflight.checkBuilt(webDir);
|
||||
await clearStaleCacheIfNeeded(webDir);
|
||||
}
|
||||
|
||||
spinner.start("Starting dashboard");
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { existsSync, rmSync } from "node:fs";
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import ora from "ora";
|
||||
import { exec, execSilent } from "./shell.js";
|
||||
|
||||
|
|
@ -69,6 +69,52 @@ export async function cleanNextCache(webDir: string): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the .next/AO_VERSION stamp against the current web package version.
|
||||
* If they differ (or the stamp is missing), clear the .next/cache directory
|
||||
* so Next.js doesn't serve stale pages after a version upgrade.
|
||||
*/
|
||||
export async function clearStaleCacheIfNeeded(webDir: string): Promise<void> {
|
||||
try {
|
||||
const stampPath = resolve(webDir, ".next", "AO_VERSION");
|
||||
const pkgPath = resolve(webDir, "package.json");
|
||||
|
||||
if (!existsSync(pkgPath)) return;
|
||||
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { version: string };
|
||||
const currentVersion = pkg.version;
|
||||
|
||||
if (!currentVersion) return;
|
||||
|
||||
let needsClear = false;
|
||||
|
||||
if (!existsSync(stampPath)) {
|
||||
needsClear = true;
|
||||
} else {
|
||||
const stamp = readFileSync(stampPath, "utf8").trim();
|
||||
needsClear = stamp !== currentVersion;
|
||||
}
|
||||
|
||||
if (needsClear) {
|
||||
const cacheDir = resolve(webDir, ".next", "cache");
|
||||
if (existsSync(cacheDir)) {
|
||||
const spinner = ora();
|
||||
spinner.start("Clearing stale Next.js cache (version upgrade detected)");
|
||||
rmSync(cacheDir, { recursive: true, force: true });
|
||||
spinner.succeed(`Cleared stale .next cache → v${currentVersion}`);
|
||||
}
|
||||
// Update stamp so subsequent starts skip the check
|
||||
const nextDir = resolve(webDir, ".next");
|
||||
if (existsSync(nextDir)) {
|
||||
writeFileSync(stampPath, currentVersion, "utf8");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Best-effort cache cleanup — never prevent dashboard from starting
|
||||
console.debug("Cache version check skipped:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild dashboard production artifacts (Next.js build + server compilation)
|
||||
* from a source checkout. Throws if called from an npm global install.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"dev:next": "next dev -p ${PORT:-3000}",
|
||||
"dev:direct-terminal": "node scripts/dev-direct-terminal.mjs",
|
||||
"prebuild": "rimraf .next dist-server",
|
||||
"build": "next build && tsc -p tsconfig.server.json",
|
||||
"build": "next build && tsc -p tsconfig.server.json && node scripts/stamp-version.js",
|
||||
"start": "next start",
|
||||
"start:all": "node dist-server/start-all.js",
|
||||
"dev:optimized": "rimraf .next dist-server && next build && tsc -p tsconfig.server.json && node dist-server/start-all.js",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Writes the current package version to .next/AO_VERSION after a Next.js build.
|
||||
* This stamp is compared at `ao start` to detect stale runtime caches from
|
||||
* a previous version and clear them automatically.
|
||||
*/
|
||||
|
||||
import { writeFileSync, readFileSync, existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const nextDir = resolve(__dirname, "..", ".next");
|
||||
|
||||
if (!existsSync(nextDir)) {
|
||||
console.warn("stamp-version: .next directory not found — skipping stamp");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const pkgPath = resolve(__dirname, "..", "package.json");
|
||||
if (!existsSync(pkgPath)) {
|
||||
console.warn("stamp-version: package.json not found — skipping stamp");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
||||
if (!pkg.version) {
|
||||
console.warn("stamp-version: no version field in package.json — skipping stamp");
|
||||
process.exit(0);
|
||||
}
|
||||
writeFileSync(resolve(nextDir, "AO_VERSION"), pkg.version, "utf8");
|
||||
Loading…
Reference in New Issue