chore: import upstream snapshot with attribution
docmd CI verification / verify (push) Failing after 0s
docmd CI verification / verify (push) Failing after 0s
This commit is contained in:
+103
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the minimalist, zero-config documentation generator.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const newVersion = process.argv[2];
|
||||
|
||||
if (!newVersion) {
|
||||
console.error("Usage: node tools/bump-version.js <new-version>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const root = process.cwd();
|
||||
const packagesDir = path.join(root, "packages");
|
||||
|
||||
function updateVersion(pkgPath) {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
pkg.version = newVersion;
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
||||
console.log(`Updated: ${pkg.name} → ${newVersion}`);
|
||||
}
|
||||
|
||||
function updateSourceVersion(pkgDir) {
|
||||
const indexPath = path.join(pkgDir, "src", "index.ts");
|
||||
if (fs.existsSync(indexPath)) {
|
||||
let content = fs.readFileSync(indexPath, "utf8");
|
||||
const versionRegex = /version:\s*(['"])(.*?)(['"])/g;
|
||||
// Update any file in src/index.ts that has a version property
|
||||
if (versionRegex.test(content)) {
|
||||
content = content.replace(versionRegex, `version: $1${newVersion}$3`);
|
||||
fs.writeFileSync(indexPath, content);
|
||||
console.log(`Updated Source Version: ${path.basename(pkgDir)} → ${newVersion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCargoVersion(pkgDir) {
|
||||
const cargoPath = path.join(pkgDir, "native", "Cargo.toml");
|
||||
if (fs.existsSync(cargoPath)) {
|
||||
let content = fs.readFileSync(cargoPath, "utf8");
|
||||
const versionRegex = /version\s*=\s*(['"])(.*?)(['"])/;
|
||||
if (versionRegex.test(content)) {
|
||||
content = content.replace(versionRegex, `version = "${newVersion}"`);
|
||||
fs.writeFileSync(cargoPath, content);
|
||||
console.log(`Updated Cargo Version: ${path.basename(pkgDir)} → ${newVersion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDockerVersion() {
|
||||
// Update Docker workflow if it exists
|
||||
const dockerWorkflowPath = path.join(root, ".github", "workflows", "docker-publish.yml");
|
||||
if (fs.existsSync(dockerWorkflowPath)) {
|
||||
let content = fs.readFileSync(dockerWorkflowPath, "utf8");
|
||||
// Update any hardcoded version references in comments or examples
|
||||
// This is mainly for documentation purposes in the workflow
|
||||
const versionCommentRegex = /# Version:\s*v?[\d.]+/g;
|
||||
if (versionCommentRegex.test(content)) {
|
||||
content = content.replace(versionCommentRegex, `# Version: v${newVersion}`);
|
||||
fs.writeFileSync(dockerWorkflowPath, content);
|
||||
console.log(`Updated Docker Workflow Version → ${newVersion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1️⃣ Update root
|
||||
updateVersion(path.join(root, "package.json"));
|
||||
|
||||
// 2️⃣ Update Docker version references
|
||||
updateDockerVersion();
|
||||
|
||||
// 2️⃣ Recursively update all packages
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
if (['node_modules', 'dist', '.build'].includes(entry)) continue;
|
||||
|
||||
const full = path.join(dir, entry);
|
||||
|
||||
if (fs.existsSync(path.join(full, "package.json"))) {
|
||||
updateVersion(path.join(full, "package.json"));
|
||||
updateSourceVersion(full);
|
||||
updateCargoVersion(full);
|
||||
} else if (fs.statSync(full).isDirectory()) {
|
||||
walk(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(packagesDir);
|
||||
|
||||
console.log("Version bump complete.");
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# Stub out build scripts that cannot run inside Docker before pnpm -r run build.
|
||||
# This file is only executed during Docker image builds.
|
||||
|
||||
# engine-rust-binaries: requires the Rust toolchain (cargo) which is not
|
||||
# installed in the builder image. The pre-built .node binaries are committed
|
||||
# to the repo and copied into the production image directly.
|
||||
RUST_BIN_PKG="packages/engines/rust-binaries/package.json"
|
||||
if [ -f "$RUST_BIN_PKG" ]; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('$RUST_BIN_PKG', 'utf8'));
|
||||
pkg.scripts = pkg.scripts || {};
|
||||
pkg.scripts.build = 'echo [docker] skipped rust-binaries build';
|
||||
fs.writeFileSync('$RUST_BIN_PKG', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
echo "[docker-prebuild] Stubbed rust-binaries build script"
|
||||
fi
|
||||
|
||||
# _playground: runs a full docmd site build — a dev sandbox, not part of the
|
||||
# production image. Skip it to avoid unnecessary work and potential failures.
|
||||
PLAYGROUND_PKG="packages/_playground/package.json"
|
||||
if [ -f "$PLAYGROUND_PKG" ]; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('$PLAYGROUND_PKG', 'utf8'));
|
||||
pkg.scripts = pkg.scripts || {};
|
||||
pkg.scripts.build = 'echo [docker] skipped playground build';
|
||||
fs.writeFileSync('$PLAYGROUND_PKG', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
echo "[docker-prebuild] Stubbed _playground build script"
|
||||
fi
|
||||
+526
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the zero-config documentation generator.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*
|
||||
* Release preparation pipeline. Runs the full dev suite in five
|
||||
* clear category sections:
|
||||
*
|
||||
* 1. Setup - stop running servers, wipe global binaries,
|
||||
* clean the monorepo
|
||||
* 2. Lint - eslint over the entire repo, summarised
|
||||
* 3. Build - pnpm install + pnpm -r run build, so the
|
||||
* test sections have dist files to run
|
||||
* against (clean wipes dist, this rebuilds it)
|
||||
* 4. Docker - optional Docker availability check
|
||||
* 5. Tests - categorised test suite (tests/runner.js,
|
||||
* 365+ assertions covering Phase 1 security
|
||||
* CVEs + Phase 2 container parser + Phase 3
|
||||
* CLI contracts + OKF/LLMS plugin tests +
|
||||
* Mega Integration Test for workspaces,
|
||||
* i18n, versioning, and plugin combinations)
|
||||
* followed by per-package unit tests
|
||||
* (parser, utils, mermaid, okf) via
|
||||
* `pnpm -r run test --if-present` so a local
|
||||
* regression fails the release pipeline
|
||||
* 6. Link - optional global npm link
|
||||
*
|
||||
* Each step inside a section shows [WAIT] (dim) when it starts
|
||||
* and [DONE] (green) when it finishes. No emojis.
|
||||
*
|
||||
* Default output is intentionally minimal: every step collapses to
|
||||
* one line, and every count (passed tests, lint errors, package
|
||||
* totals, Docker version, ...) accumulates into a single trailing
|
||||
* Summary block at the end of the pipeline. A fully-green run prints
|
||||
* a green `┌─ Summary` listing each section with its stat; a run
|
||||
* with any failure replaces that block with a red `┌─ Issues` listing
|
||||
* every failure with file:line detail.
|
||||
*
|
||||
* Pass --verbose (or --full) to stream the full test output as the
|
||||
* suite runs — useful when actively iterating on a test that just
|
||||
* started failing and you want to see the assertion text inline.
|
||||
*
|
||||
* Run: pnpm prep
|
||||
* pnpm prep --link (skip tests, install globally)
|
||||
* pnpm prep --skip-tests (skip both test suites)
|
||||
* pnpm prep --only=exit-codes (run a single test section)
|
||||
* pnpm prep --verbose (stream full test output inline)
|
||||
* pnpm prep --full (alias for --verbose)
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// --verbose / --full streams every step's raw output as it runs.
|
||||
// Default mode keeps the pipeline minimal: each step collapses to
|
||||
// one line and a final Issues section appears only if something failed.
|
||||
const verbose = args.includes('--verbose') || args.includes('--full');
|
||||
|
||||
// Issue accumulator — populated by every section so a single trailing
|
||||
// "Issues" block can show the operator everything that needs fixing.
|
||||
const issues = [];
|
||||
function addIssue(severity, section, message, details = []) {
|
||||
issues.push({ severity, section, message, details });
|
||||
}
|
||||
|
||||
// Stat accumulator — every section contributes a one-line entry that
|
||||
// prints in the trailing Summary block. Lets the per-step line stay
|
||||
// minimal ("[ DONE ] 28s") while still surfacing the actual numbers
|
||||
// (passed counts, lint counts, etc.) at the end.
|
||||
const stats = [];
|
||||
function addStat(label, value, severity = 'ok') {
|
||||
stats.push({ label, value, severity });
|
||||
}
|
||||
|
||||
// ── TUI design tokens (no emojis, only [WAIT] / [DONE] / [ FAIL ]) ──
|
||||
const C = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m'
|
||||
};
|
||||
|
||||
// ── TUI primitives ────────────────────────────────────────────────────
|
||||
// section() and footer() pair up to produce exactly one blank line
|
||||
// between groups: footer ends with \n, section starts without \n.
|
||||
function section(label, color) {
|
||||
console.log(`${color}${C.bold}┌─ ${label}${C.reset}`);
|
||||
}
|
||||
|
||||
function footer(color) {
|
||||
console.log(`${color}└${'─'.repeat(50)}${C.reset}`);
|
||||
}
|
||||
|
||||
function startStep(label) {
|
||||
// Print the [WAIT] sign and return a handle to update on completion.
|
||||
const bar = `${C.cyan}│${C.reset}`;
|
||||
const text = `${C.dim}${label}${C.reset}`;
|
||||
process.stdout.write(`${bar} ${text.padEnd(52)}${C.dim}[ WAIT ]${C.reset}\n`);
|
||||
return { label, startMs: Date.now(), bar, text };
|
||||
}
|
||||
|
||||
function finishStep(s, status, summary) {
|
||||
// Rewrite the same line with [DONE] / [ WARN ] / [ FAIL ] + elapsed time.
|
||||
// Default status is 'done' — the common case.
|
||||
const effectiveStatus = status || 'done';
|
||||
const ms = Date.now() - s.startMs;
|
||||
const t = ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
||||
const tag = effectiveStatus === 'done'
|
||||
? `${C.green}[ DONE ]${C.reset}`
|
||||
: effectiveStatus === 'warn'
|
||||
? `${C.yellow}[ WARN ]${C.reset}`
|
||||
: `${C.red}[ FAIL ]${C.reset}`;
|
||||
const sumTxt = summary ? ` ${C.dim}${summary}${C.reset}` : '';
|
||||
// Move cursor up 1 line, clear it, rewrite.
|
||||
process.stdout.write('\x1b[1A\x1b[2K');
|
||||
process.stdout.write(
|
||||
`${s.bar} ${s.text.padEnd(52)}${tag} ${C.dim}${t}${C.reset}${sumTxt}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function run(cmd, opts = {}) {
|
||||
// opts.silent — when true (default), suppress output unless cmd failed.
|
||||
// opts.capture — return stdout/stderr as strings; never stream to the TUI.
|
||||
// Used by collapsed-mode test steps so we can parse a
|
||||
// summary line instead of dumping hundreds of test logs.
|
||||
// The function never calls process.exit; callers inspect `result.ok`.
|
||||
const silent = opts.silent !== false;
|
||||
const capture = opts.capture === true;
|
||||
try {
|
||||
const out = execSync(cmd, {
|
||||
stdio: capture
|
||||
? ['ignore', 'pipe', 'pipe']
|
||||
: (silent ? 'ignore' : 'inherit'),
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
stdout: capture ? out.toString() : '',
|
||||
stderr: '',
|
||||
status: 0,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
ok: false,
|
||||
stdout: capture ? (e.stdout ? e.stdout.toString() : '') : '',
|
||||
stderr: capture ? (e.stderr ? e.stderr.toString() : '') : '',
|
||||
status: e.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step helpers ──────────────────────────────────────────────────────
|
||||
function deepWipe() {
|
||||
const bins = ['docmd', 'docmd-live'];
|
||||
for (const bin of bins) {
|
||||
try {
|
||||
const paths = execSync(`which -a ${bin}`, { stdio: 'pipe' })
|
||||
.toString().split('\n').filter(Boolean);
|
||||
for (const p of paths) {
|
||||
try { if (fs.existsSync(p)) fs.unlinkSync(p); }
|
||||
catch {
|
||||
try { execSync(`rm -f "${p}"`, { stdio: 'ignore' }); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
} catch { /* ignore which failure */ }
|
||||
}
|
||||
}
|
||||
|
||||
function runLint() {
|
||||
const s = startStep('Running eslint over monorepo');
|
||||
// eslint exits non-zero on errors — JSON is still on stdout. run()
|
||||
// never exits the process, so we can collect the lint output cleanly.
|
||||
const result = run('pnpm -s exec eslint . --format json', { capture: true });
|
||||
|
||||
let errors = 0, warnings = 0;
|
||||
const details = [];
|
||||
try {
|
||||
const results = JSON.parse(result.stdout || '[]');
|
||||
for (const file of results) {
|
||||
for (const msg of file.messages || []) {
|
||||
if (msg.severity === 2) errors++;
|
||||
else if (msg.severity === 1) warnings++;
|
||||
if (msg.severity >= 1) {
|
||||
const relPath = (file.filePath || '')
|
||||
.replace(process.cwd() + '/', '')
|
||||
.replace(/^.*\/docmd\//, '');
|
||||
details.push(`${relPath}:${msg.line} — ${msg.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) { /* malformed output */ }
|
||||
|
||||
const status = errors > 0 ? 'fail' : (warnings > 0 ? 'warn' : 'done');
|
||||
finishStep(s, status);
|
||||
|
||||
const summary = `${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`;
|
||||
addStat('Lint', summary, errors > 0 ? 'fail' : (warnings > 0 ? 'warn' : 'ok'));
|
||||
|
||||
if (errors > 0) addIssue('error', 'Lint', `${errors} lint error(s)`, details);
|
||||
else if (warnings > 0) addIssue('warning', 'Lint', `${warnings} lint warning(s)`, details);
|
||||
}
|
||||
|
||||
// ── Collapsed-mode test runner ────────────────────────────────────────
|
||||
// Captures test output, parses pass/fail counts and any failure
|
||||
// titles, then renders a one-line summary. Verbose mode bypasses
|
||||
// the capture and streams raw output as it arrives instead.
|
||||
function summariseTests(stdout) {
|
||||
// tests/runner.js emits a single aggregate line at the end.
|
||||
// Pattern: "Test summary: 367 passed, 0 failed across 9 files"
|
||||
let m = stdout.match(/Test summary:\s+(\d+)\s+passed,\s+(\d+)\s+failed\s+across\s+(\d+)\s+files?/i);
|
||||
if (m) {
|
||||
return {
|
||||
tests: parseInt(m[1]) + parseInt(m[2]),
|
||||
pass: parseInt(m[1]),
|
||||
fail: parseInt(m[2]),
|
||||
units: parseInt(m[3]),
|
||||
unitLabel: 'files',
|
||||
failures: extractFailures(stdout),
|
||||
};
|
||||
}
|
||||
|
||||
// Per-package output (pnpm -r run test): each suite prints its own
|
||||
// ℹ tests N / ℹ pass N / ℹ fail N block, so we sum them all up.
|
||||
let tests = 0, pass = 0, fail = 0, pkgs = 0;
|
||||
for (const t of stdout.matchAll(/ℹ\s+tests\s+(\d+)/g)) tests += parseInt(t[1]);
|
||||
for (const p of stdout.matchAll(/ℹ\s+pass\s+(\d+)/g)) pass += parseInt(p[1]);
|
||||
for (const f of stdout.matchAll(/ℹ\s+fail\s+(\d+)/g)) fail += parseInt(f[1]);
|
||||
// Count distinct packages that reported a result. Look for the
|
||||
// pnpm-recursive "Done" line which marks each package's end.
|
||||
for (const _ of stdout.matchAll(/\bDone$/gm)) pkgs++;
|
||||
// Fallback when no "Done" markers are present: count `ℹ tests` lines.
|
||||
if (pkgs === 0) {
|
||||
for (const _ of stdout.matchAll(/ℹ\s+tests\s+\d+/g)) pkgs++;
|
||||
}
|
||||
return {
|
||||
tests, pass, fail,
|
||||
units: pkgs,
|
||||
unitLabel: pkgs === 1 ? 'package' : 'packages',
|
||||
failures: extractFailures(stdout),
|
||||
};
|
||||
}
|
||||
|
||||
function extractFailures(stdout) {
|
||||
// node:test prints "not ok N - <name>" lines on failure. Trim
|
||||
// surrounding ANSI noise and keep the title only — full diagnostics
|
||||
// remain in --verbose output.
|
||||
const out = [];
|
||||
// Build the ANSI-stripping regex at runtime so the static linter
|
||||
// doesn't flag the embedded ESC (\x1b) control character.
|
||||
const ANSI_RE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, 'g');
|
||||
for (const m of stdout.matchAll(/not ok \d+ - (.+)/g)) {
|
||||
const t = m[1].replace(ANSI_RE, '').trim();
|
||||
if (t) out.push(t);
|
||||
if (out.length >= 20) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function runTestStep(label, cmd, statLabel = label) {
|
||||
const s = startStep(label);
|
||||
|
||||
// Verbose mode: stream the raw output as before. The finish line
|
||||
// rewrites with [DONE] / [FAIL] depending on the exit code.
|
||||
if (verbose) {
|
||||
const result = run(cmd, { silent: false });
|
||||
if (result.ok) finishStep(s);
|
||||
else {
|
||||
finishStep(s, 'fail');
|
||||
addIssue('error', label, 'test step failed', []);
|
||||
}
|
||||
addStat(statLabel, result.ok ? 'passed (see --verbose output for details)' : 'failed', result.ok ? 'ok' : 'fail');
|
||||
return;
|
||||
}
|
||||
|
||||
// Default (collapsed) mode: capture output, summarise, render one line.
|
||||
const result = run(cmd, { capture: true });
|
||||
const sum = summariseTests(result.stdout + result.stderr);
|
||||
|
||||
if (result.ok && sum.fail === 0 && sum.tests > 0) {
|
||||
finishStep(s, 'done');
|
||||
addStat(statLabel, `${sum.pass} passed across ${sum.units} ${sum.unitLabel}`, 'ok');
|
||||
} else if (result.ok && sum.tests === 0) {
|
||||
// No tests ran at all (e.g. --only matched nothing). Be honest
|
||||
// about that rather than printing a misleading "0 passed".
|
||||
finishStep(s, 'warn');
|
||||
addStat(statLabel, 'no tests ran', 'warn');
|
||||
} else {
|
||||
const statValue = sum.fail > 0
|
||||
? `${sum.fail} of ${sum.tests} failed across ${sum.units} ${sum.unitLabel}`
|
||||
: `command exited with status ${result.status}`;
|
||||
finishStep(s, 'fail');
|
||||
addStat(statLabel, statValue, 'fail');
|
||||
addIssue('error', label,
|
||||
sum.fail > 0 ? `${sum.fail} test failure(s)` : 'test command failed',
|
||||
sum.failures);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Final Summary / Issues section ───────────────────────────────────
|
||||
// Single trailing block. When nothing failed, it renders as a green
|
||||
// Summary listing every stat (one line per section). When issues were
|
||||
// collected, it flips to a red Issues block with the same group /
|
||||
// bullet layout used elsewhere in the TUI. Either way it lives in the
|
||||
// same slot at the end of the pipeline, so the operator always knows
|
||||
// where to look for the verdict.
|
||||
function printSummary() {
|
||||
if (issues.length === 0) {
|
||||
// Green Summary — every stat in one tidy list. Pad the label
|
||||
// column to the longest label so values align regardless of
|
||||
// how many sections contributed.
|
||||
section('Summary', C.green);
|
||||
const pad = Math.max(...stats.map(s => s.label.length)) + 2;
|
||||
for (const s of stats) {
|
||||
const tag = s.severity === 'warn'
|
||||
? `${C.yellow}[ WARN ]${C.reset}`
|
||||
: `${C.green}[ DONE ]${C.reset}`;
|
||||
const label = `${s.label}`.padEnd(pad);
|
||||
console.log(`${C.green}│${C.reset} ${tag} ${C.bold}${label}${C.reset}${s.value}`);
|
||||
}
|
||||
footer(C.green);
|
||||
return;
|
||||
}
|
||||
|
||||
// Failure case: render the Issues block.
|
||||
const errors = issues.filter(i => i.severity === 'error').length;
|
||||
const warnings = issues.filter(i => i.severity === 'warning').length;
|
||||
const color = errors > 0 ? C.red : C.yellow;
|
||||
const head = `Issues — ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`;
|
||||
section(head, color);
|
||||
|
||||
// Group by section so two issues from the same source don't repeat
|
||||
// the section header; the operator reads the section once and then
|
||||
// scans the bullets.
|
||||
const bySection = new Map();
|
||||
for (const i of issues) {
|
||||
if (!bySection.has(i.section)) bySection.set(i.section, []);
|
||||
bySection.get(i.section).push(i);
|
||||
}
|
||||
for (const [name, items] of bySection) {
|
||||
const tag = items.some(i => i.severity === 'error')
|
||||
? `${C.red}[ FAIL ]${C.reset}`
|
||||
: `${C.yellow}[ WARN ]${C.reset}`;
|
||||
console.log(`${color}│${C.reset} ${tag} ${C.bold}${name}${C.reset}`);
|
||||
for (const item of items) {
|
||||
console.log(`${color}│${C.reset} ${item.message}`);
|
||||
const detailCap = 8;
|
||||
for (const detail of item.details.slice(0, detailCap)) {
|
||||
console.log(`${color}│${C.reset} ${C.dim}${detail}${C.reset}`);
|
||||
}
|
||||
if (item.details.length > detailCap) {
|
||||
console.log(`${color}│${C.reset} ${C.dim}… ${item.details.length - detailCap} more (re-run with --verbose for full output)${C.reset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
footer(color);
|
||||
}
|
||||
|
||||
function runDockerCheck() {
|
||||
const s = startStep('Checking Docker availability');
|
||||
let hasDocker = '';
|
||||
try {
|
||||
hasDocker = execSync('which docker 2>/dev/null', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
}).trim();
|
||||
} catch { /* not installed */ }
|
||||
|
||||
if (!hasDocker && !process.env.DOCKER_HOST) {
|
||||
finishStep(s, 'warn');
|
||||
addStat('Docker', 'not installed — skipped', 'warn');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const version = execSync('docker --version', { encoding: 'utf8', stdio: ['pipe', 'pipe'] }).trim();
|
||||
finishStep(s, 'done');
|
||||
addStat('Docker', version.replace('Docker version ', 'Docker '), 'ok');
|
||||
} catch {
|
||||
finishStep(s, 'warn');
|
||||
addStat('Docker', 'binary not functional', 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main pipeline ────────────────────────────────────────────────────
|
||||
|
||||
// Header — banner + "Maintenance Pipeline" subtitle. Inlined here
|
||||
// (rather than calling `node tools/status.js start:reset`) because
|
||||
// that helper opens a section frame which we don't want prep.js
|
||||
// to inherit. prep.js owns its own section boundaries.
|
||||
const LOGO = '\n'
|
||||
+ ' _ _ \n'
|
||||
+ ' _| |___ ___ _____ _| |\n'
|
||||
+ ' | . | . | _| | . |\n'
|
||||
+ ' |___|___|___|_|_|_|___|\n';
|
||||
console.log(`${C.blue}${LOGO}${C.reset}`);
|
||||
console.log(`${C.dim} Monorepo Maintenance Pipeline ${C.reset}\n`);
|
||||
|
||||
// Section 1: Setup
|
||||
section('Setup', C.blue);
|
||||
{
|
||||
const s = startStep('Stopping active servers');
|
||||
const r = run('pnpm -s stop');
|
||||
if (r.ok) finishStep(s);
|
||||
else { finishStep(s, 'fail', `exit ${r.status}`); addIssue('error', 'Setup', `pnpm stop failed (exit ${r.status})`, []); }
|
||||
}
|
||||
{
|
||||
const s = startStep('Wiping global docmd binaries');
|
||||
const pkgs = ['@docmd/core', '@docmd/monorepo', 'docmd', 'docmd-live'];
|
||||
for (const pkg of pkgs) {
|
||||
try { execSync('npm uninstall -g ' + pkg + ' -s', { stdio: 'ignore' }); } catch { /* ignore */ }
|
||||
try { execSync('pnpm uninstall -g ' + pkg + ' -s', { stdio: 'ignore' }); } catch { /* ignore */ }
|
||||
}
|
||||
deepWipe();
|
||||
finishStep(s);
|
||||
}
|
||||
{
|
||||
const s = startStep('Cleaning monorepo');
|
||||
const r = run('pnpm -s clean');
|
||||
if (r.ok) finishStep(s);
|
||||
else { finishStep(s, 'fail', `exit ${r.status}`); addIssue('error', 'Setup', `pnpm clean failed (exit ${r.status})`, []); }
|
||||
}
|
||||
{
|
||||
// pnpm clean wipes node_modules; reinstall before lint so the eslint
|
||||
// binary is on PATH. Without this step the lint section silently exits
|
||||
// 0 with no output because `pnpm exec eslint` can't find the binary.
|
||||
const s = startStep('Installing monorepo dependencies');
|
||||
const r = run('pnpm install --frozen-lockfile');
|
||||
if (r.ok) finishStep(s);
|
||||
else { finishStep(s, 'fail', `exit ${r.status}`); addIssue('error', 'Setup', `pnpm install failed (exit ${r.status})`, []); }
|
||||
}
|
||||
addStat('Setup', 'cleaned + installed dependencies', 'ok');
|
||||
footer(C.blue);
|
||||
|
||||
// Section 2: Lint — runs after Setup reinstalls deps so eslint is available.
|
||||
section('Lint', C.cyan);
|
||||
runLint();
|
||||
footer(C.cyan);
|
||||
|
||||
// Section 3: Build — required so tests have dist files to run against.
|
||||
// `pnpm clean` (called in Setup) wipes every package's dist directory;
|
||||
// deps are already restored by Setup's install step.
|
||||
section('Build', C.cyan);
|
||||
{
|
||||
const s = startStep('Building all packages (pnpm -r run build)');
|
||||
const r = run('pnpm -r run build');
|
||||
if (r.ok) finishStep(s);
|
||||
else { finishStep(s, 'fail', `exit ${r.status}`); addIssue('error', 'Build', `pnpm -r run build failed (exit ${r.status})`, []); }
|
||||
}
|
||||
addStat('Build', 'built all packages', 'ok');
|
||||
footer(C.cyan);
|
||||
|
||||
// Section 4: Docker (optional)
|
||||
section('Docker', C.blue);
|
||||
runDockerCheck();
|
||||
footer(C.blue);
|
||||
|
||||
// Section 5: Tests
|
||||
section('Tests', C.blue);
|
||||
if (args.includes('--skip-tests')) {
|
||||
const s = startStep('Skipping test suite (--skip-tests)');
|
||||
finishStep(s, 'done');
|
||||
addStat('Tests', 'skipped by user request', 'ok');
|
||||
} else {
|
||||
const only = args.find((a) => a.startsWith('--only='));
|
||||
const runnerArgs = only ? ' ' + only : '';
|
||||
// The categorised runner now includes the Mega Integration Test
|
||||
// (workspaces + i18n + versioning + plugins), which used to live
|
||||
// in `tests/failsafe.test.mjs`. The old failsafe was removed because
|
||||
// it duplicated Setup / Build work that `pnpm prep` already does.
|
||||
// runTestStep() collapses to a one-line summary by default; pass
|
||||
// --verbose to stream the full output.
|
||||
// The shortLabel is what shows in the Summary block; the longer
|
||||
// stepLabel stays in the per-step line.
|
||||
runTestStep('Categorised test suite (tests/runner.js)',
|
||||
'node tests/runner.js' + runnerArgs, 'Tests · runner.js');
|
||||
|
||||
// Per-package unit tests (parser, utils, mermaid, okf). Packages
|
||||
// without a `test` script are skipped by `--if-present`. Wired in
|
||||
// here so a regression in any plugin's local suite fails the
|
||||
// release pipeline just like a regression in tests/runner.js.
|
||||
runTestStep('Per-package unit tests (pnpm -r run test)',
|
||||
'pnpm -r run test --if-present', 'Tests · per-package units');
|
||||
}
|
||||
footer(C.blue);
|
||||
|
||||
// Section 6: Link (optional)
|
||||
if (args.includes('--link')) {
|
||||
section('Link', C.blue);
|
||||
const s = startStep('Linking @docmd/core globally');
|
||||
try {
|
||||
execSync('npm link --silent', {
|
||||
cwd: require('path').join(process.cwd(), 'packages/core'),
|
||||
stdio: 'ignore'
|
||||
});
|
||||
finishStep(s, 'done', 'docmd command available globally');
|
||||
} catch {
|
||||
finishStep(s, 'fail', 'npm link failed');
|
||||
addIssue('error', 'Link', 'npm link failed', []);
|
||||
}
|
||||
footer(C.blue);
|
||||
}
|
||||
|
||||
// Section 7: Summary / Issues — single trailing block. Renders a
|
||||
// green Summary on a clean run, or a red Issues block on failure.
|
||||
// Either way it lives in the same slot at the end of the pipeline
|
||||
// so the operator always knows where to look for the verdict.
|
||||
printSummary();
|
||||
|
||||
// Exit code: any error-level issue is fatal. Warnings alone keep the
|
||||
// pipeline green so the operator can decide whether to act on them.
|
||||
const hasErrors = issues.some(i => i.severity === 'error');
|
||||
if (hasErrors) process.exit(1);
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the zero-config documentation engine.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*
|
||||
* Resolves the external (non-@docmd) runtime dependencies of @docmd/core
|
||||
* and its transitive deps, then either:
|
||||
*
|
||||
* node tools/print-okf-runtime-deps.js
|
||||
* → prints `name@range` lines to stdout (human readable / CI log)
|
||||
*
|
||||
* node tools/print-okf-runtime-deps.js --emit-pkg=<path>
|
||||
* → writes a package.json to <path> listing all external deps under
|
||||
* `dependencies`. Used by the Docker builder stage so the
|
||||
* production stage can do a single `npm install --prefix /usr/local`
|
||||
* instead of maintaining a hand-rolled list in the Dockerfile.
|
||||
*
|
||||
* The script is the single source of truth for what external deps the
|
||||
* Docker image needs. Run it after adding a new dep to any @docmd/*
|
||||
* package — the next Docker build will pick up the change automatically
|
||||
* because the builder stage regenerates the package.json fresh.
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = process.cwd();
|
||||
const emitArg = process.argv.find((a) => a.startsWith("--emit-pkg="));
|
||||
const emitPath = emitArg ? emitArg.slice("--emit-pkg=".length) : null;
|
||||
|
||||
function findPackageDir(name, dir = path.join(root, "packages")) {
|
||||
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
const pj = path.join(full, "package.json");
|
||||
if (fs.existsSync(pj)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(pj, "utf8"));
|
||||
if (pkg.name === name) return full;
|
||||
}
|
||||
const r = findPackageDir(name, full);
|
||||
if (r) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectDeps(name, seen = new Set()) {
|
||||
if (seen.has(name)) return seen;
|
||||
seen.add(name);
|
||||
const dir = findPackageDir(name);
|
||||
if (!dir) return seen;
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
||||
for (const dep of Object.keys(pkg.dependencies || {})) {
|
||||
if (dep.startsWith("@docmd/")) collectDeps(dep, seen);
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
const internal = collectDeps("@docmd/core");
|
||||
const external = new Map();
|
||||
|
||||
for (const name of internal) {
|
||||
const dir = findPackageDir(name);
|
||||
if (!dir) continue;
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
||||
for (const [dep, range] of Object.entries(pkg.dependencies || {})) {
|
||||
if (dep.startsWith("@docmd/")) continue;
|
||||
if (dep.startsWith("@mgks/")) continue;
|
||||
if (!external.has(dep)) external.set(dep, range);
|
||||
}
|
||||
}
|
||||
|
||||
const lines = [...external].sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
if (emitPath) {
|
||||
// Write a package.json the production stage can `npm install` directly.
|
||||
// `name` and `version` are required by npm but otherwise arbitrary.
|
||||
const pkg = {
|
||||
name: "docmd-runtime-deps",
|
||||
version: "0.0.0",
|
||||
private: true,
|
||||
description: "Auto-generated by tools/print-okf-runtime-deps.js — do not edit.",
|
||||
dependencies: Object.fromEntries(lines)
|
||||
};
|
||||
fs.mkdirSync(path.dirname(emitPath), { recursive: true });
|
||||
fs.writeFileSync(emitPath, JSON.stringify(pkg, null, 2) + "\n");
|
||||
console.log(`[generate-runtime-pkg] wrote ${lines.length} deps → ${emitPath}`);
|
||||
} else {
|
||||
for (const [dep, range] of lines) {
|
||||
console.log(`${dep}@${range}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the minimalist, zero-config documentation generator.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = process.cwd();
|
||||
const packagesDir = path.join(root, "packages");
|
||||
|
||||
// Build name → version map
|
||||
const versionMap = {};
|
||||
|
||||
function collectVersions(dir) {
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, entry);
|
||||
if (fs.existsSync(path.join(full, "package.json"))) {
|
||||
const pkg = require(path.join(full, "package.json"));
|
||||
versionMap[pkg.name] = pkg.version;
|
||||
} else if (fs.statSync(full).isDirectory()) {
|
||||
collectVersions(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectVersions(packagesDir);
|
||||
|
||||
// Rewrite workspace deps
|
||||
function rewriteDeps(dir) {
|
||||
for (const entry of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, entry);
|
||||
|
||||
if (fs.existsSync(path.join(full, "package.json"))) {
|
||||
const pkgPath = path.join(full, "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
|
||||
["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].forEach(field => {
|
||||
if (!pkg[field]) return;
|
||||
|
||||
for (const dep in pkg[field]) {
|
||||
if (pkg[field][dep].startsWith("workspace:")) {
|
||||
pkg[field][dep] = `^${versionMap[dep]}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
||||
} else if (fs.statSync(full).isDirectory()) {
|
||||
rewriteDeps(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rewriteDeps(packagesDir);
|
||||
|
||||
console.log("Workspace dependencies resolved.");
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : Monorepo Security Audit
|
||||
*
|
||||
* Scans the codebase for high-risk patterns that could lead to
|
||||
* XSS, RCE, or directory traversal vulnerabilities.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import nativeFs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CWD = path.resolve(__dirname, '..');
|
||||
|
||||
const AUDIT_CONFIG = {
|
||||
patterns: [
|
||||
{
|
||||
name: 'Potential DOM XSS (innerHTML)',
|
||||
regex: /\.innerHTML\s*=\s*/g,
|
||||
severity: 'HIGH',
|
||||
exclude: []
|
||||
},
|
||||
{
|
||||
name: 'Raw EJS Output (<%-)',
|
||||
regex: /<%-/g,
|
||||
severity: 'MEDIUM',
|
||||
include: [/\.ejs$/],
|
||||
exclude: [
|
||||
/layout\.ejs/, // Main layout (legit for page content)
|
||||
/menubar\.ejs/,
|
||||
/sidebar\.ejs/,
|
||||
/packages\/plugins\/threads\/src\/plugin\/templates\// // Threads use raw for markdown
|
||||
],
|
||||
notes: 'Ensure the output is either trusted Markdown or manually escaped.'
|
||||
},
|
||||
{
|
||||
name: 'Unsafe Command Execution (eval)',
|
||||
regex: /\beval\s*\(/g,
|
||||
severity: 'CRITICAL'
|
||||
},
|
||||
{
|
||||
name: 'Unsafe Function Construction',
|
||||
regex: /new\s+Function\s*\(/g,
|
||||
severity: 'CRITICAL'
|
||||
},
|
||||
{
|
||||
name: 'Unsafe Shell Execution',
|
||||
regex: /(?<!\.(?:[a-zA-Z0-9_$]+))\bexec\s*\(/g,
|
||||
severity: 'HIGH',
|
||||
exclude: [
|
||||
/scripts\//, // Build scripts are allowed (legacy path)
|
||||
/tools\//, // Build tooling is allowed (current path)
|
||||
/packages\/core\/src\/engine\/workspace\.ts/ // Legit for git metadata
|
||||
],
|
||||
notes: 'Check if this is child_process.exec. Avoid RegExp.exec false positives.'
|
||||
},
|
||||
{
|
||||
name: 'Path Traversal Risk (fs access with dynamic path)',
|
||||
regex: /fs\.(?:readFileSync|writeFile|mkdirSync|copyFileSync)\([^,)]*[\+\$]/g,
|
||||
severity: 'MEDIUM',
|
||||
exclude: [
|
||||
/scripts\//,
|
||||
/tools\//,
|
||||
/packages\/core\/src\/engine\//, // Core engine handles paths
|
||||
/packages\/plugins\/search\/src\/index\.ts/
|
||||
]
|
||||
}
|
||||
],
|
||||
// Custom filter to skip RegExp.exec specifically if the lookbehind isn't enough.
|
||||
// The `Unsafe Shell Execution` regex above uses a variable-length negative
|
||||
// lookbehind that only suppresses method calls on identifiers of 2+ chars
|
||||
// (e.g. `child_process.exec` — the chars before `exec(` are `s.`, which
|
||||
// matches the lookbehind pattern). Short identifiers like `re` slip
|
||||
// through because the chars before `exec(` are `e.` (just 2 chars) and
|
||||
// the lookbehind's `\.[a-zA-Z0-9_$]+` requires at least 1 identifier
|
||||
// char AFTER the dot, so it doesn't match `.` alone.
|
||||
//
|
||||
// To compensate, this filter suppresses the match when the line's
|
||||
// `.exec(` call is on a recognised RegExp variable name. The list
|
||||
// covers every common JS naming convention for a RegExp instance.
|
||||
// Real or potentially-real shell calls (any other variable name,
|
||||
// `child_process.exec`, `cp.exec`, standalone `exec(cmd)`, etc.)
|
||||
// are NOT suppressed — they remain flagged for operator review.
|
||||
filter: (pattern, line) => {
|
||||
if (pattern.name === 'Unsafe Shell Execution' && line.includes('.exec(')) {
|
||||
const before = line.split('.exec(')[0];
|
||||
if (before && /(?:re|regex|regexp|rgx|match|matcher|pattern|pat)(?![a-zA-Z_$])\s*(?:\.|$)/i.test(before)) return false;
|
||||
}
|
||||
|
||||
if (pattern.name === 'Raw EJS Output (<%-)') {
|
||||
const safePatterns = [
|
||||
'renderIcon', 'include', 't(', 'content', 'navigationHtml',
|
||||
'footerHtml', 'pluginHeadScriptsHtml', 'pluginBodyScriptsHtml',
|
||||
'pluginStylesHtml', 'themeInitScript', 'faviconLinkHtml',
|
||||
'metaTagsHtml', 'themeCssLinkHtml', 'frontmatter.customHead',
|
||||
'frontmatter.customScripts', "'<script>'", "'</script>'",
|
||||
'JSON.stringify', 'relativePathToRoot'
|
||||
];
|
||||
if (safePatterns.some(p => line.includes(p))) return false;
|
||||
}
|
||||
|
||||
if (pattern.name === 'Potential DOM XSS (innerHTML)') {
|
||||
if (line.includes("innerHTML = ''") || line.includes('innerHTML = ""') || line.includes('innerHTML = ``')) return false;
|
||||
if (line.includes('this.sanitize(') || line.includes('innerHTML = sanitized') || line.includes("'×'")) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
extensions: ['.ts', '.js', '.ejs', '.json'],
|
||||
// Build output directories are excluded because their content is generated
|
||||
// and minified — the audit is meant to catch issues in source code.
|
||||
excludeDirs: ['node_modules', 'dist', 'site', 'site-sky', 'out', '.git', 'temp', 'public', 'vendor', '_backup']
|
||||
};
|
||||
|
||||
let issuesCount = 0;
|
||||
|
||||
function scanDir(dir) {
|
||||
const entries = nativeFs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relPath = path.relative(CWD, fullPath);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (AUDIT_CONFIG.excludeDirs.includes(entry.name)) continue;
|
||||
scanDir(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name);
|
||||
if (!AUDIT_CONFIG.extensions.includes(ext)) continue;
|
||||
|
||||
const content = nativeFs.readFileSync(fullPath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const pattern of AUDIT_CONFIG.patterns) {
|
||||
// Check global include/exclude for the file
|
||||
if (pattern.include && !pattern.include.some(re => re.test(relPath))) continue;
|
||||
if (pattern.exclude && pattern.exclude.some(re => re.test(relPath))) continue;
|
||||
|
||||
let match;
|
||||
pattern.regex.lastIndex = 0; // Reset regex
|
||||
while ((match = pattern.regex.exec(content)) !== null) {
|
||||
const index = match.index;
|
||||
const lineNumber = content.substring(0, index).split('\n').length;
|
||||
const line = lines[lineNumber - 1].trim();
|
||||
|
||||
// Custom filters
|
||||
if (AUDIT_CONFIG.filter && !AUDIT_CONFIG.filter(pattern, line)) continue;
|
||||
|
||||
// Inline exclusion comment support
|
||||
if (line.includes('audit-ignore') || line.includes('eslint-disable')) continue;
|
||||
|
||||
issuesCount++;
|
||||
const skipHeader = process.argv.includes('--skip-header');
|
||||
const prefix = skipHeader ? '\x1b[34m│\x1b[0m ' : '\x1b[34m│\x1b[0m ';
|
||||
|
||||
console.log(`${prefix}[\x1b[1m${pattern.severity}\x1b[0m] \x1b[33m${pattern.name}\x1b[0m`);
|
||||
console.log(`${prefix}Location: \x1b[34m${relPath}:${lineNumber}\x1b[0m`);
|
||||
console.log(`${prefix}\x1b[2mCode: ${line.substring(0, 80)}${line.length > 80 ? '...' : ''}\x1b[0m`);
|
||||
if (pattern.notes) console.log(`${prefix}\x1b[36mNote: ${pattern.notes}\x1b[0m`);
|
||||
console.log(`\x1b[34m│\x1b[0m`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const skipHeader = process.argv.includes('--skip-header');
|
||||
|
||||
if (!skipHeader) {
|
||||
console.log(`\x1b[34m┌─ Monorepo Security Audit\x1b[0m`);
|
||||
console.log(`\x1b[34m│\x1b[0m Scanning packages and scripts...`);
|
||||
console.log(`\x1b[34m│\x1b[0m`);
|
||||
}
|
||||
|
||||
try {
|
||||
scanDir(path.join(CWD, 'packages'));
|
||||
scanDir(path.join(CWD, 'tools'));
|
||||
|
||||
if (issuesCount > 0) {
|
||||
if (!skipHeader) {
|
||||
console.log(`\x1b[34m│\x1b[0m \x1b[31mFound ${issuesCount} potential security issues.\x1b[0m`);
|
||||
console.log(`\x1b[34m└──────────────────────────────────────────────────────────\x1b[0m\n`);
|
||||
} else {
|
||||
console.log(`\x1b[34m│\x1b[0m \x1b[31mFound ${issuesCount} potential security issues.\x1b[0m`);
|
||||
}
|
||||
} else {
|
||||
if (!skipHeader) {
|
||||
console.log(`\x1b[34m│\x1b[0m \x1b[32mNo high-risk patterns detected.\x1b[0m`);
|
||||
console.log(`\x1b[34m└──────────────────────────────────────────────────────────\x1b[0m\n`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`\x1b[31m│\x1b[0m Fatal error during security audit: ${e.message}`);
|
||||
if (!skipHeader) {
|
||||
console.log(`\x1b[31m└──────────────────────────────────────────────────────────\x1b[0m\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(issuesCount > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the minimalist, zero-config documentation generator.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const TYPE = args[0];
|
||||
const skipHeader = args.includes('--skip-header');
|
||||
|
||||
// TUI Design Tokens
|
||||
const C = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
bgBlue: '\x1b[44m',
|
||||
black: '\x1b[30m'
|
||||
};
|
||||
|
||||
const LOGO = `
|
||||
_ _
|
||||
_| |___ ___ _____ _| |
|
||||
| . | . | _| | . |
|
||||
|___|___|___|_|_|_|___|
|
||||
`;
|
||||
|
||||
/**
|
||||
* Modern TUI Components
|
||||
*/
|
||||
const TUI = {
|
||||
header: (title) => {
|
||||
console.log(`\n${C.blue}${LOGO}${C.reset}`);
|
||||
console.log(`${C.dim} ${title} ${C.reset}\n`);
|
||||
},
|
||||
|
||||
section: (label, color = C.cyan) => {
|
||||
console.log(`${color}${C.bold}┌─ ${label}${C.reset}`);
|
||||
},
|
||||
|
||||
item: (label, status = 'DONE', color = C.dim, barColor = '\x1b[36m') => {
|
||||
const indicator = status === 'DONE' ? `\x1b[32m[ DONE ]\x1b[0m` : `\x1b[36m[ ${status} ]\x1b[0m`;
|
||||
console.log(`${barColor}│\x1b[0m ${color}${label.padEnd(45)}${C.reset} ${indicator}`);
|
||||
},
|
||||
|
||||
footer: (color = C.cyan) => {
|
||||
console.log(`${color}└──────────────────────────────────────────────────────────${C.reset}\n`);
|
||||
},
|
||||
|
||||
alert: (msg, color = C.green) => {
|
||||
console.log(`${color}${C.bold}⬢ ${msg}${C.reset}\n`);
|
||||
},
|
||||
|
||||
error: (msg, detail) => {
|
||||
console.error(`\n\x1b[31m\x1b[1m┌─ Failure\x1b[0m`);
|
||||
console.error(`\x1b[31m│\x1b[0m ${msg}`);
|
||||
if (detail) {
|
||||
detail.split('\n').forEach(line => {
|
||||
console.error(`\x1b[31m│\x1b[0m \x1b[2m${line}\x1b[0m`);
|
||||
});
|
||||
}
|
||||
console.error(`\x1b[31m└──────────────────────────────────────────────────────────\x1b[0m\n`);
|
||||
}
|
||||
};
|
||||
|
||||
if (TYPE && TYPE.startsWith('start:') && !skipHeader) {
|
||||
TUI.header('Monorepo Maintenance Pipeline');
|
||||
}
|
||||
|
||||
if (TYPE === 'start:reset') {
|
||||
TUI.section('Resetting Docmd Engine', C.cyan);
|
||||
} else if (TYPE === 'reset') {
|
||||
TUI.footer(C.cyan);
|
||||
TUI.alert('Ready for fresh verification.');
|
||||
} else if (TYPE === 'start:verify') {
|
||||
TUI.section('Failsafe Verification', C.blue);
|
||||
} else if (TYPE === 'verify') {
|
||||
//TUI.footer(C.blue);
|
||||
TUI.alert('docmd is production-ready.');
|
||||
} else if (TYPE === 'error') {
|
||||
TUI.error(process.argv[3], process.argv[4]);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* --------------------------------------------------------------------
|
||||
* docmd : the minimalist, zero-config documentation generator.
|
||||
*
|
||||
* @package @docmd/core (and ecosystem)
|
||||
* @website https://docmd.io
|
||||
* @repository https://github.com/docmd-io/docmd
|
||||
* @license MIT
|
||||
* @copyright Copyright (c) 2025-present docmd.io
|
||||
*
|
||||
* [docmd-source] - Please do not remove this header.
|
||||
* --------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const isCI = process.env.CI === 'true' || args.includes('--skip-setup');
|
||||
const shouldLink = args.includes('--link');
|
||||
const skipHeader = args.includes('--skip-header');
|
||||
|
||||
function run(cmd, silent = true) {
|
||||
// Pipe stdout/stderr so a failing child surfaces its real output. With
|
||||
// stdio 'inherit', a CI runner (no TTY) discards the child's output and
|
||||
// execSync's error object carries no captured buffers — leaving us with
|
||||
// an opaque "exit code 1" and no way to see which assertion failed.
|
||||
try {
|
||||
execSync(cmd, {
|
||||
stdio: silent ? 'ignore' : 'pipe',
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.stdout) process.stderr.write(e.stdout);
|
||||
if (e.stderr) process.stderr.write(e.stderr);
|
||||
if (!silent) console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Show starting logo (only if not in CI or if user wants it)
|
||||
if (!isCI) {
|
||||
const headerCmd = skipHeader ? 'node tools/status.js start:verify --skip-header' : 'node tools/status.js start:verify';
|
||||
run(headerCmd, false);
|
||||
}
|
||||
|
||||
// 2. Run the categorised test suite (tests/runner.js replaced the
|
||||
// legacy scripts/failsafe.mjs after the tests/scripts split).
|
||||
// Forwarding arguments so --skip-setup / future flags reach the runner.
|
||||
const failsafeArgs = args.filter(a => a !== '--link' && a !== '--skip-header').join(' ');
|
||||
run(`node tests/runner.js ${failsafeArgs}`, false);
|
||||
|
||||
// 3. Handle global linking if requested
|
||||
if (shouldLink) {
|
||||
process.stdout.write(`\x1b[34m│\x1b[0m \x1b[2mLinking docmd globally...\x1b[0m`);
|
||||
try {
|
||||
execSync('npm link --silent', { cwd: path.join(process.cwd(), 'packages/core'), stdio: 'ignore' });
|
||||
console.log(' \x1b[32m[ DONE ]\x1b[0m');
|
||||
} catch {
|
||||
console.log(' \x1b[31m[ FAIL ]\x1b[0m');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Show final completion status
|
||||
if (!isCI) {
|
||||
const statusCmd = 'node tools/status.js verify';
|
||||
run(statusCmd, false);
|
||||
} else {
|
||||
console.log('\n\x1b[32m\x1b[1m⬢ Docmd verification passed!\x1b[0m\n');
|
||||
}
|
||||
Reference in New Issue
Block a user