@karaoke-cms/create 0.6.1 → 0.6.2
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/package.json +13 -3
- package/src/index.js +169 -0
- package/src/templates.js +184 -0
package/package.json
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karaoke-cms/create",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.2",
|
|
5
5
|
"description": "Scaffold a new karaoke-cms project",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-karaoke-cms": "./src/index.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22.12.0"
|
|
14
|
+
},
|
|
9
15
|
"keywords": [
|
|
10
16
|
"astro",
|
|
11
17
|
"cms",
|
|
12
18
|
"create",
|
|
13
|
-
"scaffold"
|
|
19
|
+
"scaffold",
|
|
20
|
+
"obsidian"
|
|
14
21
|
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"vitest": "^4.1.1"
|
|
24
|
+
},
|
|
15
25
|
"scripts": {
|
|
16
|
-
"test": "
|
|
26
|
+
"test": "vitest run test/templates.test.js"
|
|
17
27
|
}
|
|
18
28
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @karaoke-cms/create — scaffold a new karaoke-cms project.
|
|
4
|
+
* Usage: npm create @karaoke-cms@latest [project-dir]
|
|
5
|
+
*
|
|
6
|
+
* Zero runtime dependencies — uses Node.js built-ins only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createInterface } from 'readline';
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
11
|
+
import { join, resolve } from 'path';
|
|
12
|
+
import {
|
|
13
|
+
packageJson, astroConfig, karaokeConfig, contentConfig,
|
|
14
|
+
envDts, tsConfig, gitignore, cloudflareRedirects,
|
|
15
|
+
helloWorld, draftPost, gettingStarted,
|
|
16
|
+
} from './templates.js';
|
|
17
|
+
|
|
18
|
+
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
19
|
+
const R = '\x1b[0m';
|
|
20
|
+
const BOLD = '\x1b[1m';
|
|
21
|
+
const GREEN = '\x1b[32m';
|
|
22
|
+
const CYAN = '\x1b[36m';
|
|
23
|
+
const GRAY = '\x1b[90m';
|
|
24
|
+
const RED = '\x1b[31m';
|
|
25
|
+
|
|
26
|
+
// Use create package's own version as the @karaoke-cms/astro dep range
|
|
27
|
+
// (packages ship in lockstep)
|
|
28
|
+
const { version: astroVersion } = JSON.parse(
|
|
29
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Separate input (readline) from output (process.stdout) so piped stdin works
|
|
33
|
+
// correctly. Buffer all 'line' events into a queue so that lines emitted before
|
|
34
|
+
// the next nextLine() call are never dropped (this happens with piped input
|
|
35
|
+
// because readline reads the entire pipe before the first await completes).
|
|
36
|
+
const rl = createInterface({ input: process.stdin });
|
|
37
|
+
const lineQueue = [];
|
|
38
|
+
let lineWaiter = null;
|
|
39
|
+
let inputClosed = false;
|
|
40
|
+
|
|
41
|
+
rl.on('line', line => {
|
|
42
|
+
if (lineWaiter) {
|
|
43
|
+
const resolve = lineWaiter;
|
|
44
|
+
lineWaiter = null;
|
|
45
|
+
resolve(line);
|
|
46
|
+
} else {
|
|
47
|
+
lineQueue.push(line);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
rl.on('close', () => {
|
|
52
|
+
inputClosed = true;
|
|
53
|
+
if (lineWaiter) { lineWaiter(''); lineWaiter = null; }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function nextLine() {
|
|
57
|
+
if (lineQueue.length > 0) return Promise.resolve(lineQueue.shift());
|
|
58
|
+
if (inputClosed) return Promise.resolve('');
|
|
59
|
+
return new Promise(resolve => { lineWaiter = resolve; });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function ask(question, defaultValue = '') {
|
|
63
|
+
const hint = defaultValue ? ` ${GRAY}(${defaultValue})${R}` : '';
|
|
64
|
+
process.stdout.write(`${CYAN}?${R} ${question}${hint} › `);
|
|
65
|
+
const ans = await nextLine();
|
|
66
|
+
return ans.trim() || defaultValue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function askYesNo(question, defaultYes = true) {
|
|
70
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
71
|
+
process.stdout.write(`${CYAN}?${R} ${question} ${GRAY}(${hint})${R} › `);
|
|
72
|
+
const ans = await nextLine();
|
|
73
|
+
return ans.trim() ? ans.trim().toLowerCase().startsWith('y') : defaultYes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function askChoice(question, choices, defaultIndex = 0) {
|
|
77
|
+
console.log(`${CYAN}?${R} ${question}`);
|
|
78
|
+
choices.forEach((c, i) => {
|
|
79
|
+
const marker = i === defaultIndex ? `${CYAN}›${R}` : ' ';
|
|
80
|
+
console.log(` ${marker} ${i + 1}) ${c}`);
|
|
81
|
+
});
|
|
82
|
+
process.stdout.write(` ${GRAY}Enter number (default ${defaultIndex + 1})${R} › `);
|
|
83
|
+
const ans = await nextLine();
|
|
84
|
+
const n = parseInt(ans.trim()) - 1;
|
|
85
|
+
return choices[n >= 0 && n < choices.length ? n : defaultIndex];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
89
|
+
async function main() {
|
|
90
|
+
console.log(`\n${BOLD}Create a new karaoke-cms project${R}\n`);
|
|
91
|
+
|
|
92
|
+
// Project directory (can be passed as first CLI arg)
|
|
93
|
+
let dir = process.argv[2]?.replace(/^-+/, '') || '';
|
|
94
|
+
if (!dir) dir = await ask('Project directory', 'my-cms');
|
|
95
|
+
|
|
96
|
+
const targetDir = resolve(process.cwd(), dir);
|
|
97
|
+
|
|
98
|
+
if (existsSync(targetDir)) {
|
|
99
|
+
console.error(`\n${RED}✗${R} Directory already exists: ${BOLD}${targetDir}${R}`);
|
|
100
|
+
rl.close();
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Derive a readable default title from the directory name
|
|
105
|
+
const defaultTitle = dir
|
|
106
|
+
.replace(/[-_]/g, ' ')
|
|
107
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
108
|
+
|
|
109
|
+
const title = await ask('Site title', defaultTitle);
|
|
110
|
+
const siteUrl = await ask('Site URL', 'https://my-site.pages.dev');
|
|
111
|
+
const description = await ask('Description', 'Our team knowledge base.');
|
|
112
|
+
const theme = await askChoice('Theme', ['default', 'minimal'], 0);
|
|
113
|
+
const search = await askYesNo('Enable search? (Pagefind)', true);
|
|
114
|
+
const commentsEnabled = await askYesNo('Enable comments? (requires Giscus setup)', false);
|
|
115
|
+
|
|
116
|
+
let comments = null;
|
|
117
|
+
if (commentsEnabled) {
|
|
118
|
+
console.log(`\n ${GRAY}Get these values from giscus.app after enabling Discussions on your repo.${R}\n`);
|
|
119
|
+
const repo = await ask('GitHub repo (owner/repo)');
|
|
120
|
+
const repoId = await ask('Repo ID');
|
|
121
|
+
const category = await ask('Discussion category', 'General');
|
|
122
|
+
const categoryId = await ask('Category ID');
|
|
123
|
+
comments = { repo, repoId, category, categoryId };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
rl.close();
|
|
127
|
+
|
|
128
|
+
// ── Scaffold ────────────────────────────────────────────────────────────────
|
|
129
|
+
console.log(`\n Scaffolding ${BOLD}${dir}${R}...\n`);
|
|
130
|
+
|
|
131
|
+
const date = new Date().toISOString().split('T')[0];
|
|
132
|
+
|
|
133
|
+
mkdirSync(join(targetDir, 'src'), { recursive: true });
|
|
134
|
+
mkdirSync(join(targetDir, 'content/blog'), { recursive: true });
|
|
135
|
+
mkdirSync(join(targetDir, 'content/docs'), { recursive: true });
|
|
136
|
+
mkdirSync(join(targetDir, 'public'), { recursive: true });
|
|
137
|
+
|
|
138
|
+
const files = {
|
|
139
|
+
'package.json': packageJson({ name: dir, astroVersion }),
|
|
140
|
+
'astro.config.mjs': astroConfig({ siteUrl }),
|
|
141
|
+
'karaoke.config.ts': karaokeConfig({ title, description, theme, search, comments }),
|
|
142
|
+
'src/content.config.ts': contentConfig(),
|
|
143
|
+
'src/env.d.ts': envDts(),
|
|
144
|
+
'tsconfig.json': tsConfig(),
|
|
145
|
+
'.gitignore': gitignore(),
|
|
146
|
+
'public/_redirects': cloudflareRedirects(),
|
|
147
|
+
'content/blog/hello-world.md': helloWorld({ date }),
|
|
148
|
+
'content/blog/draft-post.md': draftPost({ date }),
|
|
149
|
+
'content/docs/getting-started.md': gettingStarted({ date }),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
for (const [file, content] of Object.entries(files)) {
|
|
153
|
+
writeFileSync(join(targetDir, file), content);
|
|
154
|
+
console.log(` ${GREEN}+${R} ${file}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Done ────────────────────────────────────────────────────────────────────
|
|
158
|
+
console.log(`\n${GREEN}✓${R} Done! Created ${BOLD}${dir}/${R}\n`);
|
|
159
|
+
console.log(` Next steps:\n`);
|
|
160
|
+
console.log(` ${CYAN}cd ${dir}${R}`);
|
|
161
|
+
console.log(` ${CYAN}npm install${R}`);
|
|
162
|
+
console.log(` ${CYAN}npm run dev${R} ${GRAY}→ http://localhost:4321${R}\n`);
|
|
163
|
+
console.log(` ${GRAY}Open ${BOLD}${dir}/${R}${GRAY} in Obsidian as a vault to write content.${R}\n`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
main().catch(err => {
|
|
167
|
+
console.error(`\n${RED}✗${R} ${err.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
package/src/templates.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure template functions for karaoke-cms project scaffolding.
|
|
3
|
+
* Each returns a file's content as a string.
|
|
4
|
+
* Exported separately so they're independently testable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {{ name: string, astroVersion: string }} opts
|
|
9
|
+
*/
|
|
10
|
+
export function packageJson({ name, astroVersion }) {
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
name,
|
|
13
|
+
private: true,
|
|
14
|
+
type: 'module',
|
|
15
|
+
engines: { node: '>=22.12.0' },
|
|
16
|
+
scripts: {
|
|
17
|
+
dev: 'astro dev',
|
|
18
|
+
build: 'astro build',
|
|
19
|
+
preview: 'astro preview',
|
|
20
|
+
},
|
|
21
|
+
dependencies: {
|
|
22
|
+
'@karaoke-cms/astro': `^${astroVersion}`,
|
|
23
|
+
astro: '^6.0.0',
|
|
24
|
+
},
|
|
25
|
+
}, null, 2) + '\n';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {{ siteUrl: string }} opts
|
|
30
|
+
*/
|
|
31
|
+
export function astroConfig({ siteUrl }) {
|
|
32
|
+
return `// @ts-check
|
|
33
|
+
import { defineConfig } from 'astro/config';
|
|
34
|
+
import karaoke from '@karaoke-cms/astro';
|
|
35
|
+
|
|
36
|
+
let karaokeConfig = {};
|
|
37
|
+
try {
|
|
38
|
+
karaokeConfig = (await import('./karaoke.config.ts')).default;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default defineConfig({
|
|
44
|
+
site: ${JSON.stringify(siteUrl)},
|
|
45
|
+
integrations: [karaoke(karaokeConfig)],
|
|
46
|
+
});
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {{
|
|
52
|
+
* title: string,
|
|
53
|
+
* description: string,
|
|
54
|
+
* theme: string,
|
|
55
|
+
* search: boolean,
|
|
56
|
+
* comments: { repo: string, repoId: string, category: string, categoryId: string } | null,
|
|
57
|
+
* }} opts
|
|
58
|
+
*/
|
|
59
|
+
export function karaokeConfig({ title, description, theme, search, comments }) {
|
|
60
|
+
const modules = {};
|
|
61
|
+
if (search) modules.search = { enabled: true };
|
|
62
|
+
if (comments) modules.comments = { enabled: true, ...comments };
|
|
63
|
+
|
|
64
|
+
const modulesStr = Object.keys(modules).length > 0
|
|
65
|
+
? `\n modules: ${JSON.stringify(modules, null, 2).replace(/\n/g, '\n ')},`
|
|
66
|
+
: '';
|
|
67
|
+
|
|
68
|
+
return `import type { KaraokeConfig } from '@karaoke-cms/astro';
|
|
69
|
+
|
|
70
|
+
const config: KaraokeConfig = {
|
|
71
|
+
title: ${JSON.stringify(title)},
|
|
72
|
+
description: ${JSON.stringify(description)},
|
|
73
|
+
theme: ${JSON.stringify(theme)},${modulesStr}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default config;
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function contentConfig() {
|
|
81
|
+
return `import { makeCollections } from '@karaoke-cms/astro/collections';
|
|
82
|
+
|
|
83
|
+
export const collections = makeCollections(new URL('..', import.meta.url));
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function envDts() {
|
|
88
|
+
return `/// <reference types="astro/client" />
|
|
89
|
+
/// <reference types="@karaoke-cms/astro/client" />
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function tsConfig() {
|
|
94
|
+
return JSON.stringify(
|
|
95
|
+
{ extends: 'astro/tsconfigs/strict', include: ['.astro/types.d.ts', '**/*'], exclude: ['dist'] },
|
|
96
|
+
null, 2
|
|
97
|
+
) + '\n';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function gitignore() {
|
|
101
|
+
return `.DS_Store
|
|
102
|
+
node_modules/
|
|
103
|
+
dist/
|
|
104
|
+
.astro/
|
|
105
|
+
.env
|
|
106
|
+
.env.*
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function cloudflareRedirects() {
|
|
111
|
+
return `/* /404.html 404\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {{ date: string }} opts */
|
|
115
|
+
export function helloWorld({ date }) {
|
|
116
|
+
return `---
|
|
117
|
+
title: "Hello World"
|
|
118
|
+
publish: true
|
|
119
|
+
date: ${date}
|
|
120
|
+
author: "The Team"
|
|
121
|
+
description: "Our first published post."
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
Welcome! This post has \`publish: true\` so it appears on the site.
|
|
125
|
+
|
|
126
|
+
## How publishing works
|
|
127
|
+
|
|
128
|
+
1. Write Markdown in Obsidian (or any editor)
|
|
129
|
+
2. Add \`publish: true\` to the frontmatter when ready to share
|
|
130
|
+
3. Push to \`main\` — GitHub Actions builds and deploys automatically
|
|
131
|
+
|
|
132
|
+
Posts without \`publish: true\` stay private in your vault.
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @param {{ date: string }} opts */
|
|
137
|
+
export function draftPost({ date }) {
|
|
138
|
+
return `---
|
|
139
|
+
title: "Draft Post"
|
|
140
|
+
publish: false
|
|
141
|
+
date: ${date}
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
This post has \`publish: false\`. It lives in your vault but never appears on the site.
|
|
145
|
+
|
|
146
|
+
Change it to \`publish: true\` when you're ready to share it.
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @param {{ date: string }} opts */
|
|
151
|
+
export function gettingStarted({ date }) {
|
|
152
|
+
return `---
|
|
153
|
+
title: "Getting Started"
|
|
154
|
+
publish: true
|
|
155
|
+
date: ${date}
|
|
156
|
+
description: "How to set up and use your karaoke-cms site."
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Setup
|
|
160
|
+
|
|
161
|
+
1. Open this folder in Obsidian as a vault
|
|
162
|
+
2. Edit \`karaoke.config.ts\` — set your title, theme, and modules
|
|
163
|
+
3. Set your Cloudflare Pages URL in \`astro.config.mjs\`
|
|
164
|
+
4. Push to \`main\` — your site is live
|
|
165
|
+
|
|
166
|
+
## Writing content
|
|
167
|
+
|
|
168
|
+
- Blog posts go in \`content/blog/\`
|
|
169
|
+
- Documentation goes in \`content/docs/\`
|
|
170
|
+
- Set \`publish: true\` to make a file public
|
|
171
|
+
|
|
172
|
+
## Frontmatter reference
|
|
173
|
+
|
|
174
|
+
\`\`\`yaml
|
|
175
|
+
---
|
|
176
|
+
title: "My Post" # required
|
|
177
|
+
publish: true # required to appear on site
|
|
178
|
+
date: 2026-01-15 # optional, YYYY-MM-DD
|
|
179
|
+
author: "Name" # optional
|
|
180
|
+
description: "..." # optional, used in OG tags and RSS
|
|
181
|
+
---
|
|
182
|
+
\`\`\`
|
|
183
|
+
`;
|
|
184
|
+
}
|