Files
2026-07-13 12:35:03 +08:00

236 lines
6.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Export ArcKit ARC artefacts as an Open Knowledge Format-shaped bundle.
*
* This script mutates only the output directory. Source files under projects/
* are copied with OKF frontmatter merged into the exported copy.
*/
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
statSync,
writeFileSync,
} from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
buildOkfFieldsForArcArtifact,
mergeOkfFrontmatter,
} from '../hooks/okf-frontmatter.mjs';
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const DEFAULT_CWD = resolve(SCRIPT_DIR, '..', '..', '..');
function usage() {
return `Usage: export-okf.mjs (--all | --project <project-id>) [--out <path>] [--cwd <repo-root>]`;
}
function parseArgs(argv) {
const args = {
all: false,
project: null,
out: 'okf',
cwd: process.cwd(),
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--all') {
args.all = true;
} else if (arg === '--project') {
args.project = argv[++i] || null;
} else if (arg === '--out') {
args.out = argv[++i] || null;
} else if (arg === '--cwd') {
args.cwd = argv[++i] || null;
} else if (arg === '--help' || arg === '-h') {
args.help = true;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
if (args.help) return args;
if (!args.all && !args.project) throw new Error('provide --all or --project <project-id>');
if (args.all && args.project) throw new Error('choose either --all or --project, not both');
if (!args.out) throw new Error('--out requires a path');
if (!args.cwd) args.cwd = DEFAULT_CWD;
return args;
}
function toPosix(path) {
return path.split(/[\\/]/).join('/');
}
function findProjectDirs(repoRoot, projectArg, all) {
const projectsRoot = join(repoRoot, 'projects');
if (!existsSync(projectsRoot) || !statSync(projectsRoot).isDirectory()) {
throw new Error(`projects directory not found: ${projectsRoot}`);
}
const projects = readdirSync(projectsRoot, { withFileTypes: true })
.filter(entry => entry.isDirectory() && /^\d{3}($|-)/.test(entry.name))
.map(entry => entry.name)
.sort();
if (all) return projects.map(name => join(projectsRoot, name));
const matches = projects.filter(name => name === projectArg || name.startsWith(`${projectArg}-`));
if (matches.length === 0) throw new Error(`project not found: ${projectArg}`);
if (matches.length > 1) throw new Error(`project is ambiguous: ${projectArg} (${matches.join(', ')})`);
return [join(projectsRoot, matches[0])];
}
function walk(dir) {
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
const files = [];
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...walk(path));
} else if (entry.isFile() && /^ARC-\d{3}-.+-v\d+(?:\.\d+)?\.md$/.test(entry.name)) {
files.push(path);
}
}
return files;
}
function writeIndex(outDir, exported, generatedAt) {
const lines = [
'---',
'type: OKF Bundle Index',
'title: ArcKit OKF Export Index',
'description: Index of ArcKit artefacts exported as an OKF bundle.',
`timestamp: ${JSON.stringify(generatedAt)}`,
'tags:',
' - arckit',
' - okf',
'---',
'',
'# ArcKit OKF Export Index',
'',
`Generated: ${generatedAt}`,
'',
'| Artifact | Type | Source |',
'|---|---|---|',
];
for (const item of exported) {
lines.push(`| [${item.documentId}](${item.outputPath}) | ${item.type} | \`${item.sourcePath}\` |`);
}
writeFileSync(join(outDir, 'index.md'), `${lines.join('\n')}\n`, 'utf8');
}
function writeLog(outDir, summary) {
const lines = [
'---',
'type: OKF Export Log',
'title: ArcKit OKF Export Log',
'description: Export log for an ArcKit OKF bundle.',
`timestamp: ${JSON.stringify(summary.generated_at)}`,
'tags:',
' - arckit',
' - okf',
' - export-log',
'---',
'',
'# ArcKit OKF Export Log',
'',
`Generated: ${summary.generated_at}`,
`Repository root: \`${summary.repo_root}\``,
`Output directory: \`${summary.output_dir}\``,
`Artifact count: ${summary.artifact_count}`,
'',
'## Projects',
'',
...summary.projects.map(project => `- \`${project}\``),
'',
];
writeFileSync(join(outDir, 'log.md'), `${lines.join('\n')}\n`, 'utf8');
}
function exportOkf(args) {
const repoRoot = resolve(args.cwd);
const outDir = resolve(repoRoot, args.out);
const projectDirs = findProjectDirs(repoRoot, args.project, args.all);
const generatedAt = new Date().toISOString();
const exported = [];
const warnings = [];
mkdirSync(outDir, { recursive: true });
for (const projectDir of projectDirs) {
const artifactPaths = walk(projectDir);
if (artifactPaths.length === 0) warnings.push(`no ARC markdown artefacts found in ${toPosix(relative(repoRoot, projectDir))}`);
for (const artifactPath of artifactPaths) {
const sourceRel = toPosix(relative(repoRoot, artifactPath));
const content = readFileSync(artifactPath, 'utf8');
const stat = statSync(artifactPath);
const fields = buildOkfFieldsForArcArtifact({
filePath: artifactPath,
content,
resource: sourceRel,
timestamp: stat.mtime.toISOString(),
});
if (!fields) {
warnings.push(`skipped non-ARC file: ${sourceRel}`);
continue;
}
const outputRel = sourceRel;
const outputPath = join(outDir, ...outputRel.split('/'));
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, mergeOkfFrontmatter(content, fields), 'utf8');
exported.push({
documentId: fields.arckit.document_id,
type: fields.type,
sourcePath: sourceRel,
outputPath: toPosix(relative(outDir, outputPath)),
});
}
}
exported.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath));
const summary = {
ok: true,
generated_at: generatedAt,
repo_root: repoRoot,
output_dir: outDir,
artifact_count: exported.length,
projects: projectDirs.map(projectDir => relative(join(repoRoot, 'projects'), projectDir)).sort(),
artifacts: exported,
warnings,
};
writeIndex(outDir, exported, generatedAt);
writeLog(outDir, summary);
return summary;
}
function main() {
try {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(usage());
return;
}
const summary = exportOkf(args);
console.log(JSON.stringify(summary, null, 2));
} catch (error) {
console.error(JSON.stringify({ ok: false, error: error.message }, null, 2));
process.exit(1);
}
}
if (fileURLToPath(import.meta.url) === resolve(process.argv[1] || '')) {
main();
}
export { exportOkf, parseArgs };