@anja.codes/vitakit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/check.d.mts +22 -0
  4. package/dist/check.mjs +196 -0
  5. package/dist/cli.d.mts +45 -0
  6. package/dist/cli.mjs +225 -0
  7. package/dist/index.d.mts +398 -0
  8. package/dist/index.mjs +21 -0
  9. package/dist/nuxt.d.mts +15 -0
  10. package/dist/nuxt.mjs +98 -0
  11. package/dist/runtime/components/VitaDate.vue +20 -0
  12. package/dist/runtime/components/VitaEntry.vue +24 -0
  13. package/dist/runtime/components/VitaIssues.vue +25 -0
  14. package/dist/runtime/components/VitaLinks.vue +19 -0
  15. package/dist/runtime/components/VitaMarkdown.vue +14 -0
  16. package/dist/runtime/components/VitaPublication.vue +30 -0
  17. package/dist/runtime/components/VitaPublications.vue +39 -0
  18. package/dist/runtime/components/VitaSection.vue +60 -0
  19. package/dist/runtime/composables/useVitakit.ts +47 -0
  20. package/dist/schemas/education.ts +21 -0
  21. package/dist/schemas/employment.ts +21 -0
  22. package/dist/schemas/publications.ts +48 -0
  23. package/dist/schemas/supervision.ts +20 -0
  24. package/dist/schemas/talks.ts +19 -0
  25. package/dist/schemas/teaching.ts +19 -0
  26. package/dist/shared/index.d.mts +68 -0
  27. package/dist/shared/index.mjs +57 -0
  28. package/dist/shared/vitakit.BRhjqpAt.mjs +250 -0
  29. package/dist/shared/vitakit.BlU-p01e.mjs +14 -0
  30. package/dist/shared/vitakit.BxcnlFGP.mjs +325 -0
  31. package/dist/shared/vitakit.CypCF-4Q.mjs +503 -0
  32. package/dist/shared/vitakit.D8WRg_Xx.d.mts +153 -0
  33. package/dist/styles/vitakit-math.csl +314 -0
  34. package/dist/tex/templates/_generic.eta +6 -0
  35. package/dist/tex/templates/education.eta +9 -0
  36. package/dist/tex/templates/employment.eta +9 -0
  37. package/dist/tex/templates/publications.eta +7 -0
  38. package/dist/tex/templates/supervision.eta +4 -0
  39. package/dist/tex/templates/talks.eta +3 -0
  40. package/dist/tex/templates/teaching.eta +6 -0
  41. package/dist/tex/vitakit-cv.cls +119 -0
  42. package/dist/tex.d.mts +93 -0
  43. package/dist/tex.mjs +129 -0
  44. package/package.json +121 -0
  45. package/src/bin/vitakit-build-cv.mjs +50 -0
  46. package/src/bin/vitakit-check.mjs +57 -0
  47. package/src/bin/vitakit.mjs +158 -0
  48. package/styles/vitakit-math.csl +314 -0
package/dist/tex.mjs ADDED
@@ -0,0 +1,129 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, readFile, writeFile, rm, copyFile, rename } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import process from 'node:process';
5
+ import { join, delimiter } from 'node:path';
6
+ import { l as loadContent, a as loadPublicationsSchema } from './shared/vitakit.CypCF-4Q.mjs';
7
+ import { m as fail, n as isSitePath } from './shared/vitakit.BxcnlFGP.mjs';
8
+ import { d as discoverDocuments, e as emitDocument, b as bundledTexDir, a as escapeTex, c as escapeUrl, S as SITE_PATH_HELP } from './shared/vitakit.BRhjqpAt.mjs';
9
+ export { f as documentSchema, g as escapeRichTex, s as selectEntries } from './shared/vitakit.BRhjqpAt.mjs';
10
+ import 'gray-matter';
11
+ import 'yaml';
12
+ import 'unified';
13
+ import 'remark-parse';
14
+ import 'remark-gfm';
15
+ import 'remark-math';
16
+ import 'remark-rehype';
17
+ import 'rehype-katex';
18
+ import 'rehype-raw';
19
+ import 'rehype-stringify';
20
+ import 'unist-util-visit';
21
+ import '@behackl/citation-js-extras';
22
+ import 'rehype-parse';
23
+ import 'rehype-sanitize';
24
+ import 'zod';
25
+ import 'node:url';
26
+ import 'eta';
27
+ import './shared/index.mjs';
28
+ import './shared/vitakit.BlU-p01e.mjs';
29
+
30
+ const CLASS_FILE = "vitakit-cv.cls";
31
+ function run(command, args, cwd, env = {}) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn(command, args, { cwd, stdio: "inherit", env: { ...process.env, ...env } });
34
+ child.on("error", (error) => reject(
35
+ error.code === "ENOENT" ? new Error(`${command} not found. Install a TeX distribution, or use --no-pdf to only generate .tex files.`) : error
36
+ ));
37
+ child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited with code ${code}`)));
38
+ });
39
+ }
40
+ function profilePreamble(content) {
41
+ const { profile } = content;
42
+ const href = (url) => {
43
+ if (isSitePath(url)) fail("content/profile.yml", `links: ${url} ${SITE_PATH_HELP}`);
44
+ return escapeUrl(url);
45
+ };
46
+ const affiliation = profile.affiliation?.name ? escapeTex(profile.affiliation.name) : "";
47
+ const tagline = [profile.title && escapeTex(profile.title), affiliation].filter(Boolean).join(", ");
48
+ const contact = [
49
+ profile.email && `\\href{mailto:${escapeUrl(profile.email)}}{${escapeTex(profile.email)}}`,
50
+ ...profile.links.map((link) => `\\href{${href(link.url)}}{${escapeTex(link.label)}}`)
51
+ ].filter(Boolean).join(" \\quad ");
52
+ return [
53
+ "% Generated by vitakit. Do not edit; edit content/profile.yml instead.",
54
+ `\\vitaname{${escapeTex(profile.name)}}`,
55
+ `\\vitatagline{${tagline}}`,
56
+ `\\vitacontact{${contact}}`,
57
+ ...profile.url ? siteUrlSourcemap(profile.url) : [],
58
+ ""
59
+ ].join("\n");
60
+ }
61
+ function siteUrlSourcemap(siteUrl) {
62
+ const base = siteUrl.replace(/\/?$/, "/");
63
+ if (!/^https?:\/\/[\w.:/-]+$/.test(base)) {
64
+ fail("content/profile.yml", `url: ${siteUrl} may only contain letters, digits and - . _ : /`);
65
+ }
66
+ return [
67
+ "\\ifdefined\\DeclareSourcemap",
68
+ ` \\DeclareSourcemap{\\maps[datatype=bibtex]{\\map{\\step[fieldsource=url, match=\\regexp{\\A/}, replace={${base}}]}}}`,
69
+ "\\fi"
70
+ ];
71
+ }
72
+ async function buildDocument(document, content, options, cvDir) {
73
+ const outDir = join(cvDir, "generated", document.name);
74
+ await emitDocument({
75
+ sections: content.sections,
76
+ publications: content.publications,
77
+ siteUrl: content.profile.url,
78
+ options: document.options,
79
+ templateDir: join(cvDir, "templates"),
80
+ outDir
81
+ });
82
+ await writeFile(join(outDir, "profile.tex"), profilePreamble(content));
83
+ const generatedClass = join(outDir, CLASS_FILE);
84
+ if (existsSync(join(cvDir, CLASS_FILE))) await rm(generatedClass, { force: true });
85
+ else await copyFile(join(bundledTexDir, CLASS_FILE), generatedClass);
86
+ if (options.noPdf) return;
87
+ const relativeOut = join("generated", document.name);
88
+ await run("latexmk", [
89
+ "-pdf",
90
+ "-halt-on-error",
91
+ "-interaction=nonstopmode",
92
+ `-outdir=${relativeOut}`,
93
+ `${document.name}.tex`
94
+ ], cvDir, {
95
+ // A trailing delimiter keeps the standard search paths.
96
+ TEXINPUTS: `.${delimiter}${relativeOut}${delimiter}${process.env.TEXINPUTS ?? ""}`
97
+ });
98
+ await rename(join(outDir, `${document.name}.pdf`), join(cvDir, `${document.name}.pdf`));
99
+ }
100
+ async function buildCv(options) {
101
+ const cvDir = join(options.rootDir, "cv-tex");
102
+ if (!existsSync(cvDir)) {
103
+ throw new Error(`no cv-tex/ directory in ${options.rootDir}`);
104
+ }
105
+ const content = await loadContent(options.rootDir, { strict: true, maxVisibility: "private", target: "pdf" });
106
+ const documents = await discoverDocuments(cvDir);
107
+ const groups = (await loadPublicationsSchema(options.rootDir)).groups.map((group) => group.id);
108
+ for (const document of documents) {
109
+ const unknown = (document.options.publications.groups ?? []).filter((id) => !groups.includes(id));
110
+ if (unknown.length) {
111
+ fail(`cv-tex/${document.name}.yml`, `publications.groups: unknown group ${unknown.join(", ")}; the groups are ${groups.join(", ")} (schemas/publications.ts)`);
112
+ }
113
+ }
114
+ const selected = options.only?.length ? documents.filter((document) => options.only.includes(document.name)) : documents;
115
+ if (!selected.length) {
116
+ const known = documents.map((document) => document.name).join(", ");
117
+ throw new Error(`no document matched ${options.only?.join(", ")}; available: ${known}`);
118
+ }
119
+ await mkdir(join(cvDir, "generated"), { recursive: true });
120
+ for (const document of selected) {
121
+ await buildDocument(document, content, options, cvDir);
122
+ }
123
+ return selected.map((document) => join(cvDir, `${document.name}.pdf`));
124
+ }
125
+ async function readGenerated(rootDir, document, section) {
126
+ return readFile(join(rootDir, "cv-tex", "generated", document, `${section}.tex`), "utf8");
127
+ }
128
+
129
+ export { buildCv, bundledTexDir, discoverDocuments, emitDocument, escapeTex, escapeUrl, readGenerated };
package/package.json ADDED
@@ -0,0 +1,121 @@
1
+ {
2
+ "name": "@anja.codes/vitakit",
3
+ "version": "0.1.0",
4
+ "description": "Structured CV data in, academic website and LaTeX CV out.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "cv",
8
+ "academic",
9
+ "nuxt",
10
+ "nuxt-module",
11
+ "latex",
12
+ "bibtex",
13
+ "katex",
14
+ "static-site"
15
+ ],
16
+ "homepage": "https://github.com/anja-codes/vitakit#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/anja-codes/vitakit/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/anja-codes/vitakit.git"
23
+ },
24
+ "type": "module",
25
+ "packageManager": "pnpm@10.29.1",
26
+ "engines": {
27
+ "node": ">=22.18"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.mts",
32
+ "import": "./dist/index.mjs"
33
+ },
34
+ "./nuxt": {
35
+ "types": "./dist/nuxt.d.mts",
36
+ "import": "./dist/nuxt.mjs"
37
+ },
38
+ "./runtime/*": "./dist/runtime/*",
39
+ "./styles/*": "./dist/styles/*",
40
+ "./tex/*": "./dist/tex/*",
41
+ "./shared": {
42
+ "types": "./dist/shared/index.d.mts",
43
+ "import": "./dist/shared/index.mjs"
44
+ },
45
+ "./tex": {
46
+ "types": "./dist/tex.d.mts",
47
+ "import": "./dist/tex.mjs"
48
+ },
49
+ "./check": {
50
+ "types": "./dist/check.d.mts",
51
+ "import": "./dist/check.mjs"
52
+ },
53
+ "./cli": {
54
+ "types": "./dist/cli.d.mts",
55
+ "import": "./dist/cli.mjs"
56
+ }
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "styles",
61
+ "src/bin",
62
+ "README.md",
63
+ "LICENSE"
64
+ ],
65
+ "scripts": {
66
+ "build": "unbuild",
67
+ "prepare": "unbuild",
68
+ "dev": "unbuild --stub",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "typecheck": "tsc --noEmit",
72
+ "check": "pnpm typecheck && pnpm test && pnpm build && node scripts/check-dist.mjs && pnpm example:check",
73
+ "check:dist": "node scripts/check-dist.mjs",
74
+ "prepublishOnly": "pnpm check",
75
+ "example": "node scripts/generate-example.mjs",
76
+ "example:check": "node scripts/generate-example.mjs --check",
77
+ "check:deps": "node scripts/check-deps.mjs"
78
+ },
79
+ "dependencies": {
80
+ "@behackl/citation-js-extras": "^0.4.0",
81
+ "@types/hast": "^3.0.5",
82
+ "citation-js": "^0.7.22",
83
+ "eta": "^4.6.0",
84
+ "gray-matter": "^4.0.3",
85
+ "rehype-katex": "^7.0.1",
86
+ "rehype-parse": "^9.0.1",
87
+ "rehype-raw": "^7.0.0",
88
+ "rehype-sanitize": "^6.0.0",
89
+ "rehype-stringify": "^10.0.1",
90
+ "remark-gfm": "^4.0.1",
91
+ "remark-math": "^6.0.0",
92
+ "remark-parse": "^11.0.0",
93
+ "remark-rehype": "^11.1.2",
94
+ "unified": "^11.0.5",
95
+ "unist-util-visit": "^5.1.0",
96
+ "yaml": "^2.8.1",
97
+ "zod": "^4.5.4"
98
+ },
99
+ "peerDependencies": {
100
+ "@nuxt/kit": "^4.0.0",
101
+ "nuxt": "^4.0.0"
102
+ },
103
+ "devDependencies": {
104
+ "@nuxt/kit": "^4.5.2",
105
+ "@nuxt/schema": "^4.5.2",
106
+ "@types/node": "^26.4.1",
107
+ "nitropack": "^2.13.4",
108
+ "typescript": "^5.9.3",
109
+ "unbuild": "^3.6.1",
110
+ "vitest": "^5.0.0",
111
+ "vue": "^3.5.42"
112
+ },
113
+ "publishConfig": {
114
+ "access": "public"
115
+ },
116
+ "bin": {
117
+ "vitakit": "src/bin/vitakit.mjs",
118
+ "vitakit-check": "src/bin/vitakit-check.mjs",
119
+ "vitakit-build-cv": "src/bin/vitakit-build-cv.mjs"
120
+ }
121
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Render content/ into cv-tex/generated/ and typeset it.
4
+ */
5
+ import process from 'node:process'
6
+ import { buildCv } from '../../dist/tex.mjs'
7
+
8
+ const HELP = `vitakit-build-cv — build the PDF documents in cv-tex/
9
+
10
+ Usage
11
+ pnpm build:cv [document...] [options]
12
+
13
+ Renders content/ into cv-tex/generated/<document>/, one .tex file per section,
14
+ then runs latexmk. The finished PDF is written next to its source, as
15
+ cv-tex/<document>.pdf; everything else stays in the generated directory.
16
+
17
+ pnpm build:cv every document in cv-tex/
18
+ pnpm build:cv cv just cv.tex
19
+ pnpm build:cv cv application several
20
+
21
+ Options
22
+ --no-pdf write the .tex files but do not run latexmk. Useful without a
23
+ TeX installation, and to inspect what was generated.
24
+ --help this text
25
+
26
+ A document is a .tex file you own; a copy of cv.tex is a new document. What it
27
+ contains is decided by an optional cv-tex/<document>.yml (visibility, which
28
+ entries, how many). See docs/cv-documents.md.
29
+
30
+ Requires latexmk and biber, from any current TeX distribution.`
31
+
32
+ const args = process.argv.slice(2)
33
+
34
+ if (args.includes('--help') || args.includes('-h')) {
35
+ console.log(HELP)
36
+ process.exit(0)
37
+ }
38
+
39
+ const noPdf = args.includes('--no-pdf')
40
+ const only = args.filter(argument => !argument.startsWith('-'))
41
+
42
+ try {
43
+ const outputs = await buildCv({ rootDir: process.cwd(), only, noPdf })
44
+ console.log(noPdf
45
+ ? `vitakit: generated .tex for ${only.length ? only.join(', ') : 'all documents'}`
46
+ : `vitakit: built ${outputs.join(', ')}`)
47
+ } catch (error) {
48
+ console.error(`\nvitakit: ${error instanceof Error ? error.message : String(error)}\n`)
49
+ process.exit(1)
50
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Assert that the generated site is complete and correct.
4
+ */
5
+ import process from 'node:process'
6
+ import { checkStatic } from '../../dist/check.mjs'
7
+
8
+ const HELP = `vitakit-check — verify the generated site
9
+
10
+ Usage
11
+ pnpm check:static [--out <dir>]
12
+
13
+ Run after \`nuxt generate\`; \`pnpm check\` does both. Checks the built HTML,
14
+ not your content, because a schema cannot catch a renderer that silently did
15
+ nothing:
16
+
17
+ - every page in content/pages/ was generated
18
+ - mathematics was typeset, and KaTeX reported no errors
19
+ - the site was built for the deployment base path (NUXT_APP_BASE_URL), and
20
+ every link and asset (href, src, srcset, poster, …) stays inside it
21
+ - every link into the site leads to a file or page that was generated,
22
+ e.g. slides: /slides/talk.pdf to public/slides/talk.pdf
23
+ - no entry marked \`visibility: cv\` or \`private\` appears anywhere in the site
24
+ - the bibliography is fully formatted wherever it is shown
25
+
26
+ Options
27
+ --out <dir> the generated site; defaults to .output/public
28
+ --no-leak-check skip the check for cv and private entries. It compares their
29
+ titles with the text of every page, so a title that is
30
+ legitimately public elsewhere looks like a leak.
31
+ --help this text
32
+
33
+ Reports every problem and exits non-zero if there is one, so it can gate a
34
+ deployment.`
35
+
36
+ const args = process.argv.slice(2)
37
+
38
+ if (args.includes('--help') || args.includes('-h')) {
39
+ console.log(HELP)
40
+ process.exit(0)
41
+ }
42
+
43
+ const outIndex = args.indexOf('--out')
44
+
45
+ const { checked, failures } = await checkStatic({
46
+ rootDir: process.cwd(),
47
+ outDir: outIndex === -1 ? undefined : args[outIndex + 1],
48
+ skipLeakCheck: args.includes('--no-leak-check'),
49
+ })
50
+
51
+ for (const line of checked) console.log(` ok ${line}`)
52
+ if (failures.length) {
53
+ console.error(`\nvitakit-check: ${failures.length} problem(s):`)
54
+ for (const failure of failures) console.error(` \u2716 ${failure}`)
55
+ process.exit(1)
56
+ }
57
+ console.log('\nvitakit-check: generated site looks correct.')
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The vitakit command. Three subcommands, each of which only writes files you
4
+ * could have written yourself — see docs/cli.md for why there are not more.
5
+ */
6
+ import process from 'node:process'
7
+ import { relative } from 'node:path'
8
+ import { addSection, eject, listEjectable } from '../../dist/cli.mjs'
9
+
10
+ const USAGE = `vitakit — files for a vitakit project
11
+
12
+ Usage
13
+ pnpm vitakit <command> [options]
14
+
15
+ Commands
16
+ add <name> set up a new section: content file, schema, component
17
+ list show everything that can be ejected
18
+ eject <name> copy a package default into your project
19
+
20
+ Run a command with --help for details and examples.
21
+
22
+ Related commands, from the same package:
23
+ pnpm build:cv render content/ into cv-tex/ and run latexmk
24
+ pnpm check typecheck, generate the site, and verify the output
25
+ `
26
+
27
+ const HELP = {
28
+ add: `vitakit add <name> — set up a new section
29
+
30
+ A section is a file in content/, a schema describing what it may contain, and
31
+ usually a component deciding how it looks. This writes whichever of those are
32
+ missing; it never overwrites a file you have.
33
+
34
+ pnpm vitakit add outreach
35
+
36
+ content/outreach.yml your data — edit this first
37
+ schemas/outreach.ts the shape of an entry
38
+ app/components/OutreachSection.vue how the entries look
39
+
40
+ If content/<name>.yml already exists, the schema is drafted from it: field
41
+ types are guessed from your data — dates, URLs, enums, and which fields are
42
+ optional — so you correct a draft instead of writing one.
43
+
44
+ pnpm vitakit add outreach # after writing content/outreach.yml
45
+
46
+ Options
47
+ --no-component do not write a component; the section then renders through
48
+ <VitaSection>, which is a fallback rather than a design
49
+
50
+ Notes
51
+ The name becomes a file name: lowercase, kebab-case (research-stays).
52
+ For a built-in section (education, employment, teaching, talks,
53
+ supervision) you do not need this: create content/<name>.yml and it works.
54
+ To change what a built-in section may contain, use \`vitakit eject <name>\`.
55
+
56
+ Afterwards
57
+ Show it on a page with <OutreachSection /> in app/pages/, or put
58
+ \\vitasection{outreach} in cv-tex/cv.tex to add it to the PDF.`,
59
+
60
+ list: `vitakit list — show everything that can be ejected
61
+
62
+ Prints each item, what kind of thing it is, and where a copy would land:
63
+
64
+ VitaSection component -> app/components/VitaSection.vue
65
+ talks.eta template -> cv-tex/templates/talks.eta
66
+ teaching schema -> schemas/teaching.ts
67
+ publications schema -> schemas/publications.ts
68
+ publications.csl style -> content/publications.csl
69
+ vitakit-cv.cls class -> cv-tex/vitakit-cv.cls
70
+
71
+ component how a section looks on the website
72
+ schema what a section may contain; for publications, the groups,
73
+ the links after each reference and the fields passed on
74
+ template how a section is written into the PDF
75
+ style how publications.bib is formatted on the website (CSL)
76
+ class the LaTeX document class: fonts, spacing, the entry macros`,
77
+
78
+ eject: `vitakit eject <name> — copy a package default into your project
79
+
80
+ The copy wins from then on: vitakit always prefers a file in your project over
81
+ its own. This is the only command that exists purely for convenience — the
82
+ files live inside node_modules/.pnpm/…, at a path that changes with every
83
+ update, so copying them by hand is impractical.
84
+
85
+ pnpm vitakit eject VitaSection # restyle how sections look
86
+ pnpm vitakit eject teaching # add or remove fields of a section
87
+ pnpm vitakit eject talks.eta # change how talks appear in the PDF
88
+ pnpm vitakit eject publications.csl # change the citation style of the website
89
+ pnpm vitakit eject vitakit-cv.cls # take over the LaTeX document class
90
+
91
+ Use \`vitakit list\` to see the names.
92
+
93
+ Options
94
+ --force overwrite an existing file
95
+
96
+ Trade-off
97
+ An ejected file is yours forever: later improvements to it in vitakit will
98
+ not reach you. Everything that can carry a bug — validation, rendering,
99
+ sorting, escaping — stays in the package, so this is usually the right
100
+ trade, but eject deliberately rather than by default.
101
+
102
+ Before ejecting a component, check whether CSS is enough: the classes
103
+ (vita-entry, vita-entry__date, …) are styled in app/assets/css/main.css.`,
104
+ }
105
+
106
+ const [command, ...rest] = process.argv.slice(2)
107
+ const wantsHelp = rest.includes('--help') || rest.includes('-h')
108
+ const root = process.cwd()
109
+ const pascal = name => name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('')
110
+
111
+ try {
112
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
113
+ console.log(USAGE)
114
+ process.exit(command ? 0 : 1)
115
+ }
116
+
117
+ if (!HELP[command]) {
118
+ throw new Error(`unknown command "${command}".\n\n${USAGE}`)
119
+ }
120
+
121
+ if (wantsHelp) {
122
+ console.log(HELP[command])
123
+ process.exit(0)
124
+ }
125
+
126
+ if (command === 'add') {
127
+ const name = rest.find(argument => !argument.startsWith('-'))
128
+ if (!name) throw new Error(`missing name.\n\n${HELP.add}`)
129
+ const written = await addSection(name, root, { component: !rest.includes('--no-component') })
130
+ for (const file of written) console.log(` ${file.path.padEnd(34)} ${file.note}`)
131
+ console.log(`\nShow it with <${pascal(name)}Section /> on a page,`)
132
+ console.log(`or add \\vitasection{${name}} to cv-tex/cv.tex for the PDF.`)
133
+ } else if (command === 'list') {
134
+ const items = await listEjectable()
135
+ const width = Math.max(...items.map(item => item.name.length))
136
+ for (const item of items) {
137
+ console.log(`${item.name.padEnd(width)} ${item.kind.padEnd(9)} -> ${item.target}`)
138
+ }
139
+ console.log('\nCopy one with `pnpm vitakit eject <name>`; see --help for what each kind does.')
140
+ } else if (command === 'eject') {
141
+ const name = rest.find(argument => !argument.startsWith('-'))
142
+ if (!name) throw new Error(`missing name. Run \`pnpm vitakit list\` to see the options.`)
143
+ const { item, path } = await eject(name, root, { force: rest.includes('--force') })
144
+ console.log(`vitakit: wrote ${relative(root, path)}`)
145
+ const hints = {
146
+ publications: ' Edit it to change the publication groups, badges or fields; the website,\n'
147
+ + ' vitakit-check and the PDF all read it from now on.',
148
+ schema: ' Edit it to change what this section may contain; it now defines the section.',
149
+ style: ' Edit it, or replace it with any CSL style (e.g. from zotero.org/styles);\n'
150
+ + ' it formats content/publications.bib on the website from now on.',
151
+ }
152
+ const hint = item.kind === 'schema' && item.name === 'publications' ? hints.publications : hints[item.kind]
153
+ console.log(hint ?? ' It takes precedence over the package copy from now on.')
154
+ }
155
+ } catch (error) {
156
+ console.error(`\nvitakit: ${error instanceof Error ? error.message : String(error)}\n`)
157
+ process.exit(1)
158
+ }