forge/tools/sync-docus-docs.mjs
Jacob Schmidt a4d5c2fd4d Enhance documentation structure and content across multiple guides
- Added frontmatter to various markdown files for better metadata handling.
- Updated site URLs in configuration files for consistency.
- Improved content organization and clarity in getting started, server extension, and client addon guides.
2026-05-16 10:33:17 -05:00

775 lines
18 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const docusDir = path.join(repoRoot, 'docus');
const contentDir = path.join(docusDir, 'content');
const generatedPages = [
{
source: 'docs/FRAMEWORK_ARCHITECTURE.md',
target: '1.getting-started/1.architecture.md'
},
{
source: 'docs/MODULE_REFERENCE.md',
target: '1.getting-started/2.module-reference.md'
},
{
source: 'docs/DEVELOPMENT_GUIDE.md',
target: '1.getting-started/3.development.md'
},
{
source: 'docs/surrealdb-setup.md',
target: '1.getting-started/4.surrealdb-setup.md'
},
{
source: 'arma/server/docs/README.md',
target: '2.server-extension/0.index.md'
},
{
source: 'arma/server/docs/api-reference.md',
target: '2.server-extension/1.api-reference.md'
},
{
source: 'arma/server/docs/usage-examples.md',
target: '2.server-extension/2.usage-examples.md'
},
{
source: 'arma/server/addons/common/README.md',
target: '2.server-extension/3.common.md'
},
{
source: 'docs/ACTOR_USAGE_GUIDE.md',
target: '3.server-modules/1.actor.md'
},
{
source: 'docs/BANK_USAGE_GUIDE.md',
target: '3.server-modules/2.bank.md'
},
{
source: 'docs/CAD_USAGE_GUIDE.md',
target: '3.server-modules/3.cad.md'
},
{
source: 'docs/ECONOMY_USAGE_GUIDE.md',
target: '3.server-modules/4.economy.md'
},
{
source: 'docs/GARAGE_USAGE_GUIDE.md',
target: '3.server-modules/5.garage.md'
},
{
source: 'docs/LOCKER_USAGE_GUIDE.md',
target: '3.server-modules/6.locker.md'
},
{
source: 'docs/ORG_USAGE_GUIDE.md',
target: '3.server-modules/7.organization.md'
},
{
source: 'docs/OWNED_STORAGE_USAGE_GUIDE.md',
target: '3.server-modules/8.owned-storage.md'
},
{
source: 'docs/PHONE_USAGE_GUIDE.md',
target: '3.server-modules/9.phone.md'
},
{
source: 'docs/STORE_USAGE_GUIDE.md',
target: '3.server-modules/10.store.md'
},
{
source: 'docs/TASK_USAGE_GUIDE.md',
target: '3.server-modules/11.task.md'
},
{
source: 'docs/CLIENT_USAGE_GUIDE.md',
target: '4.client-addons/0.index.md'
},
{
source: 'docs/CLIENT_MAIN_USAGE_GUIDE.md',
target: '4.client-addons/1.main.md'
},
{
source: 'docs/CLIENT_COMMON_USAGE_GUIDE.md',
target: '4.client-addons/2.common.md'
},
{
source: 'docs/CLIENT_ACTOR_USAGE_GUIDE.md',
target: '4.client-addons/3.actor.md'
},
{
source: 'docs/CLIENT_BANK_USAGE_GUIDE.md',
target: '4.client-addons/4.bank.md'
},
{
source: 'docs/CLIENT_CAD_USAGE_GUIDE.md',
target: '4.client-addons/5.cad.md'
},
{
source: 'docs/CLIENT_GARAGE_USAGE_GUIDE.md',
target: '4.client-addons/6.garage.md'
},
{
source: 'docs/CLIENT_LOCKER_USAGE_GUIDE.md',
target: '4.client-addons/7.locker.md'
},
{
source: 'docs/CLIENT_NOTIFICATIONS_USAGE_GUIDE.md',
target: '4.client-addons/8.notifications.md'
},
{
source: 'docs/CLIENT_ORG_USAGE_GUIDE.md',
target: '4.client-addons/9.organization.md'
},
{
source: 'docs/CLIENT_PHONE_USAGE_GUIDE.md',
target: '4.client-addons/10.phone.md'
},
{
source: 'docs/CLIENT_STORE_USAGE_GUIDE.md',
target: '4.client-addons/11.store.md'
}
];
const virtualRoutes = new Map([
['README.md', '/getting-started'],
['docs/README.md', '/getting-started']
]);
for (const page of generatedPages) {
virtualRoutes.set(toPosix(page.source), toRoute(page.target));
}
const staticFiles = [
{
target: 'index.md',
content: `---
seo:
title: Forge Framework Documentation
description: Documentation for the Forge Arma 3 framework, covering architecture, persistence, extension APIs, gameplay modules, and client UIs.
---
::u-page-hero
#title
Forge Framework Documentation
#description
Forge is a persistent Arma 3 framework that combines SQF addons, a Rust
\`arma-rs\` extension, SurrealDB persistence, shared domain crates, and
browser-backed player interfaces.
Use these docs to understand the runtime architecture, extension API surface,
server gameplay modules, and client addon integration patterns.
#links
:::u-button
---
color: primary
size: xl
to: /getting-started
trailing-icon: i-lucide-arrow-right
---
Start here
:::
:::u-button
---
color: neutral
icon: simple-icons-github
size: xl
to: https://github.com/InnovativeDevSolutions/forge
variant: outline
---
View source
:::
::
::u-page-section
#title
What Forge Covers
#features
:::u-page-feature
---
icon: i-lucide-boxes
---
#title
Domain [Modules]{.text-primary}
#description
Actor, bank, CAD, garage, locker, organization, phone, store, task, and
owned-storage workflows share a consistent service and extension model.
:::
:::u-page-feature
---
icon: i-lucide-server
---
#title
Rust [Extension]{.text-primary}
#description
The server extension keeps command parsing thin, routes domain requests into
services, and persists durable state through SurrealDB.
:::
:::u-page-feature
---
icon: i-lucide-database-zap
---
#title
Durable [Persistence]{.text-primary}
#description
Repository traits stay storage-agnostic while concrete adapters in the
extension handle schema and database mapping.
:::
:::u-page-feature
---
icon: i-lucide-monitor-smartphone
---
#title
Browser [UIs]{.text-primary}
#description
Client addons host web-based interfaces inside Arma displays and synchronize
state through namespaced browser bridge events.
:::
:::u-page-feature
---
icon: i-lucide-arrow-left-right
---
#title
Transport [Layer]{.text-primary}
#description
Large payloads move through chunked request and response transport while
smaller commands still use direct \`callExtension\` paths.
:::
:::u-page-feature
---
icon: i-lucide-wrench
---
#title
Development [Workflow]{.text-primary}
#description
The docs cover module boundaries, local validation checks, and where new
domain logic belongs across Rust, SQF, and web UI layers.
:::
::
::u-page-section
#title
Documentation Areas
#features
:::u-page-feature
---
icon: i-lucide-rocket
to: /getting-started
---
#title
[Getting Started]{.text-primary}
#description
Framework overview, architecture, module reference, and development rules.
:::
:::u-page-feature
---
icon: i-lucide-server-cog
to: /server-extension
---
#title
Server [Extension]{.text-primary}
#description
Extension architecture, command surface, and SQF usage examples.
:::
:::u-page-feature
---
icon: i-lucide-layers-3
to: /server-modules
---
#title
Server [Modules]{.text-primary}
#description
Gameplay-domain usage guides for persistence, hot state, and command flows.
:::
:::u-page-feature
---
icon: i-lucide-monitor-smartphone
to: /client-addons
---
#title
Client [Addons]{.text-primary}
#description
Browser bridge, client UX entry points, and addon-specific event contracts.
:::
::
`
},
{
target: '1.getting-started/.navigation.yml',
content: `title: Getting Started
icon: i-lucide-rocket
`
},
{
target: '1.getting-started/0.index.md',
content: `---
title: Getting Started
description: Use this section as the main entry point for the Forge framework.
---
Forge combines:
- Arma 3 client addons for UX and browser-hosted interfaces
- Arma 3 server addons for mission integration and authoritative flow control
- a Rust server extension for command routing and persistence
- shared Rust crates for models, repositories, and services
- SurrealDB for durable storage
## Common Commands
\`\`\`powershell
cargo test
npm run build:webui
.\\build-arma.ps1
\`\`\`
## Start Here
::u-page-grid
:::u-page-card
---
icon: i-lucide-network
title: Architecture
to: /getting-started/architecture
---
Understand how SQF, Rust services, SurrealDB, and browser UIs fit together.
:::
:::u-page-card
---
icon: i-lucide-boxes
title: Module Reference
to: /getting-started/module-reference
---
Review gameplay domains, infrastructure modules, and extension command groups.
:::
:::u-page-card
---
icon: i-lucide-wrench
title: Development Guide
to: /getting-started/development
---
See the rules for adding modules and changing boundaries without regressions.
:::
:::u-page-card
---
icon: i-lucide-database
title: SurrealDB Setup
to: /getting-started/surrealdb-setup
---
Install SurrealDB, match Forge config values, and choose the right setup path
for developers or admin-facing roles.
:::
:::u-page-card
---
icon: i-lucide-server-cog
title: Server Extension
to: /server-extension
---
Follow the extension architecture, API surface, and SQF usage examples.
:::
:::u-page-card
---
icon: i-lucide-layers-3
title: Server Modules
to: /server-modules
---
Dive into the actor, bank, CAD, garage, locker, organization, phone, store,
task, and owned-storage guides.
:::
:::u-page-card
---
icon: i-lucide-monitor-smartphone
title: Client Addons
to: /client-addons
---
Explore the client bridge model and addon-specific browser integration rules.
:::
::
`
},
{
target: '3.server-modules/.navigation.yml',
content: `title: Server Modules
icon: i-lucide-layers-3
`
},
{
target: '3.server-modules/0.index.md',
content: `---
title: Server Module Guides
description: These pages document the authoritative server-side workflows in Forge.
---
Most modules follow the same shape:
1. Server SQF gathers game context and validates mission/runtime assumptions.
2. The \`forge_server\` extension routes the request into the matching command group.
3. Services apply business rules through storage-agnostic repository traits.
4. The extension persists durable state through SurrealDB adapters when needed.
## Gameplay Domains
::u-page-grid
:::u-page-card
---
icon: i-lucide-user-round
title: Actor
to: /server-modules/actor
---
Persistent player identity, position, loadout, contact fields, and hot state.
:::
:::u-page-card
---
icon: i-lucide-wallet
title: Bank
to: /server-modules/bank
---
Player funds, transfers, PIN validation, checkout charging, and bank hot state.
:::
:::u-page-card
---
icon: i-lucide-map
title: CAD
to: /server-modules/cad
---
Dispatch requests, assignments, profiles, grouped state, and hydrated views.
:::
:::u-page-card
---
icon: i-lucide-ambulance
title: Economy
to: /server-modules/economy
---
Fuel, service, and medical charging rules across player and organization funds.
:::
:::u-page-card
---
icon: i-lucide-car-front
title: Garage
to: /server-modules/garage
---
Vehicle storage, hot-state updates, and persistence of vehicle condition.
:::
:::u-page-card
---
icon: i-lucide-package
title: Locker
to: /server-modules/locker
---
Player inventory storage, unique item limits, and locker hot-state behavior.
:::
:::u-page-card
---
icon: i-lucide-building-2
title: Organization
to: /server-modules/organization
---
Membership, treasury, shared assets, fleet, and organization hot workflows.
:::
:::u-page-card
---
icon: i-lucide-key-round
title: Owned Storage
to: /server-modules/owned-storage
---
Owner-scoped locker and vehicle unlock storage used by org-linked features.
:::
:::u-page-card
---
icon: i-lucide-smartphone
title: Phone
to: /server-modules/phone
---
Contacts, message threads, and email state for in-game phone workflows.
:::
:::u-page-card
---
icon: i-lucide-shopping-cart
title: Store
to: /server-modules/store
---
Checkout orchestration across pricing, grants, payment sources, and rollback.
:::
:::u-page-card
---
icon: i-lucide-flag
title: Task
to: /server-modules/task
---
Task catalog, ownership, status transitions, defuse counters, and rewards.
:::
::
`
},
{
target: '4.client-addons/.navigation.yml',
content: `title: Client Addons
icon: i-lucide-monitor-smartphone
`
}
];
await fs.rm(contentDir, { recursive: true, force: true });
await fs.mkdir(contentDir, { recursive: true });
for (const file of staticFiles) {
await writeContentFile(file.target, file.content);
}
for (const page of generatedPages) {
const sourceRel = toPosix(page.source);
const sourcePath = path.join(repoRoot, page.source);
const rawContent = await fs.readFile(sourcePath, 'utf8');
const content = prepareGeneratedPageContent(rewriteMarkdownLinks(rawContent, sourceRel));
await writeContentFile(page.target, content);
}
console.log(`Generated ${staticFiles.length + generatedPages.length} Docus content files.`);
function rewriteMarkdownLinks(content, sourceRel) {
const sourceDir = path.posix.dirname(sourceRel);
return content.replace(/\]\(([^)]+)\)/g, (match, rawTarget) => {
if (
rawTarget.startsWith('http://') ||
rawTarget.startsWith('https://') ||
rawTarget.startsWith('#') ||
rawTarget.startsWith('mailto:')
) {
return match;
}
const [targetPath, targetHash] = rawTarget.split('#');
if (!targetPath || !targetPath.toLowerCase().endsWith('.md')) {
return match;
}
const normalizedTarget = toPosix(
path.posix.normalize(path.posix.join(sourceDir, targetPath.replace(/\\/g, '/')))
);
const route = virtualRoutes.get(normalizedTarget);
if (!route) {
return match;
}
return `](${route}${targetHash ? `#${targetHash}` : ''})`;
});
}
function prepareGeneratedPageContent(content) {
const title = extractFirstH1(content);
const description = extractLeadParagraph(content);
const body = stripMatchingLeadParagraph(stripFirstH1(content), description).trimStart();
const frontmatter = [
'---',
title ? `title: ${yamlString(title)}` : undefined,
description ? `description: ${yamlString(description)}` : undefined,
'---'
].filter(Boolean).join('\n');
return `${frontmatter}\n\n${body}`;
}
function extractFirstH1(content) {
const match = content.match(/^#\s+(.+?)\s*#*\s*$/m);
return match ? match[1].trim() : '';
}
function extractLeadParagraph(content) {
const lines = stripFirstH1(content).split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index].trim();
if (!line) {
continue;
}
if (!isParagraphStart(line)) {
continue;
}
const paragraph = [];
for (let paragraphIndex = index; paragraphIndex < lines.length; paragraphIndex += 1) {
const paragraphLine = lines[paragraphIndex].trim();
if (!paragraphLine) {
break;
}
paragraph.push(paragraphLine);
}
return normalizeParagraph(paragraph.join(' '));
}
return '';
}
function stripFirstH1(content) {
const lines = content.split(/\r?\n/);
const headingIndex = lines.findIndex((line) => /^#\s+.+/.test(line.trim()));
if (headingIndex === -1) {
return content;
}
lines.splice(headingIndex, 1);
while (headingIndex < lines.length && !lines[headingIndex].trim()) {
lines.splice(headingIndex, 1);
}
return lines.join('\n');
}
function stripMatchingLeadParagraph(content, description) {
if (!description) {
return content;
}
const lines = content.split(/\r?\n/);
let startIndex = 0;
while (startIndex < lines.length && !lines[startIndex].trim()) {
startIndex += 1;
}
if (startIndex < lines.length && /^##\s+overview\s*$/i.test(lines[startIndex].trim())) {
const sectionStart = startIndex;
startIndex += 1;
while (startIndex < lines.length && !lines[startIndex].trim()) {
startIndex += 1;
}
let sectionEnd = startIndex;
const sectionLines = [];
while (sectionEnd < lines.length && !/^#{2,}\s+/.test(lines[sectionEnd].trim())) {
if (lines[sectionEnd].trim()) {
sectionLines.push(lines[sectionEnd].trim());
}
sectionEnd += 1;
}
if (normalizeParagraph(sectionLines.join(' ')) === description) {
while (sectionEnd < lines.length && !lines[sectionEnd].trim()) {
sectionEnd += 1;
}
return [...lines.slice(0, sectionStart), ...lines.slice(sectionEnd)].join('\n');
}
startIndex = sectionStart;
}
if (startIndex >= lines.length || !isParagraphStart(lines[startIndex].trim())) {
return content;
}
let endIndex = startIndex;
const paragraph = [];
while (endIndex < lines.length && lines[endIndex].trim()) {
paragraph.push(lines[endIndex].trim());
endIndex += 1;
}
if (normalizeParagraph(paragraph.join(' ')) !== description) {
return content;
}
while (endIndex < lines.length && !lines[endIndex].trim()) {
endIndex += 1;
}
return [...lines.slice(0, startIndex), ...lines.slice(endIndex)].join('\n');
}
function isParagraphStart(line) {
return !(
line.startsWith('#') ||
line.startsWith('![') ||
line.startsWith('```') ||
line.startsWith(':::') ||
line.startsWith('::') ||
line.startsWith('|') ||
/^[-*+]\s+/.test(line) ||
/^\d+\.\s+/.test(line)
);
}
function normalizeParagraph(value) {
return value.replace(/\s+/g, ' ').trim();
}
function yamlString(value) {
return JSON.stringify(value);
}
function toRoute(target) {
const withoutExt = toPosix(target).replace(/\.md$/i, '');
const parts = withoutExt.split('/');
if (parts.length === 1 && parts[0] === 'index') {
return '/';
}
const mapped = parts
.map((part, index) => {
if (index === parts.length - 1 && (part === '0.index' || part === 'index')) {
return '';
}
return part.replace(/^\d+\./, '');
})
.filter(Boolean);
return `/${mapped.join('/')}`;
}
function toPosix(value) {
return value.replace(/\\/g, '/');
}
async function writeContentFile(target, content) {
const targetPath = path.join(contentDir, target);
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.writeFile(targetPath, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
}