UNPKG

@curvenote/cli

Version:
221 lines (220 loc) 9.41 kB
import { postNewWork, postNewWorkVersionFromMetadata } from './push.js'; import { postNewSubmission, postUpdateSubmissionWorkVersion } from '../submissions/utils.js'; import { determineCollectionAndKind, getVenueCollections } from '../submissions/submit.utils.js'; import { checkVenueExists, ensureVenue } from '../sites/utils.js'; import { writeJsonLogs } from 'myst-cli'; import fs from 'node:fs'; import path from 'node:path'; import { load as yamlLoad } from 'js-yaml'; import { getFromJournals } from '../utils/api.js'; import { resolveExistingWork } from './resolveExistingWork.js'; function detectContentYamlPath() { const cwd = process.cwd(); const curvenoteConfig = path.join(cwd, 'curvenote.yml'); if (fs.existsSync(curvenoteConfig)) return curvenoteConfig; const mystConfig = path.join(cwd, 'myst.yml'); if (fs.existsSync(mystConfig)) return mystConfig; return undefined; } function resolveContentYamlPath(opts) { const explicit = opts?.contentYaml?.trim(); if (explicit) { const resolved = path.resolve(explicit); if (!fs.existsSync(resolved)) { throw new Error(`content yaml file not found: ${resolved}`); } return resolved; } return detectContentYamlPath(); } function parseContentYaml(session, filePath) { if (!filePath) return undefined; if (!fs.existsSync(filePath)) { throw new Error(`content yaml file not found: ${filePath}`); } const raw = fs.readFileSync(filePath, 'utf-8'); const parsed = yamlLoad(raw); if (!parsed || typeof parsed !== 'object') { throw new Error(`invalid content yaml: expected object at root`); } const root = parsed.project && typeof parsed.project === 'object' ? parsed.project : parsed; const authorDetails = Array.isArray(root.authors) ? root.authors.map((author) => (typeof author === 'string' ? { name: author } : author)) : undefined; const authors = authorDetails ?.map((author) => author?.name) .filter((name) => typeof name === 'string' && name.length > 0); session.log.debug(`Loaded content metadata from ${filePath}`); return { raw: parsed, id: typeof root.id === 'string' ? root.id : undefined, title: typeof root.title === 'string' ? root.title : undefined, description: typeof root.description === 'string' ? root.description : undefined, authors, author_details: authorDetails, doi: typeof root.doi === 'string' ? root.doi : undefined, date: typeof root.date === 'string' ? root.date : root.date instanceof Date ? root.date.toISOString() : undefined, }; } function parseMetadataJson(metadataInput, label = 'metadata') { if (!metadataInput) return undefined; const raw = fs.existsSync(metadataInput) ? fs.readFileSync(metadataInput, 'utf-8') : metadataInput; let parsed; try { parsed = JSON.parse(raw); } catch { throw new Error(`invalid ${label} json (provide inline JSON or a JSON file path)`); } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error(`invalid ${label} json: expected an object`); } return parsed; } function buildWorkVersionMetadata(yamlMetadata, workMetadataInput) { const workMetadata = parseMetadataJson(workMetadataInput, 'work-metadata'); const mystRaw = yamlMetadata?.raw; if (!mystRaw && !workMetadata) return undefined; return { ...(mystRaw ? { 'frontmatter.myst': mystRaw } : {}), ...workMetadata, }; } const registrationTargetsCache = new Map(); function registrationTargetsCacheKey(venue, opts) { return `${venue}\0${opts?.collection ?? ''}\0${opts?.kind ?? ''}`; } async function resolveRegistrationTargets(session, venue, opts) { const cacheKey = registrationTargetsCacheKey(venue, opts); const cached = registrationTargetsCache.get(cacheKey); if (cached) return cached; const collections = await getVenueCollections(session, venue); const resolved = await determineCollectionAndKind(session, venue, collections, { kind: opts?.kind, collection: opts?.collection, yes: opts?.yes, }); registrationTargetsCache.set(cacheKey, { collection: resolved.collection, kind: resolved.kind, }); return registrationTargetsCache.get(cacheKey); } export async function register(session, opts) { if (session.isAnon) { throw new Error('⛔️ You must be authenticated for this command. Use `curvenote token set [token]`'); } if (!opts?.venue) { throw new Error('venue is required'); } if ((opts.cdn && !opts.cdnKey) || (!opts.cdn && opts.cdnKey)) { throw new Error('cdn and cdnKey must be provided together'); } if (opts.cdn && !opts.source) { throw new Error('source is required when cdn/cdnKey are provided'); } const yamlMetadata = parseContentYaml(session, resolveContentYamlPath(opts)); const submissionMetadata = parseMetadataJson(opts.submissionMetadata, 'submission-metadata'); const workVersionMetadata = buildWorkVersionMetadata(yamlMetadata, opts.workMetadata); const title = opts.title ?? yamlMetadata?.title; if (!title) { throw new Error('title is required (pass --title or set title in myst.yml/curvenote.yml)'); } const venue = await ensureVenue(session, opts.venue, opts); await checkVenueExists(session, venue); const { collection, kind } = await resolveRegistrationTargets(session, venue, opts); const doi = yamlMetadata?.doi; let work; let workVersionId; const existingWork = await resolveExistingWork(session, { mode: opts.key ?? 'id', key: yamlMetadata?.id, doi, fallbackCreateKey: yamlMetadata?.id, yes: opts.yes, forceNew: opts.new, contextLabel: 'register', }); const tags = opts.tags && opts.tags.length > 0 ? opts.tags : undefined; if (existingWork) { const updated = await postNewWorkVersionFromMetadata(session, existingWork.links.versions, { title, description: yamlMetadata?.description, authors: yamlMetadata?.authors, author_details: yamlMetadata?.author_details, doi, date: yamlMetadata?.date, contains: opts.source ? [opts.source] : undefined, cdn: opts.cdn, cdn_key: opts.cdnKey, metadata: workVersionMetadata, }); work = updated; workVersionId = updated.version_id; } else { work = await postNewWork(session, '', '', yamlMetadata?.id, { title, description: yamlMetadata?.description, authors: yamlMetadata?.authors, author_details: yamlMetadata?.author_details, doi, date: yamlMetadata?.date, contains: opts.source ? [opts.source] : undefined, cdn: opts.cdn, cdn_key: opts.cdnKey, metadata: workVersionMetadata, }); workVersionId = work.version_id; } if (!workVersionId) throw new Error('Failed to create a work version'); // If a submission already exists for this work at this venue, create a new submission version const mine = (await getFromJournals(session, `/my/submissions/?work_id=${encodeURIComponent(work.id)}&site=${encodeURIComponent(venue)}`)); const existingSubmission = mine.items.find((s) => s.site_name === venue && s.active_version.work_id === work.id); let submissionId; let submissionDateCreated; let submissionVersionId; let submissionVersionDateCreated; if (existingSubmission) { const sv = await postUpdateSubmissionWorkVersion(session, venue, existingSubmission.links.versions, workVersionId, undefined, submissionMetadata, tags); submissionId = existingSubmission.id; submissionDateCreated = existingSubmission.date_created; submissionVersionId = sv.id; submissionVersionDateCreated = sv.date_created; } else { const submission = await postNewSubmission(session, venue, collection.id, kind.id, workVersionId, opts.draft ?? false, undefined, submissionMetadata, tags); submissionId = submission.id; submissionDateCreated = submission.date_created; submissionVersionId = submission.active_version_id; submissionVersionDateCreated = submission.date_created; } writeJsonLogs(session, 'curvenote.work.register.json', { venue, work: { id: work.id, date_created: work.date_created }, workVersion: { id: workVersionId, date_created: work.date_created }, submission: { id: submissionId, date_created: submissionDateCreated }, submissionVersion: { id: submissionVersionId, date_created: submissionVersionDateCreated }, }); const workAction = existingWork ? 'Updated existing work' : 'Registered new work'; session.log.info(`✅ ${workAction} with "${venue}"`); session.log.debug(`Work ID: ${work.id}`); session.log.debug(`Work Version ID: ${workVersionId}`); session.log.debug(`Submission ID: ${submissionId}`); session.log.debug(`Submission Version ID: ${submissionVersionId}`); }