publish-core-docs.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env node
  2. import fs from 'fs';
  3. import os from 'os';
  4. import path from 'path';
  5. import { fileURLToPath } from 'url';
  6. import {
  7. VL_VERSION,
  8. THEME_VERSION,
  9. THEME_VERSION_FULL,
  10. STYLESPACE_VERSION,
  11. WORKFLOW_SPEC_DOC_VERSION,
  12. } from '../src/data/versions.js';
  13. import { DOCCENTER_API_URL } from '../src/data/doc-paths.js';
  14. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  15. const ROOT = path.resolve(__dirname, '..');
  16. const APPLY = process.argv.includes('--apply');
  17. const AUTH_PATH = path.join(os.homedir(), '.vl-code', 'auth.json');
  18. const cookie = process.env.DOCCENTER_COOKIE || (() => {
  19. try {
  20. return JSON.parse(fs.readFileSync(AUTH_PATH, 'utf-8')).cookie;
  21. } catch {
  22. return '';
  23. }
  24. })();
  25. if (!cookie) {
  26. console.error('Missing DocCenter cookie. Set DOCCENTER_COOKIE or login so ~/.vl-code/auth.json exists.');
  27. process.exit(1);
  28. }
  29. const headers = {
  30. 'Content-Type': 'application/json',
  31. Cookie: String(cookie).startsWith('ih5bearer=') ? String(cookie) : `ih5bearer=${cookie}`,
  32. };
  33. async function dcPost(endpoint, body) {
  34. const res = await fetch(`${DOCCENTER_API_URL}/${endpoint}`, {
  35. method: 'POST',
  36. headers,
  37. body: JSON.stringify(body),
  38. });
  39. const text = await res.text();
  40. let payload = {};
  41. try {
  42. payload = text ? JSON.parse(text) : {};
  43. } catch {
  44. throw new Error(`DocCenter returned non-JSON for ${endpoint}: ${text.slice(0, 300)}`);
  45. }
  46. if (!res.ok) {
  47. throw new Error(payload?.error || payload?.message || `DocCenter ${endpoint} failed: ${res.status}`);
  48. }
  49. return payload;
  50. }
  51. async function loadCatalog() {
  52. const result = await dcPost('SERVICE_DocCenter_GetDocList', {
  53. keyword: '',
  54. tagId: 0,
  55. page: 1,
  56. pageSize: 300,
  57. });
  58. return Array.isArray(result?.data) ? result.data : [];
  59. }
  60. async function getDocById(docId) {
  61. const result = await dcPost('SERVICE_DocCenter_GetDocById', { docId });
  62. return result?.data || null;
  63. }
  64. function normalizeSyntaxContent(content) {
  65. return String(content || '')
  66. .replace(/Current version:\s*`\/\/ VL_VERSION:[\d.]+`/g, `Current version: \`// VL_VERSION:${VL_VERSION}\``)
  67. .replace(/\/\/ VL_VERSION:[\d.]+/g, `// VL_VERSION:${VL_VERSION}`)
  68. .replace(/THEME_[\d.]+\.vth/g, `THEME_${THEME_VERSION}.vth`)
  69. .replace(/Theme-Enterprise-[\d.]+/g, `Theme-Enterprise-${THEME_VERSION}`);
  70. }
  71. function normalizeThemeContent(content) {
  72. return String(content || '')
  73. .replace(/VL_VERSION:[\d.]+/g, `VL_VERSION:${VL_VERSION}`)
  74. .replace(/Theme-Enterprise-[\d.]+/g, `Theme-Enterprise-${THEME_VERSION}`)
  75. .replace(/version:"[\d.]+"/g, `version:"${THEME_VERSION_FULL}"`)
  76. .replace(/styleSpaceVersion:"[\d.]+"/g, `styleSpaceVersion:"${STYLESPACE_VERSION}"`);
  77. }
  78. async function buildManifest(catalog) {
  79. const syntaxDoc = catalog.find((doc) => String(doc.path) === '1');
  80. const syntaxRemote = syntaxDoc ? await getDocById(syntaxDoc._id || syntaxDoc.id) : null;
  81. return [
  82. {
  83. path: '1',
  84. name: `VL_${VL_VERSION}.md`,
  85. description: `Canonical VL syntax reference for VL ${VL_VERSION}.`,
  86. currentContent: normalizeSyntaxContent(
  87. syntaxRemote?.currentContent || fs.readFileSync(path.join(ROOT, 'src', 'data', 'vl-syntax.md'), 'utf-8')
  88. ),
  89. changeNote: `v${VL_VERSION} — align syntax reference with Doc ID based official document resolution and THEME ${THEME_VERSION}`,
  90. },
  91. {
  92. path: '2',
  93. name: `THEME_${THEME_VERSION}.vth`,
  94. description: `Canonical enterprise theme for VL ${VL_VERSION} / THEME ${THEME_VERSION}.`,
  95. currentContent: normalizeThemeContent(
  96. fs.readFileSync(path.join(ROOT, 'public', 'seed-theme', 'Theme.vth'), 'utf-8')
  97. ),
  98. changeNote: `v${THEME_VERSION} — align Theme root tag, VL version, and enterprise seed with official Doc ID configuration`,
  99. },
  100. {
  101. path: '3',
  102. name: `VL Workflow Spec ${WORKFLOW_SPEC_DOC_VERSION}`,
  103. description: 'Canonical workflow specification with Doc ID priority, Tool_* runtime semantics, and checkpoint rerun compatibility.',
  104. currentContent: fs.readFileSync(path.join(ROOT, 'docs', `vl-workflow-spec-${WORKFLOW_SPEC_DOC_VERSION}.md`), 'utf-8'),
  105. changeNote: `v${WORKFLOW_SPEC_DOC_VERSION} — document Doc ID priority, locked core spec slots, and workflow runtime compatibility`,
  106. },
  107. {
  108. path: '4',
  109. name: 'VL Metadata Spec',
  110. description: 'Canonical VL ProjectMeta schema for normalization, diff, workflow regeneration, and IDE tooling.',
  111. currentContent: fs.readFileSync(path.join(ROOT, 'docs', 'vl-metadata-spec-3.0.md'), 'utf-8'),
  112. changeNote: 'v3.0 — define canonical ProjectMeta schema, ID rules, theme slot metadata, and legacy normalization mapping',
  113. },
  114. ];
  115. }
  116. async function ensureDoc(entry, catalog) {
  117. let doc = catalog.find((item) => String(item.path) === String(entry.path));
  118. if (doc) return doc;
  119. if (!APPLY) {
  120. return null;
  121. }
  122. const created = await dcPost('SERVICE_DocCenter_CreateDoc', {
  123. name: entry.name,
  124. description: entry.description,
  125. path: entry.path,
  126. });
  127. doc = created?.data || created;
  128. catalog.push({
  129. _id: doc?._id || doc?.id || doc?.docId,
  130. id: doc?.id,
  131. path: entry.path,
  132. name: entry.name,
  133. });
  134. return catalog.find((item) => String(item.path) === String(entry.path)) || doc;
  135. }
  136. async function publishEntry(entry, catalog) {
  137. const doc = await ensureDoc(entry, catalog);
  138. const docId = doc?._id || doc?.id || null;
  139. const remote = docId ? await getDocById(docId) : null;
  140. const remoteContent = remote?.currentContent || '';
  141. const action = remoteContent === entry.currentContent ? 'unchanged' : (APPLY ? 'published' : 'publish');
  142. if (APPLY && remoteContent !== entry.currentContent) {
  143. await dcPost('SERVICE_DocCenter_SaveAsVersion', {
  144. path: entry.path,
  145. name: entry.name,
  146. description: entry.description,
  147. currentContent: entry.currentContent,
  148. changeNote: entry.changeNote,
  149. });
  150. }
  151. return {
  152. action,
  153. path: entry.path,
  154. name: entry.name,
  155. docId: docId || '(new)',
  156. };
  157. }
  158. async function main() {
  159. const catalog = await loadCatalog();
  160. const manifest = await buildManifest(catalog);
  161. console.log(`\nDocCenter core-doc publish ${APPLY ? '(apply)' : '(dry-run)'}`);
  162. for (const entry of manifest) {
  163. const result = await publishEntry(entry, catalog);
  164. const label = result.action.padEnd(9, ' ');
  165. console.log(`${label} path ${result.path} ${result.name} (docId ${result.docId})`);
  166. }
  167. }
  168. main().catch((err) => {
  169. console.error(err.message);
  170. process.exit(1);
  171. });