@anja.codes/create-vitakit 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +47 -2
- package/deployments.mjs +178 -0
- package/index.mjs +99 -0
- package/package.json +40 -4
- package/scaffold.mjs +124 -0
- package/template/.gitignore.template +19 -0
- package/template/_forgejo/workflows/codeberg.yml +37 -0
- package/template/_forgejo/workflows/deploy.yml +31 -0
- package/template/_github/workflows/cloudflare.yml +33 -0
- package/template/_github/workflows/cv.yml +36 -0
- package/template/_github/workflows/deploy.yml +53 -0
- package/template/_github/workflows/workers.yml +39 -0
- package/template/app/app.vue +5 -0
- package/template/app/assets/css/main.css +129 -0
- package/template/app/layouts/default.vue +39 -0
- package/template/app/pages/[...slug].vue +30 -0
- package/template/app/pages/cv.vue +34 -0
- package/template/app/pages/index.vue +62 -0
- package/template/app/pages/publications.vue +27 -0
- package/template/content/education.yml +18 -0
- package/template/content/employment.yml +18 -0
- package/template/content/pages/cv.md +9 -0
- package/template/content/pages/index.md +11 -0
- package/template/content/pages/publications.md +8 -0
- package/template/content/pages/research.md +32 -0
- package/template/content/profile.yml +33 -0
- package/template/content/publications.bib +43 -0
- package/template/content/supervision.yml +12 -0
- package/template/content/talks.yml +17 -0
- package/template/content/teaching.yml +17 -0
- package/template/cv-tex/cv.tex +39 -0
- package/template/cv-tex/cv.yml +7 -0
- package/template/cv-tex/latexmkrc +5 -0
- package/template/cv-tex/preamble.tex +7 -0
- package/template/cv-tex/prose/research-statement.tex +17 -0
- package/template/gitlab-ci.yml.template +28 -0
- package/template/nuxt.config.ts +32 -0
- package/template/package.json.template +33 -0
- package/template/public/img/portrait.svg +6 -0
- package/template/public/img/tree.svg +10 -0
- package/template/tsconfig.json +9 -0
- package/template/wrangler.json.template +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anja Ressmann, Benjamin Hackl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @anja.codes/create-vitakit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffold a [vitakit](https://github.com/anja-codes/vitakit) project: an academic
|
|
4
|
+
website and a LaTeX CV built from one set of files.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pnpm create @anja.codes/vitakit my-site
|
|
8
|
+
cd my-site
|
|
9
|
+
pnpm install
|
|
10
|
+
pnpm dev # http://localhost:3000
|
|
11
|
+
pnpm pdf # cv-tex/cv.pdf (needs a TeX distribution)
|
|
12
|
+
pnpm check # what CI runs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`npm create @anja.codes/vitakit my-site` works too: the project is then written
|
|
16
|
+
for npm, with `npm run …` in its scripts, workflows and instructions.
|
|
17
|
+
|
|
18
|
+
It asks for your name, position, affiliation, email and a deployment target
|
|
19
|
+
(GitHub Pages, Cloudflare Workers or Pages, GitLab Pages, Codeberg Pages,
|
|
20
|
+
existing web space via SSH/rsync, or decide later), copies the template and
|
|
21
|
+
substitutes those few values.
|
|
22
|
+
|
|
23
|
+
Choose `?` at the deployment prompt for a compact comparison of URLs,
|
|
24
|
+
requirements and CI. Numbered setup steps follow your selection.
|
|
25
|
+
To read the comparison without creating files:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm create @anja.codes/vitakit --deployment-help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **GitHub Pages:** public source on Free; a paid plan for private source.
|
|
32
|
+
- **Cloudflare Workers:** recommended for new Cloudflare projects; uses GitHub Actions.
|
|
33
|
+
- **Cloudflare Pages:** legacy option, still available; also uses GitHub Actions.
|
|
34
|
+
- **GitLab Pages:** private source supported on Free; uses GitLab CI/CD.
|
|
35
|
+
- **Codeberg Pages:** uses git-pages and Forgejo Actions. Hosted CI requires public,
|
|
36
|
+
freely licensed source; use your own runner for sensitive CV source.
|
|
37
|
+
- **SSH/rsync:** needs existing web space; the preset uses Forgejo Actions.
|
|
38
|
+
|
|
39
|
+
Choosing to decide later adds no workflows and leaves all hosting options open.
|
|
40
|
+
|
|
41
|
+
See [publishing and setup](../docs/guide/publishing.md) for details.
|
|
42
|
+
|
|
43
|
+
Everything afterwards is ordinary files in a repository you own. The
|
|
44
|
+
`vitakit` command in the project helps with the common changes — `pnpm vitakit
|
|
45
|
+
status` shows what is there, `pnpm vitakit add section <name>` sets up a new
|
|
46
|
+
section — and shows every change before it makes it.
|
|
47
|
+
|
|
48
|
+
MIT
|
package/deployments.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** Hosting choices, comparison facts and actionable setup steps. */
|
|
2
|
+
export const DEPLOYMENTS = {
|
|
3
|
+
'github-pages': {
|
|
4
|
+
label: 'GitHub Pages',
|
|
5
|
+
description: 'Static hosting built into GitHub.',
|
|
6
|
+
details: {
|
|
7
|
+
URL: 'https://<user>.github.io/<repo>/ or a custom domain.',
|
|
8
|
+
Source: 'Public on Free; private needs GitHub Pro (personal) or Team/Enterprise (org).',
|
|
9
|
+
CI: 'GitHub Actions.',
|
|
10
|
+
},
|
|
11
|
+
setup: [
|
|
12
|
+
'In GitHub Settings → Pages, select GitHub Actions as the source.',
|
|
13
|
+
'Check .github/workflows/deploy.yml:\n- Project site: keep NUXT_APP_BASE_URL=/<repo>/.\n- User site (<user>.github.io) or root custom domain: remove that setting.',
|
|
14
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
15
|
+
],
|
|
16
|
+
notes: ['Private source needs GitHub Pro (personal) or Team/Enterprise (org).'],
|
|
17
|
+
files: ['.github/workflows/deploy.yml', '.github/workflows/cv.yml'],
|
|
18
|
+
},
|
|
19
|
+
'cloudflare-workers': {
|
|
20
|
+
label: 'Cloudflare Workers (recommended)',
|
|
21
|
+
description: 'Cloudflare’s current platform, serving static files without server code.',
|
|
22
|
+
details: {
|
|
23
|
+
URL: 'https://<worker>.<account>.workers.dev/ or a custom domain.',
|
|
24
|
+
Source: 'Private repos supported on Free; hosting/CI limits apply.',
|
|
25
|
+
CI: 'GitHub Actions.',
|
|
26
|
+
},
|
|
27
|
+
setup: [
|
|
28
|
+
'Enable a workers.dev subdomain in your Cloudflare account.',
|
|
29
|
+
'Set the Worker name in wrangler.json.',
|
|
30
|
+
'Add GitHub Actions repository secrets:\n- CLOUDFLARE_ACCOUNT_ID\n- CLOUDFLARE_API_TOKEN (Workers Scripts: Edit permission)',
|
|
31
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
32
|
+
],
|
|
33
|
+
files: ['.github/workflows/workers.yml', '.github/workflows/cv.yml', 'wrangler.json'],
|
|
34
|
+
},
|
|
35
|
+
'cloudflare': {
|
|
36
|
+
label: 'Cloudflare Pages (legacy)',
|
|
37
|
+
description: 'Legacy Cloudflare option. Still available; use Workers for new projects.',
|
|
38
|
+
details: {
|
|
39
|
+
URL: 'https://<project>.pages.dev/ or a custom domain.',
|
|
40
|
+
Source: 'Private repos supported on Free; hosting/CI limits apply.',
|
|
41
|
+
CI: 'GitHub Actions, uploading a prebuilt site (Direct Upload).',
|
|
42
|
+
},
|
|
43
|
+
setup: [
|
|
44
|
+
'Create a Cloudflare Pages Direct Upload project; production branch: main.',
|
|
45
|
+
'Set --project-name in .github/workflows/cloudflare.yml to match.',
|
|
46
|
+
'Add GitHub Actions repository secrets:\n- CLOUDFLARE_ACCOUNT_ID\n- CLOUDFLARE_API_TOKEN (Cloudflare Pages: Edit permission)',
|
|
47
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
48
|
+
],
|
|
49
|
+
files: ['.github/workflows/cloudflare.yml', '.github/workflows/cv.yml'],
|
|
50
|
+
},
|
|
51
|
+
'gitlab-pages': {
|
|
52
|
+
label: 'GitLab Pages',
|
|
53
|
+
description: 'Static hosting on GitLab.com or a university’s GitLab instance.',
|
|
54
|
+
details: {
|
|
55
|
+
URL: 'https://<project>-<id>.gitlab.io/; project paths/custom domains also work.',
|
|
56
|
+
Source: 'Private repos supported on Free; CI quotas apply.',
|
|
57
|
+
CI: 'GitLab CI/CD. Self-managed instances need GitLab 17.10+.',
|
|
58
|
+
},
|
|
59
|
+
setup: [
|
|
60
|
+
'Enable CI runners and Pages in your GitLab project.',
|
|
61
|
+
'For a public site, set Pages access to Everyone in project settings.',
|
|
62
|
+
'Check .gitlab-ci.yml: the base path comes from CI_PAGES_URL.\nFor a root custom domain, set CI/CD variable NUXT_APP_BASE_URL=/.',
|
|
63
|
+
'Commit pnpm-lock.yaml and push to the default branch.',
|
|
64
|
+
],
|
|
65
|
+
files: ['.gitlab-ci.yml'],
|
|
66
|
+
},
|
|
67
|
+
'codeberg-pages': {
|
|
68
|
+
label: 'Codeberg Pages (git-pages)',
|
|
69
|
+
description: 'Static hosting on Codeberg, powered by git-pages.',
|
|
70
|
+
details: {
|
|
71
|
+
URL: 'https://<owner>.codeberg.page/<repo>/ or a custom domain.',
|
|
72
|
+
Source: 'Hosted CI needs public, freely licensed source; private needs your own runner.',
|
|
73
|
+
CI: 'Forgejo Actions, using the automatic token; no publishing branch needed.',
|
|
74
|
+
},
|
|
75
|
+
setup: [
|
|
76
|
+
'Enable Actions in your Codeberg repository.',
|
|
77
|
+
'Choose a Docker-capable runner in .forgejo/workflows/codeberg.yml.\nUse your own runner for private source.',
|
|
78
|
+
'Check site and NUXT_APP_BASE_URL in that workflow.\nFor root/custom domains, update both; custom domains also need DNS authorization.',
|
|
79
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
80
|
+
],
|
|
81
|
+
notes: ['Codeberg-hosted runners require a public, freely licensed repository.'],
|
|
82
|
+
files: ['.forgejo/workflows/codeberg.yml'],
|
|
83
|
+
},
|
|
84
|
+
'rsync': {
|
|
85
|
+
label: 'Existing web space (SSH/rsync)',
|
|
86
|
+
description: 'Upload to university web space or a server you already control.',
|
|
87
|
+
details: {
|
|
88
|
+
URL: 'Your server’s URL, e.g. https://www.example.edu/~username/.',
|
|
89
|
+
Needs: 'SSH/rsync access and a writable web directory; not SFTP-only hosting.',
|
|
90
|
+
CI: 'Forgejo Actions, not GitHub Actions.',
|
|
91
|
+
},
|
|
92
|
+
setup: [
|
|
93
|
+
'Open .forgejo/workflows/deploy.yml.',
|
|
94
|
+
'Set the SSH user, host, destination path and NUXT_APP_BASE_URL.',
|
|
95
|
+
'Add SSH_DEPLOY_KEY as a Forgejo Actions repository secret.',
|
|
96
|
+
'Configure SSH host-key verification; the template currently disables it.',
|
|
97
|
+
'Commit pnpm-lock.yaml and push to main.',
|
|
98
|
+
],
|
|
99
|
+
notes: ['Check the destination: rsync --delete removes files absent from the build.'],
|
|
100
|
+
files: ['.forgejo/workflows/deploy.yml'],
|
|
101
|
+
},
|
|
102
|
+
'none': {
|
|
103
|
+
label: 'Decide later / deploy manually',
|
|
104
|
+
description: 'Choose a host later; nothing is published.',
|
|
105
|
+
details: {
|
|
106
|
+
Local: 'Website and LaTeX CV work as usual.',
|
|
107
|
+
CI: 'No deployment or PDF-build workflows added.',
|
|
108
|
+
},
|
|
109
|
+
setup: [
|
|
110
|
+
'When ready to publish, run pnpm check.',
|
|
111
|
+
'Upload only .output/public to a static host, not the source or cv-tex/.',
|
|
112
|
+
],
|
|
113
|
+
files: [],
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Wrap CLI prose with hanging indents, preserving explicit sublists. */
|
|
118
|
+
function wrap(text, prefix = '', width = 80) {
|
|
119
|
+
const continuation = ' '.repeat(prefix.length)
|
|
120
|
+
return text.split('\n').flatMap((paragraph, index) => {
|
|
121
|
+
const lines = []
|
|
122
|
+
let line = index === 0 ? prefix : continuation
|
|
123
|
+
for (const word of paragraph.split(/\s+/)) {
|
|
124
|
+
if (line.trim() && line.length + 1 + word.length > width) {
|
|
125
|
+
lines.push(line)
|
|
126
|
+
line = continuation
|
|
127
|
+
}
|
|
128
|
+
line += (line.trim() && line !== prefix ? ' ' : '') + word
|
|
129
|
+
}
|
|
130
|
+
lines.push(line)
|
|
131
|
+
return lines
|
|
132
|
+
}).join('\n')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function deploymentHelp() {
|
|
136
|
+
const sections = Object.values(DEPLOYMENTS).map(({ label, description, details }, index) => [
|
|
137
|
+
`${index + 1}. ${label}`,
|
|
138
|
+
wrap(description, ' '),
|
|
139
|
+
...Object.entries(details).map(([name, value]) => wrap(value, ` ${name}: `)),
|
|
140
|
+
].join('\n'))
|
|
141
|
+
return [
|
|
142
|
+
'Choosing a deployment target',
|
|
143
|
+
'Keep sensitive CV source private. Publish only .output/public.',
|
|
144
|
+
...sections,
|
|
145
|
+
'Setup steps are shown after selection.',
|
|
146
|
+
].join('\n\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function deploymentInstructions(key, packageManager = 'pnpm') {
|
|
150
|
+
const { label, setup, notes = [] } = DEPLOYMENTS[key]
|
|
151
|
+
const commandText = text => packageManager === 'npm'
|
|
152
|
+
? text.replace(/pnpm-lock\.yaml/g, 'package-lock.json').replace(/pnpm check/g, 'npm run check')
|
|
153
|
+
: text
|
|
154
|
+
return [
|
|
155
|
+
`Deployment: ${label}`,
|
|
156
|
+
...notes.map(note => wrap(note, ' ')),
|
|
157
|
+
...setup.map((step, index) => wrap(commandText(step), ` ${index + 1}. `)),
|
|
158
|
+
].join('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Keep help and invalid input in the selection loop, without losing answers. */
|
|
162
|
+
export async function chooseDeployment(ask, log = console.log) {
|
|
163
|
+
const keys = Object.keys(DEPLOYMENTS)
|
|
164
|
+
log('\nDeployment target (you can change this later):')
|
|
165
|
+
keys.forEach((key, index) => log(` ${index + 1}. ${DEPLOYMENTS[key].label}`))
|
|
166
|
+
log(' ?. Help choosing — URLs, requirements and setup')
|
|
167
|
+
|
|
168
|
+
while (true) {
|
|
169
|
+
const choice = (await ask('Choose a number, or ? for help:', '1')).trim()
|
|
170
|
+
if (/^(\?|h|help)$/i.test(choice)) {
|
|
171
|
+
log(`\n${deploymentHelp()}\n`)
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
const key = /^[1-9]\d*$/.test(choice) ? keys[Number(choice) - 1] : undefined
|
|
175
|
+
if (key) return key
|
|
176
|
+
log(`Please choose a number from 1 to ${keys.length}, or ? for help.`)
|
|
177
|
+
}
|
|
178
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pnpm create @anja.codes/vitakit [directory]
|
|
4
|
+
* npm create @anja.codes/vitakit [directory]
|
|
5
|
+
*
|
|
6
|
+
* Copies the template, substitutes the few things that are personal, picks a
|
|
7
|
+
* deployment workflow, and writes the project for the package manager that ran
|
|
8
|
+
* it. Deliberately the *only* scaffolding step vitakit has:
|
|
9
|
+
* everything afterwards is editing files in a repository you own.
|
|
10
|
+
*/
|
|
11
|
+
import { readdir } from 'node:fs/promises'
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { basename, resolve } from 'node:path'
|
|
14
|
+
import { createInterface } from 'node:readline/promises'
|
|
15
|
+
import process from 'node:process'
|
|
16
|
+
import { detectPackageManager, scaffold } from './scaffold.mjs'
|
|
17
|
+
import { chooseDeployment, deploymentHelp, deploymentInstructions } from './deployments.mjs'
|
|
18
|
+
|
|
19
|
+
function slugify(value) {
|
|
20
|
+
return value.normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
21
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'my-site'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const args = process.argv.slice(2)
|
|
26
|
+
const packageManager = detectPackageManager(process.env.npm_config_user_agent)
|
|
27
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
28
|
+
console.log(`Usage: ${packageManager} create @anja.codes/vitakit [directory]\n\n --help, -h Show this help without creating a project\n --deployment-help Explain deployment targets without creating a project\n`)
|
|
29
|
+
console.log(deploymentHelp())
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if (args.includes('--deployment-help')) {
|
|
33
|
+
console.log(deploymentHelp())
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const [directoryArgument] = args.filter(argument => !argument.startsWith('-'))
|
|
38
|
+
const interactive = process.stdin.isTTY && !process.env.VITAKIT_CREATE_DEFAULTS
|
|
39
|
+
|
|
40
|
+
const answers = {
|
|
41
|
+
name: 'Your Name',
|
|
42
|
+
title: 'Postdoctoral Researcher',
|
|
43
|
+
affiliation: 'Department of Mathematics, University of Graz',
|
|
44
|
+
email: 'you@example.org',
|
|
45
|
+
deployment: 'github-pages',
|
|
46
|
+
packageManager,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let directory = directoryArgument
|
|
50
|
+
|
|
51
|
+
if (interactive) {
|
|
52
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
53
|
+
const ask = async (question, fallback) => {
|
|
54
|
+
const answer = (await rl.question(`${question} (${fallback}) `)).trim()
|
|
55
|
+
return answer || fallback
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('\nvitakit — a website and a LaTeX CV from one set of files.\n')
|
|
59
|
+
answers.name = await ask('Your name:', answers.name)
|
|
60
|
+
answers.title = await ask('Your position:', answers.title)
|
|
61
|
+
answers.affiliation = await ask('Affiliation:', answers.affiliation)
|
|
62
|
+
answers.email = await ask('Email:', answers.email)
|
|
63
|
+
directory ??= await ask('Directory:', slugify(answers.name))
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
answers.deployment = await chooseDeployment(ask)
|
|
67
|
+
} finally {
|
|
68
|
+
rl.close()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
directory ??= slugify(answers.name)
|
|
73
|
+
answers.slug = slugify(basename(directory))
|
|
74
|
+
const target = resolve(process.cwd(), directory)
|
|
75
|
+
|
|
76
|
+
if (existsSync(target) && (await readdir(target)).length) {
|
|
77
|
+
throw new Error(`${directory} already exists and is not empty`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await scaffold(target, answers)
|
|
81
|
+
|
|
82
|
+
console.log(`\nCreated ${directory}\n`)
|
|
83
|
+
const pm = answers.packageManager
|
|
84
|
+
const run = pm === 'npm' ? 'npm run' : 'pnpm'
|
|
85
|
+
const step = (command, comment) => console.log(` ${command.padEnd(20)}${comment ? `# ${comment}` : ''}`.trimEnd())
|
|
86
|
+
step('cd ' + directory)
|
|
87
|
+
step(`${pm} install`)
|
|
88
|
+
step(`${run} dev`, 'http://localhost:3000')
|
|
89
|
+
step(`${run} pdf`, 'cv-tex/cv.pdf (needs LaTeX)')
|
|
90
|
+
step(`${run} check`, 'what CI runs')
|
|
91
|
+
console.log()
|
|
92
|
+
console.log('Edit content/ to make it yours. Keep sensitive CV source private.\n')
|
|
93
|
+
console.log(`${deploymentInstructions(answers.deployment, pm)}\n`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main().catch((error) => {
|
|
97
|
+
console.error(`\n@anja.codes/create-vitakit: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
98
|
+
process.exit(1)
|
|
99
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anja.codes/create-vitakit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a vitakit project: an academic website and a LaTeX CV from one set of files.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"vitakit",
|
|
8
|
+
"create",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"cv",
|
|
11
|
+
"academic",
|
|
12
|
+
"nuxt",
|
|
13
|
+
"latex"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://vitakit.anja.codes/guide/tutorial/",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/anja-codes/vitakit/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/anja-codes/vitakit.git",
|
|
22
|
+
"directory": "create"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"create-vitakit": "index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.mjs",
|
|
30
|
+
"scaffold.mjs",
|
|
31
|
+
"deployments.mjs",
|
|
32
|
+
"template",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.18"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/scaffold.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { basename, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { DEPLOYMENTS } from './deployments.mjs'
|
|
5
|
+
|
|
6
|
+
export { DEPLOYMENTS } from './deployments.mjs'
|
|
7
|
+
|
|
8
|
+
const TEMPLATE = fileURLToPath(new URL('template', import.meta.url))
|
|
9
|
+
|
|
10
|
+
/** Package managers a project can be written for; the first is the default. */
|
|
11
|
+
export const PACKAGE_MANAGERS = ['pnpm', 'npm']
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The package manager that started `create`, from the user agent every package
|
|
15
|
+
* manager sets for the processes it runs (`npm/11.6.0 node/v22.18.0 …`).
|
|
16
|
+
* Anything but npm (pnpm, another one, or running this file directly) gets
|
|
17
|
+
* the default.
|
|
18
|
+
*/
|
|
19
|
+
export function detectPackageManager(userAgent = '') {
|
|
20
|
+
return userAgent.startsWith('npm/') ? 'npm' : 'pnpm'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The template is written for pnpm. For npm, its commands are translated in
|
|
25
|
+
* every text file (scripts, workflows, comments, the pages' own instructions),
|
|
26
|
+
* so a project never mentions a package manager its owner does not use.
|
|
27
|
+
*/
|
|
28
|
+
function forNpm(text, scripts) {
|
|
29
|
+
return text
|
|
30
|
+
// The pnpm setup step, together with the comment lines directly above it.
|
|
31
|
+
.replace(/^(?:[ \t]*#.*\n)*[ \t]*- uses: \S*pnpm\/action-setup@\S+\n/gm, '')
|
|
32
|
+
.replace(/cache: pnpm\b/g, 'cache: npm')
|
|
33
|
+
.replace(/^[ \t]*- corepack (?:enable|install)\n/gm, '')
|
|
34
|
+
.replace(/pnpm install --frozen-lockfile/g, 'npm ci')
|
|
35
|
+
.replace(/pnpm vitakit\b/g, 'npx vitakit')
|
|
36
|
+
// `pnpm pdf --tex-only` -> `npm run pdf -- --tex-only`
|
|
37
|
+
.replace(/pnpm ([\w:-]+)((?: --?[\w-]+)*)/g, (command, script, args) =>
|
|
38
|
+
scripts.includes(script) ? `npm run ${script}${args ? ` --${args}` : ''}` : command)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** YAML placeholders represent whole scalars; prose/TeX placeholders stay raw. */
|
|
42
|
+
function fill(text, answers) {
|
|
43
|
+
const values = {
|
|
44
|
+
NAME: answers.name,
|
|
45
|
+
TITLE: answers.title,
|
|
46
|
+
AFFILIATION: answers.affiliation,
|
|
47
|
+
EMAIL: answers.email,
|
|
48
|
+
SLUG: answers.slug,
|
|
49
|
+
SUMMARY: `${answers.title} at ${answers.affiliation}, working on your research area.`,
|
|
50
|
+
}
|
|
51
|
+
// One callback pass preserves literal $ replacements and placeholder-like
|
|
52
|
+
// text in answers. JSON strings are valid quoted YAML scalars, including
|
|
53
|
+
// embedded quotes, backslashes and newlines.
|
|
54
|
+
return text.replace(/__(YAML_)?(NAME|TITLE|AFFILIATION|EMAIL|SLUG|SUMMARY)__/g,
|
|
55
|
+
(_, yaml, key) => yaml ? JSON.stringify(values[key]) : values[key])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const TEXT_FILE = /\.(md|ya?ml|json|ts|vue|css|bib|tex|svg|txt|yml|template)$/
|
|
59
|
+
const TEXT_NAMES = new Set(['package.json', '.gitignore', 'latexmkrc'])
|
|
60
|
+
|
|
61
|
+
/** Internal creator implementation, separate from the interactive CLI. */
|
|
62
|
+
export async function scaffold(target, answers) {
|
|
63
|
+
const deployment = DEPLOYMENTS[answers.deployment]
|
|
64
|
+
if (!deployment || !Object.hasOwn(DEPLOYMENTS, answers.deployment)) {
|
|
65
|
+
throw new Error(`Unknown deployment target: ${answers.deployment}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await mkdir(target, { recursive: true })
|
|
69
|
+
await cp(TEMPLATE, target, {
|
|
70
|
+
recursive: true,
|
|
71
|
+
// Finder metadata can exist in a checkout but must not reach a project.
|
|
72
|
+
filter: source => basename(source) !== '.DS_Store',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Files that cannot be shipped under their final name: `.gitignore` is
|
|
76
|
+
// excluded by npm, and `package.json` would make the template a workspace.
|
|
77
|
+
for (const [from, to] of [['.gitignore.template', '.gitignore'], ['package.json.template', 'package.json']]) {
|
|
78
|
+
await rename(join(target, from), join(target, to))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const [dir, name] of [['_github', '.github'], ['_forgejo', '.forgejo']]) {
|
|
82
|
+
const path = join(target, dir)
|
|
83
|
+
const keep = deployment.files.filter(file => file.startsWith(`${name}/workflows/`))
|
|
84
|
+
if (!keep.length) {
|
|
85
|
+
await rm(path, { recursive: true, force: true })
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const workflows = join(path, 'workflows')
|
|
89
|
+
for (const file of await readdir(workflows)) {
|
|
90
|
+
if (!keep.includes(`${name}/workflows/${file}`)) await rm(join(workflows, file))
|
|
91
|
+
}
|
|
92
|
+
await rename(path, join(target, name))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Standalone provider configuration is just as target-specific as workflows.
|
|
96
|
+
for (const [from, to] of [['gitlab-ci.yml.template', '.gitlab-ci.yml'], ['wrangler.json.template', 'wrangler.json']]) {
|
|
97
|
+
if (deployment.files.includes(to)) await rename(join(target, from), join(target, to))
|
|
98
|
+
else await rm(join(target, from))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const manifest = join(target, 'package.json')
|
|
102
|
+
const npm = answers.packageManager === 'npm'
|
|
103
|
+
const scripts = Object.keys(JSON.parse(await readFile(manifest, 'utf8')).scripts)
|
|
104
|
+
|
|
105
|
+
// Substitute placeholders in every text file, and translate commands for npm.
|
|
106
|
+
const walk = async (dir) => {
|
|
107
|
+
for (const item of await readdir(dir, { withFileTypes: true })) {
|
|
108
|
+
const path = join(dir, item.name)
|
|
109
|
+
if (item.isDirectory()) await walk(path)
|
|
110
|
+
else if (TEXT_FILE.test(item.name) || TEXT_NAMES.has(item.name)) {
|
|
111
|
+
const text = fill(await readFile(path, 'utf8'), answers)
|
|
112
|
+
await writeFile(path, npm ? forNpm(text, scripts) : text)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await walk(target)
|
|
117
|
+
|
|
118
|
+
// `packageManager` pins pnpm for Corepack and for pnpm/action-setup.
|
|
119
|
+
if (npm) {
|
|
120
|
+
const pkg = JSON.parse(await readFile(manifest, 'utf8'))
|
|
121
|
+
delete pkg.packageManager
|
|
122
|
+
await writeFile(manifest, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Nuxt
|
|
2
|
+
.output
|
|
3
|
+
.nuxt
|
|
4
|
+
.data
|
|
5
|
+
.nitro
|
|
6
|
+
.cache
|
|
7
|
+
dist
|
|
8
|
+
node_modules
|
|
9
|
+
|
|
10
|
+
# vitakit / latexmk output
|
|
11
|
+
# Everything generated lives in cv-tex/generated/ (the .cls and all auxiliary
|
|
12
|
+
# files included); only the finished PDF is written beside the sources.
|
|
13
|
+
cv-tex/generated
|
|
14
|
+
cv-tex/*.pdf
|
|
15
|
+
|
|
16
|
+
# Misc
|
|
17
|
+
*.log
|
|
18
|
+
.DS_Store
|
|
19
|
+
.env
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Builds the site and uploads only the checked output to Codeberg Pages.
|
|
2
|
+
# Codeberg Pages now uses git-pages; no pages branch or webhook is needed here.
|
|
3
|
+
# Enable Actions in repository settings. Codeberg's hosted runners require a
|
|
4
|
+
# PUBLIC, freely licensed repository. For private CV source, use your own
|
|
5
|
+
# compatible runner and change runs-on below; do not make sensitive source public.
|
|
6
|
+
# The automatic token authorizes the matching <owner>.codeberg.page/<repo>/ site.
|
|
7
|
+
# For a root site or custom domain, change BOTH NUXT_APP_BASE_URL and site below.
|
|
8
|
+
# Custom domains also need DNS authorization: https://docs.codeberg.org/codeberg-pages/using-custom-domain/
|
|
9
|
+
name: Deploy (Codeberg Pages)
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
branches: [main]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
deploy:
|
|
18
|
+
if: ${{ forgejo.ref == 'refs/heads/main' }}
|
|
19
|
+
# Use a Docker-capable runner; Nuxt builds need more than the tiny tier.
|
|
20
|
+
runs-on: codeberg-medium
|
|
21
|
+
env:
|
|
22
|
+
NUXT_APP_BASE_URL: /${{ forgejo.event.repository.name }}/
|
|
23
|
+
steps:
|
|
24
|
+
- uses: https://github.com/actions/checkout@v7
|
|
25
|
+
- uses: https://github.com/pnpm/action-setup@v6
|
|
26
|
+
- uses: https://github.com/actions/setup-node@v7
|
|
27
|
+
with:
|
|
28
|
+
node-version: 22
|
|
29
|
+
cache: pnpm
|
|
30
|
+
- run: pnpm install --frozen-lockfile
|
|
31
|
+
- run: pnpm check
|
|
32
|
+
- uses: https://codeberg.org/git-pages/action@v2
|
|
33
|
+
with:
|
|
34
|
+
site: https://${{ forgejo.repository_owner }}.codeberg.page/${{ forgejo.event.repository.name }}/
|
|
35
|
+
server: codeberg.page
|
|
36
|
+
token: ${{ forgejo.token }}
|
|
37
|
+
source: .output/public/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Builds the site and deploys it to web space via rsync.
|
|
2
|
+
#
|
|
3
|
+
# Secret: SSH_DEPLOY_KEY (private key with write access to the target).
|
|
4
|
+
# Adjust NUXT_APP_BASE_URL and the rsync target below.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
env:
|
|
14
|
+
NUXT_APP_BASE_URL: /
|
|
15
|
+
steps:
|
|
16
|
+
- uses: https://github.com/actions/checkout@v7
|
|
17
|
+
- run: apt update && apt install -y rsync
|
|
18
|
+
- uses: https://github.com/pnpm/action-setup@v6
|
|
19
|
+
# The runner image may ship an older Node than Nuxt requires.
|
|
20
|
+
- uses: https://github.com/actions/setup-node@v7
|
|
21
|
+
with:
|
|
22
|
+
node-version: 22
|
|
23
|
+
cache: pnpm
|
|
24
|
+
- run: pnpm install --frozen-lockfile
|
|
25
|
+
- run: pnpm check
|
|
26
|
+
- uses: https://github.com/webfactory/ssh-agent@v0.9.0
|
|
27
|
+
with:
|
|
28
|
+
ssh-private-key: ${{ secrets.SSH_DEPLOY_KEY }}
|
|
29
|
+
- run: |
|
|
30
|
+
rsync -e "ssh -o StrictHostKeyChecking=no" -av --delete \
|
|
31
|
+
.output/public/ user@example.org:/var/www/your-site/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Builds the site and deploys it to Cloudflare Pages.
|
|
2
|
+
#
|
|
3
|
+
# GitHub Actions builds the site and uploads only .output/public; the source
|
|
4
|
+
# repository can stay private. The site is public at <project>.pages.dev (or
|
|
5
|
+
# your custom domain). Free-tier hosting and GitHub Actions usage limits apply.
|
|
6
|
+
#
|
|
7
|
+
# Setup: create a Pages Direct Upload project with production branch `main`.
|
|
8
|
+
# Set --project-name below to its name.
|
|
9
|
+
# Secrets: CLOUDFLARE_API_TOKEN (Pages edit permission), CLOUDFLARE_ACCOUNT_ID.
|
|
10
|
+
name: Deploy (Cloudflare Pages)
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: [main]
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
deploy:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
- uses: pnpm/action-setup@v6
|
|
23
|
+
- uses: actions/setup-node@v7
|
|
24
|
+
with:
|
|
25
|
+
node-version: 22
|
|
26
|
+
cache: pnpm
|
|
27
|
+
- run: pnpm install --frozen-lockfile
|
|
28
|
+
- run: pnpm check
|
|
29
|
+
- uses: cloudflare/wrangler-action@v4
|
|
30
|
+
with:
|
|
31
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
32
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
33
|
+
command: pages deploy .output/public --project-name=__SLUG__
|