perf: lighter, louder local install — vendored ReDoc, bootstrap failures shown in the session

- Plugin bootstrap: a failed npm install, a Node older than 22.5 or a failed UI
  build is recorded in runtime/last-failure.json; the next SessionStart prints
  it once as a systemMessage naming the log and /agenthub-doctor (a different
  failure is reported again; a successful bootstrap clears it). The foreground
  pass checks the Node version itself and spawns no worker on an old Node.
- ReDoc: the standalone bundle (redoc 2.5.4, MIT, licence files kept) is
  vendored under server/vendor/redoc/ and the `redoc` dependency dropped —
  a runtime install (npm ci --omit=dev --ignore-scripts) goes 126 MB -> 45 MB.
  vendor/ is excluded from the header audit and prettier.
- git missing from PATH: lane git actions answer 503 EGITMISSING with the
  install command instead of a bare ENOENT.
- package.json engines: node >=22.5.0 (node:sqlite), matching MIN_NODE.
- Docs: INSTALL.md is the one install/setup guide (SETUP.md merged in and now a
  pointer); no VS Code extension claim, one client-install hint, no
  windows-build-tools, better-sqlite3 notes scoped to checkouts, new
  "Plugin bootstrap never comes up" troubleshooting entry.
This commit is contained in:
2026-10-16 10:16:54 +07:00
parent ecbbb4dd8f
commit 9a4ed54cf7
26 changed files with 2667 additions and 1527 deletions
+1 -1
View File
@@ -439,7 +439,7 @@ All endpoints return JSON unless noted. Error responses use:
| `GET` | `/api/openapi.json` | Raw OpenAPI 3.0.3 spec |
| `GET` | `/api/docs` | Interactive **Swagger UI** (try-it-out request execution) |
| `GET` | `/api/redoc` | **ReDoc** reference — clean, read-optimized three-panel rendering of the same spec |
| `GET` | `/api/redoc/redoc.standalone.js` | Self-hosted ReDoc bundle (via the `redoc` dependency, never a CDN — works offline) |
| `GET` | `/api/redoc/redoc.standalone.js` | Self-hosted ReDoc bundle (vendored in `server/vendor/redoc/`, never a CDN — works offline) |
The OpenAPI spec is generated from `server/openapi.js` (`createOpenApiSpec()`), merged with supplementary fragments under `server/openapi-extra/`, and is the source of truth for request/response contracts. It now documents every backend route (75 path entries). Both Swagger UI and ReDoc (`server/lib/redoc.js`) render the same spec; the ReDoc bundle is served locally so the reference works offline / air-gapped. A committed `openapi.yaml` at the repo root mirrors the live spec — regenerate it after API changes with `npm run openapi:yaml` (never hand-edit it).
+7
View File
@@ -213,6 +213,13 @@ describe("OpenAPI / Swagger", () => {
const res = await fetch("/api/redoc/redoc.standalone.js");
assert.equal(res.status, 200);
assert.match(res.headers["content-type"], /javascript/);
// The vendored file (server/vendor/redoc/), licence banner intact — the
// `redoc` npm package is no longer a dependency.
assert.match(
res.body,
/For license information please see redoc\.standalone\.js\.LICENSE\.txt/
);
assert.doesNotThrow(() => require("fs").statSync(require("../lib/redoc").redocBundlePath()));
});
});
+35
View File
@@ -91,6 +91,41 @@ describe("node version gate", () => {
});
});
describe("last failure report (SessionStart systemMessage)", () => {
beforeEach(resetRuntime);
it("reports a recorded failure once, naming the log and /agenthub-doctor", () => {
boot.recordFailure(RT, "install-failed", "npm broke", path.join(RT, "npm-install.log"));
const first = boot.takeFailureReport(RT);
assert.match(first.systemMessage, /npm broke/);
assert.match(first.systemMessage, /npm-install\.log/);
assert.match(first.systemMessage, /\/agenthub-doctor/);
assert.equal(boot.takeFailureReport(RT), null);
});
it("does not re-report the same failure recorded again by a later retry", () => {
boot.recordFailure(RT, "install-failed", "npm broke", "x.log");
boot.takeFailureReport(RT);
boot.recordFailure(RT, "install-failed", "npm broke", "x.log");
assert.equal(boot.takeFailureReport(RT), null);
});
it("reports a different failure, and nothing once cleared", () => {
boot.recordFailure(RT, "install-failed", "npm broke", "x.log");
boot.takeFailureReport(RT);
boot.recordFailure(RT, "ui-build-failed", "vite broke", "y.log");
assert.match(boot.takeFailureReport(RT).systemMessage, /vite broke/);
boot.recordFailure(RT, "ui-build-failed", "vite broke", "y.log");
boot.clearFailure(RT);
assert.equal(boot.readFailure(RT), null);
assert.equal(boot.takeFailureReport(RT), null);
});
it("prints nothing with no failure on record", () => {
assert.equal(boot.takeFailureReport(RT), null);
});
});
describe("depsHash — only what npm installs", () => {
function root(pkg, lock) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-deps-"));
+20
View File
@@ -465,3 +465,23 @@ describe("listBranches", () => {
assert.deepEqual(branches, []);
});
});
describe("git missing from PATH", () => {
it("answers EGITMISSING with the fix, not a bare ENOENT, when the folder exists", async () => {
const saved = process.env.PATH;
process.env.PATH = path.join(ROOT, "no-such-bin");
try {
await assert.rejects(
() => wt.git(ROOT, ["status"]),
(err) => err.code === "EGITMISSING" && /install/i.test(err.message)
);
// A missing FOLDER is the other ENOENT, and stays one.
await assert.rejects(
() => wt.git(path.join(ROOT, "gone"), ["status"]),
(err) => err.code === "ENOENT"
);
} finally {
process.env.PATH = saved;
}
});
});
+16 -8
View File
@@ -2,21 +2,29 @@
* @file Self-hosted ReDoc API reference. ReDoc renders the OpenAPI spec as a
* clean, three-panel reference document — a read-optimized complement to
* Swagger UI's interactive "try it out" console (both are served from the same
* `/api/openapi.json` spec). The ReDoc bundle ships with the `redoc`
* dependency and is served straight from `node_modules` rather than a CDN, so
* the docs render fully offline / air-gapped, consistent with the project's
* no-external-assets policy.
* `/api/openapi.json` spec). The ReDoc bundle is served from the repo itself
* rather than a CDN, so the docs render fully offline / air-gapped, consistent
* with the project's no-external-assets policy.
*
* The bundle is VENDORED (`server/vendor/redoc/`): `redoc.standalone.js` from
* the `redoc` npm package 2.5.4, byte-for-byte, with its MIT `LICENSE` and the
* bundled libraries' `redoc.standalone.js.LICENSE.txt`. The `redoc` package
* itself is not a dependency — the server only ever serves this one browser
* file, and the package pulled ~25 MB of Node-side dependencies into every
* runtime install. To update: `npm pack redoc@<version>`, copy
* `package/bundles/redoc.standalone.js` (+ `.LICENSE.txt`) over these files and
* bump the version named here.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const path = require("path");
/**
* Absolute path to the prebuilt ReDoc standalone bundle inside the installed
* `redoc` package. Resolved through Node's module resolution so it works
* regardless of hoisting / install layout. Throws if `redoc` is not installed.
* Absolute path to the vendored ReDoc standalone bundle.
* @returns {string}
*/
function redocBundlePath() {
return require.resolve("redoc/bundles/redoc.standalone.js");
return path.join(__dirname, "..", "vendor", "redoc", "redoc.standalone.js");
}
/**
+13
View File
@@ -115,6 +115,19 @@ async function git(
});
return { stdout: result.stdout, stderr: result.stderr };
} catch (err) {
// ENOENT from execFile is ambiguous: a missing binary OR a missing cwd.
// With the folder present it is git itself that is absent — say so, with
// the fix, rather than `git status failed` (like ETMUXMISSING for tmux).
if (err.code === "ENOENT" && cmd === "git" && fs.existsSync(cwd)) {
const error = new Error(
"git is not installed (or not on the dashboard's PATH), and every git action needs it. " +
"Install it (sudo apt install git / brew install git / https://git-scm.com/download/win), " +
"then restart the dashboard."
);
error.code = "EGITMISSING";
error.git = { args, code: "EGITMISSING", stderr: "", stdout: "" };
throw error;
}
const error = new Error(`git ${args[0]} failed`);
error.code = err.code;
error.git = { args, code: err.code, stderr: err.stderr, stdout: err.stdout };
+4 -1
View File
@@ -1937,7 +1937,10 @@ router.post("/:id/git/:action", sameOriginGuard, async (req, res) => {
"EMEMBERGONE",
].includes(err.code)
? 409
: 500;
: // Not the request's fault and not a server bug: the host lacks git.
err.code === "EGITMISSING"
? 503
: 500;
// git's own stderr is the useful half of a failed push or pull (a
// rejection, a missing credential, a diverged branch), so it joins the
// message rather than riding a side field the client would have to know
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015-present, Rebilly, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because one or more lines are too long
+102
View File
@@ -0,0 +1,102 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* ReDoc - OpenAPI/Swagger-generated API Reference Documentation
* -------------------------------------------------------------
* Version: "2.5.4"
* Repo: https://github.com/Redocly/redoc
*/
/*!
* Stickyfill -- `position: sticky` polyfill
* v. 1.1.1 | https://github.com/wilddeer/stickyfill
* Copyright Oleg Korsunsky | http://wd.dizaina.net/
*
* MIT License
*/
/*!
* perfect-scrollbar v1.5.3
* Copyright 2021 Hyunje Jun, MDBootstrap and Contributors
* Licensed under MIT
*/
/*! @license DOMPurify 3.4.13 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.13/LICENSE */
/*!***************************************************
* mark.js v8.11.1
* https://markjs.io/
* Copyright (c) 2014–2018, Julian Kühnel
* Released under the MIT license https://git.io/vwTVl
*****************************************************/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-shim.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Prism: Lightweight, robust, elegant syntax highlighting
*
* @license MIT <https://opensource.org/licenses/MIT>
* @author Lea Verou <https://lea.verou.me>
* @namespace
* @public
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/