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:
+1
-1
@@ -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).
|
||||
|
||||
|
||||
@@ -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()));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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-"));
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+22
@@ -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.
|
||||
|
||||
+1851
File diff suppressed because one or more lines are too long
+102
@@ -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.
|
||||
*/
|
||||
Reference in New Issue
Block a user