docs: ship the deployment Skill with applications and finish the deployment guide (#376)

* docs: ship the deployment Skill with app-skills and close the deployment guide gaps

Move `nocobase-deployment` from the repository-only `.agents/skills/` into
`@nocobase/app-skills`, so `nocobase skills sync` delivers it to every
generated application; the repository keeps a relative symbolic link there
and the Claude mirror follows it. Point each template's AGENTS.md at the
Skill, explain in the READMEs and the Skill that `pnpm build` already runs
`pnpm install --prod` inside `dist/`, tell the workflow runtime's missing
Artifact error how to enable the deployed hash, and fix the tutorial's
archive path and start command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: write the English deployment overview, configuration, standalone and Docker pages

The four pages were "Being written" placeholders while their Chinese
counterparts were complete. Each now carries the same content as the
Chinese page: the deployment modes and how Hub relates to applications,
the shared, standalone and Hub-hosted configuration with the database,
secret, address and reverse proxy details, the build-to-systemd flow with
its acceptance checks, and the Docker image, Compose and update steps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Katherine
2026-09-22 18:19:16 +08:00
committed by GitHub
co-authored by Claude Fable 5.1
parent e612b11183
commit 2ae6b2b722
23 changed files with 714 additions and 74 deletions
+25 -9
View File
@@ -11,7 +11,7 @@
//
// Links rather than copies, so editing a Skill through either path edits the one committed file. Relative links, so
// the checkout stays movable.
import { lstat, mkdir, readdir, symlink, unlink } from 'node:fs/promises';
import { lstat, mkdir, readdir, stat, symlink, unlink } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
@@ -71,18 +71,24 @@ async function createLink(root, name, linkPath) {
}
}
// `withFileTypes` reports a symbolic link as a link rather than as the directory it points at, so `.claude/skills/`
// has to accept both: its entries are the links this script writes, and a link whose target is already gone still has
// to be found in order to be pruned.
// `withFileTypes` reports a symbolic link as a link rather than as the directory it points at, so both directories
// need care. `.claude/skills/` accepts links outright: its entries are the links this script writes, and a link whose
// target is already gone still has to be found in order to be pruned. `.agents/skills/` accepts a link only when it
// resolves to a directory: a Skill that is published with a package lives in that package and is committed here as a
// relative link, and a link is still one Skill to mirror.
async function readDirectoryNames(directory, { links = false } = {}) {
try {
const entries = await readdir(directory, { withFileTypes: true });
return entries
.filter(
(entry) => entry.isDirectory() || (links && entry.isSymbolicLink()),
const names = [];
for (const entry of entries) {
if (
entry.isDirectory() ||
(entry.isSymbolicLink() &&
(links || (await isDirectory(path.join(directory, entry.name)))))
)
.map((entry) => entry.name)
.sort();
names.push(entry.name);
}
return names.sort();
} catch (error) {
if (isNodeError(error, 'ENOENT') || isNodeError(error, 'ENOTDIR'))
return [];
@@ -110,6 +116,16 @@ async function removeSymbolicLink(linkPath) {
return true;
}
async function isDirectory(target) {
try {
return (await stat(target)).isDirectory();
} catch (error) {
if (isNodeError(error, 'ENOENT') || isNodeError(error, 'ENOTDIR'))
return false;
throw error;
}
}
function isNodeError(error, code) {
return error !== null && typeof error === 'object' && error.code === code;
}