#!/usr/bin/env node import fs from 'fs'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; import { VL_VERSION, THEME_VERSION, THEME_VERSION_FULL, STYLESPACE_VERSION, WORKFLOW_SPEC_DOC_VERSION, } from '../src/data/versions.js'; import { DOCCENTER_API_URL } from '../src/data/doc-paths.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const APPLY = process.argv.includes('--apply'); const AUTH_PATH = path.join(os.homedir(), '.vl-code', 'auth.json'); const cookie = process.env.DOCCENTER_COOKIE || (() => { try { return JSON.parse(fs.readFileSync(AUTH_PATH, 'utf-8')).cookie; } catch { return ''; } })(); if (!cookie) { console.error('Missing DocCenter cookie. Set DOCCENTER_COOKIE or login so ~/.vl-code/auth.json exists.'); process.exit(1); } const headers = { 'Content-Type': 'application/json', Cookie: String(cookie).startsWith('ih5bearer=') ? String(cookie) : `ih5bearer=${cookie}`, }; async function dcPost(endpoint, body) { const res = await fetch(`${DOCCENTER_API_URL}/${endpoint}`, { method: 'POST', headers, body: JSON.stringify(body), }); const text = await res.text(); let payload = {}; try { payload = text ? JSON.parse(text) : {}; } catch { throw new Error(`DocCenter returned non-JSON for ${endpoint}: ${text.slice(0, 300)}`); } if (!res.ok) { throw new Error(payload?.error || payload?.message || `DocCenter ${endpoint} failed: ${res.status}`); } return payload; } async function loadCatalog() { const result = await dcPost('SERVICE_DocCenter_GetDocList', { keyword: '', tagId: 0, page: 1, pageSize: 300, }); return Array.isArray(result?.data) ? result.data : []; } async function getDocById(docId) { const result = await dcPost('SERVICE_DocCenter_GetDocById', { docId }); return result?.data || null; } function normalizeSyntaxContent(content) { return String(content || '') .replace(/Current version:\s*`\/\/ VL_VERSION:[\d.]+`/g, `Current version: \`// VL_VERSION:${VL_VERSION}\``) .replace(/\/\/ VL_VERSION:[\d.]+/g, `// VL_VERSION:${VL_VERSION}`) .replace(/THEME_[\d.]+\.vth/g, `THEME_${THEME_VERSION}.vth`) .replace(/Theme-Enterprise-[\d.]+/g, `Theme-Enterprise-${THEME_VERSION}`); } function normalizeThemeContent(content) { return String(content || '') .replace(/VL_VERSION:[\d.]+/g, `VL_VERSION:${VL_VERSION}`) .replace(/Theme-Enterprise-[\d.]+/g, `Theme-Enterprise-${THEME_VERSION}`) .replace(/version:"[\d.]+"/g, `version:"${THEME_VERSION_FULL}"`) .replace(/styleSpaceVersion:"[\d.]+"/g, `styleSpaceVersion:"${STYLESPACE_VERSION}"`); } async function buildManifest(catalog) { const syntaxDoc = catalog.find((doc) => String(doc.path) === '1'); const syntaxRemote = syntaxDoc ? await getDocById(syntaxDoc._id || syntaxDoc.id) : null; return [ { path: '1', name: `VL_${VL_VERSION}.md`, description: `Canonical VL syntax reference for VL ${VL_VERSION}.`, currentContent: normalizeSyntaxContent( syntaxRemote?.currentContent || fs.readFileSync(path.join(ROOT, 'src', 'data', 'vl-syntax.md'), 'utf-8') ), changeNote: `v${VL_VERSION} — align syntax reference with Doc ID based official document resolution and THEME ${THEME_VERSION}`, }, { path: '2', name: `THEME_${THEME_VERSION}.vth`, description: `Canonical enterprise theme for VL ${VL_VERSION} / THEME ${THEME_VERSION}.`, currentContent: normalizeThemeContent( fs.readFileSync(path.join(ROOT, 'public', 'seed-theme', 'Theme.vth'), 'utf-8') ), changeNote: `v${THEME_VERSION} — align Theme root tag, VL version, and enterprise seed with official Doc ID configuration`, }, { path: '3', name: `VL Workflow Spec ${WORKFLOW_SPEC_DOC_VERSION}`, description: 'Canonical workflow specification with Doc ID priority, Tool_* runtime semantics, and checkpoint rerun compatibility.', currentContent: fs.readFileSync(path.join(ROOT, 'docs', `vl-workflow-spec-${WORKFLOW_SPEC_DOC_VERSION}.md`), 'utf-8'), changeNote: `v${WORKFLOW_SPEC_DOC_VERSION} — clarify Doc ID as persisted workflow binding, keep path as slot alias, and align workflow runtime compatibility`, }, { path: '4', name: 'VL Metadata Spec', description: 'Canonical VL ProjectMeta schema for normalization, diff, workflow regeneration, and IDE tooling.', currentContent: fs.readFileSync(path.join(ROOT, 'docs', 'vl-metadata-spec-3.1.md'), 'utf-8'), changeNote: 'v3.1 — clarify Doc ID bindings stay outside ProjectMeta while keeping canonical schema and theme slot metadata rules', }, ]; } async function ensureDoc(entry, catalog) { let doc = catalog.find((item) => String(item.path) === String(entry.path)); if (doc) return doc; if (!APPLY) { return null; } const created = await dcPost('SERVICE_DocCenter_CreateDoc', { name: entry.name, description: entry.description, path: entry.path, }); doc = created?.data || created; catalog.push({ _id: doc?._id || doc?.id || doc?.docId, id: doc?.id, path: entry.path, name: entry.name, }); return catalog.find((item) => String(item.path) === String(entry.path)) || doc; } async function publishEntry(entry, catalog) { const doc = await ensureDoc(entry, catalog); const docId = doc?._id || doc?.id || null; const remote = docId ? await getDocById(docId) : null; const remoteContent = remote?.currentContent || ''; const action = remoteContent === entry.currentContent ? 'unchanged' : (APPLY ? 'published' : 'publish'); if (APPLY && remoteContent !== entry.currentContent) { await dcPost('SERVICE_DocCenter_SaveAsVersion', { path: entry.path, name: entry.name, description: entry.description, currentContent: entry.currentContent, changeNote: entry.changeNote, }); } return { action, path: entry.path, name: entry.name, docId: docId || '(new)', }; } async function main() { const catalog = await loadCatalog(); const manifest = await buildManifest(catalog); console.log(`\nDocCenter core-doc publish ${APPLY ? '(apply)' : '(dry-run)'}`); for (const entry of manifest) { const result = await publishEntry(entry, catalog); const label = result.action.padEnd(9, ' '); console.log(`${label} path ${result.path} ${result.name} (docId ${result.docId})`); } } main().catch((err) => { console.error(err.message); process.exit(1); });