@docubook/cli 0.2.1

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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @docubook/cli
2
+
3
+ Modern CLI tool for scaffolding DocuBook documentation projects.
4
+
5
+ ## Features
6
+
7
+ - ✨ **Auto-detect Package Manager** - Automatically detects npm, yarn, pnpm, or bun from invocation
8
+ - 🎨 **Modern TUI** - Beautiful terminal UI with dark theme and neon accents
9
+ - 📦 **Template Selection** - Choose from multiple documentation templates
10
+ - ⚡ **Fast Setup** - Minimal prompts, sensible defaults
11
+ - 🔧 **Fully Configurable** - Customize everything after scaffolding
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g @docubook/cli
17
+ ```
18
+
19
+ Or use directly with npx:
20
+
21
+ ```bash
22
+ npx @docubook/cli
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Create a new project
28
+
29
+ ```bash
30
+ # Using npm
31
+ npx @docubook/cli my-docs
32
+
33
+ # Using bun
34
+ bunx @docubook/cli my-docs
35
+
36
+ # Using pnpm
37
+ pnpm dlx @docubook/cli my-docs
38
+
39
+ # Using yarn
40
+ yarn dlx @docubook/cli my-docs
41
+ ```
42
+
43
+ The CLI will:
44
+ 1. Auto-detect your package manager
45
+ 2. Prompt you to select a template
46
+ 3. Create your project
47
+ 4. Install dependencies
48
+
49
+ ### Available Templates
50
+
51
+ - **Next.js (Vercel)** - Modern documentation with Next.js, Vercel deployment ready
52
+ - **React Router** - Client-side app with React Router (coming soon)
53
+
54
+ ## Package Manager Detection
55
+
56
+ The CLI automatically detects which package manager you're using:
57
+
58
+ - `npx @docubook/cli` → npm
59
+ - `bunx @docubook/cli` → bun
60
+ - `pnpm dlx @docubook/cli` → pnpm
61
+ - `yarn dlx @docubook/cli` → yarn
62
+
63
+ ## License
64
+
65
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@docubook/cli",
3
+ "version": "0.2.1",
4
+ "description": "DocuBook CLI tool that helps you initialize, update, and deploy documentation directly from your terminal.",
5
+ "type": "module",
6
+ "files": [
7
+ "src",
8
+ "README.md",
9
+ "templates.json"
10
+ ],
11
+ "bin": {
12
+ "docubook": "src/index.js"
13
+ },
14
+ "main": "./src/index.js",
15
+ "scripts": {
16
+ "dev": "node src/index.js",
17
+ "lint": "eslint src/",
18
+ "lint:fix": "eslint src/ --fix"
19
+ },
20
+ "keywords": [
21
+ "docubook",
22
+ "documentation",
23
+ "cli",
24
+ "scaffold",
25
+ "template"
26
+ ],
27
+ "homepage": "https://docubook.pro/",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/DocuBook/docubook"
31
+ },
32
+ "author": "wildan.nrs",
33
+ "author-url": "https://wildan.dev",
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "chalk": "^5.3.0",
37
+ "commander": "^12.1.0",
38
+ "ora": "^8.1.0",
39
+ "prompts": "^2.4.2",
40
+ "boxen": "^8.0.1",
41
+ "cli-progress": "^3.12.0"
42
+ },
43
+ "devDependencies": {
44
+ "eslint": "^9.39.3",
45
+ "@eslint/js": "^9.39.3",
46
+ "eslint-config-next": "^16.1.3"
47
+ },
48
+ "engines": {
49
+ "node": ">=18.0.0"
50
+ }
51
+ }
@@ -0,0 +1,75 @@
1
+ import { program } from "commander";
2
+ import { collectUserInput } from "./promptHandler.js";
3
+ import { createProject } from "../installer/projectInstaller.js";
4
+ import log from "../utils/logger.js";
5
+ import { renderWelcome, renderDone, renderError } from "../tui/renderer.js";
6
+ import { CLIState } from "../tui/state.js";
7
+ import { detectPackageManager, getPackageManagerInfo, getPackageManagerVersion } from "../utils/packageManagerDetect.js";
8
+ import { getAvailableTemplates, getTemplate, getDefaultTemplate } from "../utils/templateDetect.js";
9
+
10
+ /**
11
+ * Initializes the CLI program
12
+ * @param {string} version - Package version
13
+ */
14
+ export function initializeProgram(version) {
15
+ program
16
+ .version(version)
17
+ .description("CLI to create a new DocuBook project")
18
+ .argument("[directory]", "The name of the project directory")
19
+ .action(async (directory) => {
20
+ const state = new CLIState();
21
+
22
+ try {
23
+ // Render welcome screen with version
24
+ renderWelcome(version);
25
+ await new Promise(resolve => setTimeout(resolve, 1500));
26
+
27
+ // Auto-detect package manager
28
+ const detectedPM = detectPackageManager();
29
+ const pmVersion = getPackageManagerVersion(detectedPM);
30
+ state.setPackageManager(detectedPM);
31
+
32
+ // Get user input
33
+ const userInput = await collectUserInput(directory);
34
+ state.setProjectName(userInput.directoryName);
35
+
36
+ // Get available templates
37
+ const templates = getAvailableTemplates();
38
+ let selectedTemplate;
39
+
40
+ if (templates.length === 1) {
41
+ // Only one template, use it
42
+ selectedTemplate = templates[0].id;
43
+ state.setTemplate(templates[0].name);
44
+ } else if (templates.length > 1) {
45
+ // Multiple templates, let user choose
46
+ selectedTemplate = userInput.template || getDefaultTemplate()?.id;
47
+ const tmpl = getTemplate(selectedTemplate);
48
+ state.setTemplate(tmpl?.name || selectedTemplate);
49
+ } else {
50
+ throw new Error("No templates available");
51
+ }
52
+
53
+ // Create project with all information
54
+ await createProject({
55
+ directoryName: userInput.directoryName,
56
+ packageManager: detectedPM,
57
+ packageManagerVersion: pmVersion,
58
+ template: selectedTemplate,
59
+ autoInstall: userInput.autoInstall !== false,
60
+ docubookVersion: version,
61
+ state: state, // Pass state for TUI updates
62
+ });
63
+
64
+ // Show success message
65
+ const pmInfo = getPackageManagerInfo(detectedPM);
66
+ renderDone(userInput.directoryName, detectedPM, pmInfo.devCmd);
67
+ } catch (err) {
68
+ renderError(err.message || "An unexpected error occurred.");
69
+ log.error(err.message || "An unexpected error occurred.");
70
+ process.exit(1);
71
+ }
72
+ });
73
+
74
+ return program;
75
+ }
@@ -0,0 +1,57 @@
1
+ import prompts from "prompts";
2
+ import { getAvailableTemplates } from "../utils/templateDetect.js";
3
+ import log from "../utils/logger.js";
4
+
5
+ /**
6
+ * Collects user input for project creation
7
+ * @param {string} [cliProvidedDir] - The directory name provided via CLI argument.
8
+ * @returns {Promise<Object>} User answers
9
+ */
10
+ export async function collectUserInput(cliProvidedDir) {
11
+ let answers = {
12
+ directoryName: cliProvidedDir
13
+ };
14
+
15
+ const questions = [
16
+ {
17
+ type: cliProvidedDir ? null : "text",
18
+ name: "directoryName",
19
+ message: "What is your project named?",
20
+ initial: "my-docs",
21
+ validate: (name) => name.trim().length > 0 ? true : "Project name cannot be empty.",
22
+ },
23
+ {
24
+ type: "select",
25
+ name: "template",
26
+ message: "Select your template:",
27
+ choices: getAvailableTemplates().map(t => ({
28
+ title: `${t.name}`,
29
+ description: t.description,
30
+ value: t.id
31
+ })),
32
+ initial: 0,
33
+ },
34
+ {
35
+ type: "confirm",
36
+ name: "autoInstall",
37
+ message: "Would you like to install dependencies now?",
38
+ initial: true,
39
+ },
40
+ ];
41
+
42
+ const promptAnswers = await prompts(questions, {
43
+ onCancel: () => {
44
+ log.error("Scaffolding cancelled.");
45
+ process.exit(0);
46
+ },
47
+ });
48
+
49
+ answers = { ...answers, ...promptAnswers };
50
+
51
+ // Return all answers
52
+ return {
53
+ directoryName: answers.directoryName.trim(),
54
+ template: answers.template,
55
+ autoInstall: answers.autoInstall !== false,
56
+ };
57
+ }
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { initializeProgram } from "./cli/program.js";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // Get the directory name of the current module
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ // Read version from package.json
13
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
14
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
15
+ const VERSION = packageJson.version;
16
+
17
+ // Initialize and parse CLI arguments
18
+ const program = initializeProgram(VERSION);
19
+ program.parse(process.argv);
@@ -0,0 +1,235 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import ora from "ora";
4
+ import chalk from "chalk";
5
+ import { execSync } from "child_process";
6
+ import log from "../utils/logger.js";
7
+ import { configurePackageManager } from "../utils/packageManager.js";
8
+ import { displayManualSteps } from "../utils/display.js";
9
+ import { renderScaffolding } from "../tui/renderer.js";
10
+ import { getTemplate } from "../utils/templateDetect.js";
11
+
12
+ /**
13
+ * Creates a new DocuBook project.
14
+ * @param {Object} options - Installation options.
15
+ */
16
+ export async function createProject(options) {
17
+ const {
18
+ directoryName,
19
+ packageManager,
20
+ packageManagerVersion,
21
+ template,
22
+ autoInstall,
23
+ docubookVersion,
24
+ state
25
+ } = options;
26
+
27
+ const projectPath = path.resolve(process.cwd(), directoryName);
28
+
29
+ if (fs.existsSync(projectPath)) {
30
+ throw new Error(`Directory "${directoryName}" already exists.`);
31
+ }
32
+
33
+ log.info(`Creating a new DocuBook project in ${chalk.green(projectPath)}...`);
34
+
35
+ try {
36
+ // 1. Create project directory and get/download template
37
+ state?.setCurrentStep("Creating directories...");
38
+ renderScaffolding(state || {});
39
+
40
+ const templatePath = await getOrDownloadTemplate(template, state);
41
+
42
+ if (!templatePath || !fs.existsSync(templatePath)) {
43
+ throw new Error(`Template "${template}" could not be found or downloaded.`);
44
+ }
45
+
46
+ copyDirectoryRecursive(templatePath, projectPath);
47
+
48
+ // 2. Configure package manager specific settings
49
+ state?.setCurrentStep("Configuring package manager...");
50
+ renderScaffolding(state || {});
51
+ configurePackageManager(packageManager, projectPath);
52
+
53
+ // 3. Update package.json
54
+ state?.setCurrentStep("Updating project config...");
55
+ renderScaffolding(state || {});
56
+ const pkgPath = path.join(projectPath, "package.json");
57
+ if (fs.existsSync(pkgPath)) {
58
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
59
+ pkg.name = directoryName;
60
+ pkg.packageManager = `${packageManager}@${packageManagerVersion}`;
61
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
62
+ }
63
+
64
+ if (autoInstall) {
65
+ state?.setCurrentStep("Installing dependencies...");
66
+ renderScaffolding(state || {});
67
+ await installDependencies(directoryName, packageManager, projectPath);
68
+ }
69
+
70
+ log.info(chalk.green(`Successfully created DocuBook project v${docubookVersion}`));
71
+ } catch (err) {
72
+ // Cleanup created directory on failure
73
+ if (fs.existsSync(projectPath)) {
74
+ fs.rmSync(projectPath, { recursive: true, force: true });
75
+ }
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Gets template from local cache or downloads from GitHub
82
+ * @param {string} templateId - Template ID
83
+ * @param {Object} state - CLI state for progress updates
84
+ * @returns {Promise<string>} Path to template directory
85
+ */
86
+ async function getOrDownloadTemplate(templateId, state) {
87
+ // Try local path first (for dev environment)
88
+ const localPath = getLocalTemplatePath(templateId);
89
+ if (localPath && fs.existsSync(localPath)) {
90
+ return localPath;
91
+ }
92
+
93
+ // Download from GitHub
94
+ state?.setCurrentStep("Downloading template...");
95
+ renderScaffolding(state || {});
96
+
97
+ const templateInfo = getTemplate(templateId);
98
+ if (!templateInfo || !templateInfo.url) {
99
+ throw new Error(`Template "${templateId}" information not available.`);
100
+ }
101
+
102
+ return await downloadTemplateFromGitHub(templateId, templateInfo.url);
103
+ }
104
+
105
+ /**
106
+ * Gets local template path if it exists
107
+ * @param {string} templateId - Template ID
108
+ * @returns {string|null} Path to local template or null
109
+ */
110
+ function getLocalTemplatePath(templateId) {
111
+ import("file:///").catch(() => {});
112
+
113
+ const distPath = path.join(
114
+ new URL(".", import.meta.url).pathname,
115
+ "..",
116
+ "..",
117
+ "dist",
118
+ templateId
119
+ );
120
+
121
+ if (fs.existsSync(distPath)) {
122
+ return distPath;
123
+ }
124
+
125
+ const devPath = path.join(
126
+ new URL(".", import.meta.url).pathname,
127
+ "..",
128
+ "..",
129
+ "..",
130
+ "..",
131
+ "packages",
132
+ "template",
133
+ templateId
134
+ );
135
+
136
+ if (fs.existsSync(devPath)) {
137
+ return devPath;
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ /**
144
+ * Downloads template from GitHub repository
145
+ * @param {string} templateId - Template ID
146
+ * @param {string} gitHubUrl - GitHub URL to template
147
+ * @returns {Promise<string>} Path to downloaded template
148
+ */
149
+ async function downloadTemplateFromGitHub(templateId, gitHubUrl) {
150
+ const tempDir = fs.mkdtempSync(path.join("/tmp", "docubook-"));
151
+
152
+ try {
153
+ // Convert GitHub web URL to archive URL
154
+ // https://github.com/DocuBook/docubook/tree/main/packages/template/nextjs-vercel
155
+ // -> https://github.com/DocuBook/docubook/archive/refs/heads/main.tar.gz
156
+ const archiveUrl = "https://github.com/DocuBook/docubook/archive/refs/heads/main.tar.gz";
157
+
158
+ // Download archive
159
+ const archivePath = path.join(tempDir, "repo.tar.gz");
160
+ execSync(`curl -L -o "${archivePath}" "${archiveUrl}"`, { stdio: "pipe" });
161
+
162
+ // Extract archive
163
+ execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: "pipe" });
164
+
165
+ // Find template in extracted repo
166
+ const extractedDir = path.join(tempDir, "docubook-main", "packages", "template", templateId);
167
+
168
+ if (!fs.existsSync(extractedDir)) {
169
+ throw new Error(`Template "${templateId}" not found in repository.`);
170
+ }
171
+
172
+ return extractedDir;
173
+ } catch (err) {
174
+ fs.rmSync(tempDir, { recursive: true, force: true });
175
+ throw new Error(`Failed to download template: ${err.message}`);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Recursively copies a directory, skipping build artifacts and symlinks.
181
+ * @param {string} source - Source directory path.
182
+ * @param {string} destination - Destination directory path.
183
+ */
184
+ function copyDirectoryRecursive(source, destination, depth = 0) {
185
+ if (!fs.existsSync(destination)) {
186
+ fs.mkdirSync(destination, { recursive: true });
187
+ }
188
+
189
+ const entries = fs.readdirSync(source, { withFileTypes: true });
190
+ for (const entry of entries) {
191
+ const srcPath = path.join(source, entry.name);
192
+ const destPath = path.join(destination, entry.name);
193
+
194
+ // Skip build artifacts and node_modules at root level
195
+ if (['node_modules', '.next', '.turbo', 'dist', 'build', '.cache'].includes(entry.name) && depth === 0) {
196
+ continue;
197
+ }
198
+
199
+ // Skip symlinks and socket files
200
+ if (entry.isSymbolicLink()) {
201
+ continue;
202
+ }
203
+
204
+ if (entry.isDirectory()) {
205
+ copyDirectoryRecursive(srcPath, destPath, depth + 1);
206
+ } else if (entry.isFile()) {
207
+ try {
208
+ fs.copyFileSync(srcPath, destPath);
209
+ } catch (err) {
210
+ // Skip files that can't be copied (sockets, etc)
211
+ console.warn(`Skipped: ${entry.name} (${err.code})`);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Installs project dependencies.
219
+ * @param {string} directoryName - Project directory name.
220
+ * @param {string} packageManager - Package manager to use.
221
+ * @param {string} projectPath - Path to the project directory.
222
+ */
223
+ async function installDependencies(directoryName, packageManager, projectPath) {
224
+ log.info("Installing dependencies...");
225
+ const installSpinner = ora(`Running ${chalk.green(`${packageManager} install`)}...`).start();
226
+
227
+ try {
228
+ execSync(`${packageManager} install`, { cwd: projectPath, stdio: "ignore" });
229
+ installSpinner.succeed("Dependencies installed successfully.");
230
+ } catch {
231
+ installSpinner.fail("Failed to install dependencies.");
232
+ displayManualSteps(directoryName, packageManager);
233
+ throw new Error("Dependency installation failed.");
234
+ }
235
+ }
@@ -0,0 +1,169 @@
1
+ import { colors } from './colors.js';
2
+
3
+
4
+ const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*m/g, "");
5
+ const stringWidth = (str) => {
6
+ str = stripAnsi(str);
7
+
8
+ let width = 0;
9
+ for (const char of [...str]) {
10
+ const code = char.codePointAt(0);
11
+
12
+ // emoji / wide char
13
+ if (
14
+ code > 0xffff ||
15
+ (code >= 0x1100 &&
16
+ (
17
+ code <= 0x115f ||
18
+ code === 0x2329 ||
19
+ code === 0x232a ||
20
+ (0x2e80 <= code && code <= 0xa4cf)
21
+ ))
22
+ ) {
23
+ width += 2;
24
+ } else {
25
+ width += 1;
26
+ }
27
+ }
28
+
29
+ return width;
30
+ };
31
+
32
+ function wrapText(text, maxWidth) {
33
+ const words = text.split(" ");
34
+ const lines = [];
35
+ let current = "";
36
+
37
+ for (const word of words) {
38
+ const test = current ? current + " " + word : word;
39
+
40
+ if (stringWidth(test) <= maxWidth) {
41
+ current = test;
42
+ } else {
43
+ if (current) lines.push(current);
44
+ current = word;
45
+ }
46
+ }
47
+
48
+ if (current) lines.push(current);
49
+
50
+ return lines;
51
+ }
52
+
53
+ /**
54
+ * Create a welcome banner with full ASCII logo and tip link
55
+ */
56
+ export function createWelcomeBanner(version) {
57
+ const termWidth = process.stdout.columns || 80;
58
+ const width = Math.min(76, termWidth - 4);
59
+ const indent = " ".repeat(5)
60
+
61
+ const logoTop = `${colors.cyan}▛▀▀▜${colors.reset}`
62
+ const logoMid = `${colors.cyan}${colors.bright}▌>_▐${colors.reset}`
63
+ const logoBot = `${colors.cyan}▙▄▄▟${colors.reset}`
64
+
65
+ const title = `${colors.cyan}${colors.bright}DocuBook${colors.reset} v${version}`
66
+ const subtitle = `${colors.gray}Initialize, build, and deploy docs from terminal.${colors.reset}`
67
+
68
+ const tip = `${colors.gray}Visit our documentation.${colors.reset}`
69
+ const docsLink = `${colors.cyan}https://www.docubook.pro/${colors.reset}`
70
+
71
+ const pad = (text = "") => {
72
+ const len = stringWidth(text)
73
+ return text + " ".repeat(Math.max(0, width - len))
74
+ }
75
+
76
+ const banner = `
77
+ ${colors.cyan}┌${"─".repeat(width)}┐${colors.reset}
78
+ ${colors.cyan}│${colors.reset}${pad(` ${logoTop}`)}${colors.cyan}│${colors.reset}
79
+ ${colors.cyan}│${colors.reset}${pad(` ${logoMid} ${title}`)}${colors.cyan}│${colors.reset}
80
+ ${colors.cyan}│${colors.reset}${pad(` ${logoBot} ${subtitle}`)}${colors.cyan}│${colors.reset}
81
+ ${colors.cyan}│${colors.reset}${pad("")}${colors.cyan}│${colors.reset}
82
+ ${colors.cyan}│${colors.reset}${pad(`${indent}${tip} ${docsLink}`)}${colors.cyan}│${colors.reset}
83
+ ${colors.cyan}└${"─".repeat(width)}┘${colors.reset}
84
+ `
85
+
86
+ return banner
87
+ }
88
+
89
+ /**
90
+ * Create a banner for scaffolding progress
91
+ */
92
+ export function createScaffoldingBanner() {
93
+ return `
94
+ ${colors.cyan}Creating Project${colors.reset}
95
+ ${colors.gray}Initializing...${colors.reset}
96
+ `;
97
+ }
98
+
99
+ /**
100
+ * Create a boxed message with title and content
101
+ */
102
+ export function createBoxedMessage(title, content, color = colors.green) {
103
+ const reset = "\x1b[0m";
104
+ const termWidth = process.stdout.columns || 80;
105
+
106
+ // 1. Tentukan total lebar box (termasuk border)
107
+ const boxWidth = Math.min(80, termWidth - 4);
108
+
109
+ // 2. width adalah panjang garis horizontal (─) di atas dan bawah
110
+ // Total lebar box adalah width + 2 (untuk karakter pojok ┌ dan ┐)
111
+ const width = boxWidth - 2;
112
+
113
+ // 3. inner adalah ruang bersih di dalam box untuk teks (tanpa padding spasi)
114
+ // Kita beri padding 2 spasi di kiri dan 2 spasi di kanan (total 4)
115
+ // Jadi: 1(│) + 2(spasi) + inner + 2(spasi) + 1(│) = width + 2
116
+ const inner = (width + 2) - 6;
117
+
118
+ const centerTitle = () => {
119
+ const text = ` ${title} `;
120
+ const textWidth = stringWidth(text);
121
+ const remaining = width - textWidth;
122
+ const left = Math.floor(remaining / 2);
123
+ const right = remaining - left;
124
+ return "─".repeat(left) + text + "─".repeat(right);
125
+ };
126
+
127
+ const pad = (text = "") => {
128
+ const len = stringWidth(text);
129
+ // Tambahkan spasi hingga tepat mengisi 'inner'
130
+ return text + " ".repeat(Math.max(0, inner - len));
131
+ };
132
+
133
+ const lines = [];
134
+
135
+ // Header
136
+ lines.push(`${color}┌${centerTitle()}┐${reset}`);
137
+
138
+ // Padding atas (opsional)
139
+ lines.push(`${color}│${reset} ${pad("")} ${color}│${reset}`);
140
+
141
+ const items = typeof content === "string"
142
+ ? content.split("\n")
143
+ : Array.isArray(content) ? content : [];
144
+
145
+ for (const line of items) {
146
+ const wrapped = wrapText(line, inner);
147
+ wrapped.forEach((w) => {
148
+ // Pastikan struktur: │ + spasi(2) + konten + spasi(2) + │
149
+ lines.push(`${color}│${reset} ${pad(w)} ${color}│${reset}`);
150
+ });
151
+ }
152
+
153
+ // Padding bawah (opsional)
154
+ lines.push(`${color}│${reset} ${pad("")} ${color}│${reset}`);
155
+
156
+ // Footer
157
+ lines.push(`${color}└${"─".repeat(width)}┘${reset}`);
158
+
159
+ return "\n" + lines.join("\n") + "\n";
160
+ }
161
+
162
+ /**
163
+ * Create success banner
164
+ */
165
+ export function createSuccessBanner() {
166
+ return `
167
+ ${colors.green}✓ Project Created${colors.reset}
168
+ `;
169
+ }
@@ -0,0 +1,53 @@
1
+ // Dark theme with neon accents
2
+ export const colors = {
3
+ // Neon colors
4
+ cyan: '\x1b[36m', // #00D9FF
5
+ magenta: '\x1b[35m', // #FF00FF
6
+ yellow: '\x1b[33m', // #FFD700
7
+ green: '\x1b[32m', // #00FF00
8
+
9
+ // Grayscale
10
+ white: '\x1b[37m',
11
+ gray: '\x1b[90m',
12
+ dark: '\x1b[2m',
13
+
14
+ // Reset
15
+ reset: '\x1b[0m',
16
+
17
+ // Styles
18
+ bright: '\x1b[1m',
19
+ dim: '\x1b[2m',
20
+ };
21
+
22
+ export function colorize(text, color) {
23
+ return `${color}${text}${colors.reset}`;
24
+ }
25
+
26
+ export function line(text = '') {
27
+ if (!text) return '';
28
+ return text;
29
+ }
30
+
31
+ export function header(text) {
32
+ return colorize(text, colors.cyan + colors.bright);
33
+ }
34
+
35
+ export function success(text) {
36
+ return `${colorize('✓', colors.green)} ${text}`;
37
+ }
38
+
39
+ export function error(text) {
40
+ return `${colorize('✗', colors.magenta)} ${text}`;
41
+ }
42
+
43
+ export function info(text) {
44
+ return `${colorize('→', colors.cyan)} ${text}`;
45
+ }
46
+
47
+ export function loading(text) {
48
+ return `${colorize('◐', colors.yellow)} ${text}`;
49
+ }
50
+
51
+ export function dim(text) {
52
+ return colorize(text, colors.gray);
53
+ }
@@ -0,0 +1,89 @@
1
+ import { colors, success, info, loading, dim } from './colors.js';
2
+ import { createWelcomeBanner, createScaffoldingBanner, createSuccessBanner, createBoxedMessage } from './ascii.js';
3
+ import { readFileSync } from 'fs';
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const packageJsonPath = join(__dirname, '../../package.json');
9
+ let version = '0.1.0';
10
+
11
+ try {
12
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
13
+ version = packageJson.version || '0.1.0';
14
+ } catch {
15
+ // Fallback to default version if package.json cannot be read
16
+ }
17
+
18
+ export function renderWelcome() {
19
+ console.clear();
20
+ console.log('');
21
+ console.log(createWelcomeBanner(version));
22
+ console.log('');
23
+ }
24
+
25
+ export function renderScaffolding(state) {
26
+ console.clear();
27
+ console.log('');
28
+ console.log(createScaffoldingBanner());
29
+
30
+ if (state.projectName) {
31
+ console.log(info(`Project: ${state.projectName}`));
32
+ }
33
+
34
+ if (state.packageManager) {
35
+ console.log(success(`Package Manager: ${state.packageManager}`));
36
+ }
37
+
38
+ if (state.template) {
39
+ console.log(success(`Template: ${state.template}`));
40
+ }
41
+
42
+ if (state.currentStep) {
43
+ console.log(loading(state.currentStep));
44
+ }
45
+
46
+ console.log('');
47
+ }
48
+
49
+ export function renderDone(projectName, packageManager, nextCommand, installDependencies = true) {
50
+ console.clear();
51
+ console.log('');
52
+ console.log(createSuccessBanner());
53
+ console.log('');
54
+
55
+ // Determine install command based on package manager
56
+ const installCommand =
57
+ packageManager === 'yarn' ? 'yarn install' :
58
+ packageManager === 'pnpm' ? 'pnpm install' :
59
+ packageManager === 'bun' ? 'bun install' :
60
+ 'npm install';
61
+
62
+ const nextSteps = [
63
+ `${colors.cyan}cd${colors.reset} ${projectName}`,
64
+ ];
65
+
66
+ // Add install step only if dependencies were not already installed
67
+ if (!installDependencies) {
68
+ nextSteps.push(`${colors.cyan}${installCommand}${colors.reset}`);
69
+ }
70
+
71
+ nextSteps.push(`${colors.cyan}${nextCommand}${colors.reset}`);
72
+ nextSteps.push('');
73
+ nextSteps.push(`${dim('📚 Documentation: https://docubook.pro')}`);
74
+
75
+ console.log(createBoxedMessage('Next Steps', nextSteps, colors.green));
76
+ console.log('');
77
+ }
78
+
79
+ export function renderError(message) {
80
+ console.clear();
81
+ console.log('');
82
+ console.log(`${colors.magenta}✗ Error${colors.reset}`);
83
+ console.log(message);
84
+ console.log('');
85
+ }
86
+
87
+ export function renderBoxedMessage(title, content, color = colors.yellow) {
88
+ console.log(createBoxedMessage(title, content, color));
89
+ }
@@ -0,0 +1,18 @@
1
+ // Spinner animation frames
2
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
3
+
4
+ let currentFrame = 0;
5
+
6
+ export function getSpinner() {
7
+ const frame = spinnerFrames[currentFrame % spinnerFrames.length];
8
+ currentFrame++;
9
+ return frame;
10
+ }
11
+
12
+ export function resetSpinner() {
13
+ currentFrame = 0;
14
+ }
15
+
16
+ export function animateSpinner(text) {
17
+ return `${getSpinner()} ${text}`;
18
+ }
@@ -0,0 +1,44 @@
1
+ // State machine for CLI flow
2
+ export class CLIState {
3
+ constructor() {
4
+ this.stage = 'welcome'; // welcome → input → scaffolding → done
5
+ this.projectName = null;
6
+ this.packageManager = null;
7
+ this.template = null;
8
+ this.currentStep = null;
9
+ this.error = null;
10
+ }
11
+
12
+ setProjectName(name) {
13
+ this.projectName = name;
14
+ }
15
+
16
+ setPackageManager(pm) {
17
+ this.packageManager = pm;
18
+ }
19
+
20
+ setTemplate(template) {
21
+ this.template = template;
22
+ }
23
+
24
+ setStage(stage) {
25
+ this.stage = stage;
26
+ }
27
+
28
+ setCurrentStep(step) {
29
+ this.currentStep = step;
30
+ }
31
+
32
+ setError(error) {
33
+ this.error = error;
34
+ }
35
+
36
+ reset() {
37
+ this.stage = 'welcome';
38
+ this.projectName = null;
39
+ this.packageManager = null;
40
+ this.template = null;
41
+ this.currentStep = null;
42
+ this.error = null;
43
+ }
44
+ }
@@ -0,0 +1,84 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import cliProgress from "cli-progress";
4
+
5
+ /**
6
+ * Displays an introduction message for the CLI.
7
+ */
8
+ export function displayIntro() {
9
+ console.log(`\n${chalk.bold.green("🚀 DocuBook Installer")}\n`);
10
+ }
11
+
12
+ /**
13
+ * Displays a progress bar to simulate final setup.
14
+ * @returns {Promise<void>} Promise that resolves when simulation completes.
15
+ */
16
+ export async function simulateInstallation() {
17
+ const bar = new cliProgress.SingleBar(
18
+ {
19
+ format: `Finishing setup... ${chalk.greenBright("{bar}")} | {percentage}%`,
20
+ barCompleteChar: "\u2588",
21
+ barIncompleteChar: "\u2591",
22
+ hideCursor: true,
23
+ },
24
+ cliProgress.Presets.shades_classic
25
+ );
26
+
27
+ bar.start(100, 0);
28
+ for (let i = 0; i <= 100; i++) {
29
+ await new Promise((r) => setTimeout(r, 20)); // Faster simulation
30
+ bar.update(i);
31
+ }
32
+ bar.stop();
33
+ console.log("\n");
34
+ }
35
+
36
+ /**
37
+ * Displays manual installation steps if automatic installation fails.
38
+ * @param {string} projectDirectory - Project directory name.
39
+ * @param {string} packageManager - Package manager being used.
40
+ */
41
+ export function displayManualSteps(projectDirectory, packageManager) {
42
+ const steps = `
43
+ ${chalk.yellow("Automatic installation failed.")} Please finish setup manually:
44
+
45
+ 1. ${chalk.blueBright(`cd ${projectDirectory}`)}
46
+ 2. ${chalk.blueBright(`${packageManager} install`)}
47
+ 3. ${chalk.blueBright(`${packageManager} run dev`)}
48
+ `;
49
+ console.log(
50
+ boxen(steps, {
51
+ padding: 1,
52
+ margin: 1,
53
+ borderStyle: "round",
54
+ borderColor: "yellow",
55
+ title: "Manual Setup Required",
56
+ titleAlignment: "center",
57
+ })
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Displays next steps after successful installation.
63
+ * @param {string} directoryName - Project directory name.
64
+ * @param {string} packageManager - Package manager being used.
65
+ */
66
+ export function displayNextSteps(directoryName, packageManager) {
67
+ const steps = `
68
+ ${chalk.bold("Next steps:")}
69
+
70
+ 1. ${chalk.blueBright(`cd ${directoryName}`)}
71
+ 2. ${chalk.blueBright(`${packageManager} run dev`)}
72
+ `;
73
+
74
+ console.log(
75
+ boxen(steps, {
76
+ padding: 1,
77
+ margin: 1,
78
+ borderStyle: "round",
79
+ borderColor: "green",
80
+ title: "Success!",
81
+ titleAlignment: "center",
82
+ })
83
+ );
84
+ }
@@ -0,0 +1,10 @@
1
+ import chalk from "chalk";
2
+
3
+ const log = {
4
+ info: (msg) => console.log(`${chalk.blue("ℹ")} ${msg}`),
5
+ success: (msg) => console.log(`${chalk.green("✔")} ${msg}`),
6
+ warn: (msg) => console.log(`${chalk.yellow("⚠")} ${msg}`),
7
+ error: (msg) => console.log(`\n${chalk.red("✖")} ${chalk.bold(msg)}\n`),
8
+ };
9
+
10
+ export default log;
@@ -0,0 +1,54 @@
1
+ import { execSync } from "child_process";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ /**
6
+ * Gets the version of the specified package manager
7
+ * @param {string} pm - Package manager name
8
+ * @returns {string|null} Version string or null if not installed
9
+ */
10
+ export function getPackageManagerVersion(pm) {
11
+ try {
12
+ return execSync(`${pm} --version`).toString().trim();
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Detects the default package manager from user environment
20
+ * @returns {string} Default package manager name
21
+ */
22
+ export function detectDefaultPackageManager() {
23
+ const userAgent = process.env.npm_config_user_agent || "";
24
+ if (userAgent.includes("pnpm")) return "pnpm";
25
+ if (userAgent.includes("yarn")) return "yarn";
26
+ if (userAgent.includes("bun")) return "bun";
27
+ return "npm";
28
+ }
29
+
30
+ /**
31
+ * Updates postcss config file extension for Bun compatibility
32
+ * @param {string} projectPath - Path to the project directory
33
+ */
34
+ export function updatePostcssConfig(projectPath) {
35
+ const oldPath = path.join(projectPath, "postcss.config.js");
36
+ const newPath = path.join(projectPath, "postcss.config.cjs");
37
+ if (fs.existsSync(oldPath)) {
38
+ fs.renameSync(oldPath, newPath);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Configures package manager specific settings
44
+ * @param {string} packageManager - Package manager name
45
+ * @param {string} projectPath - Path to the project directory
46
+ */
47
+ export function configurePackageManager(packageManager, projectPath) {
48
+ if (packageManager === "bun") {
49
+ updatePostcssConfig(projectPath);
50
+ } else if (packageManager === "yarn") {
51
+ const yarnrcPath = path.join(projectPath, ".yarnrc.yml");
52
+ fs.writeFileSync(yarnrcPath, "nodeLinker: node-modules\n");
53
+ }
54
+ }
@@ -0,0 +1,83 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ // Map argv[1] patterns to package managers
4
+ const PM_DETECTION_MAP = {
5
+ 'npx': 'npm',
6
+ 'npm': 'npm',
7
+ 'bunx': 'bun',
8
+ 'bun': 'bun',
9
+ 'pnpm': 'pnpm',
10
+ 'yarn': 'yarn',
11
+ };
12
+
13
+ // Environment variable detection
14
+ function detectFromEnv() {
15
+ const userAgent = process.env.npm_config_user_agent;
16
+ if (!userAgent) return null;
17
+
18
+ if (userAgent.includes('bun')) return 'bun';
19
+ if (userAgent.includes('yarn')) return 'yarn';
20
+ if (userAgent.includes('pnpm')) return 'pnpm';
21
+ if (userAgent.includes('npm')) return 'npm';
22
+
23
+ return null;
24
+ }
25
+
26
+ // Detect from process argv (how the CLI was invoked)
27
+ function detectFromArgv() {
28
+ // process.argv[1] contains the command that was used
29
+ const argv1 = process.argv[1] || '';
30
+
31
+ for (const [pattern, pm] of Object.entries(PM_DETECTION_MAP)) {
32
+ if (argv1.includes(pattern)) {
33
+ return pm;
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ // Get package manager version
41
+ export function getPackageManagerVersion(pm) {
42
+ try {
43
+ const version = execSync(`${pm} --version`, { encoding: 'utf-8' }).trim();
44
+ return version;
45
+ } catch {
46
+ return 'unknown';
47
+ }
48
+ }
49
+
50
+ // Detect package manager
51
+ export function detectPackageManager() {
52
+ // Priority: argv > env > default
53
+ const detected = detectFromArgv() || detectFromEnv();
54
+ return detected || 'npm';
55
+ }
56
+
57
+ // Get package manager info
58
+ export function getPackageManagerInfo(pm) {
59
+ const info = {
60
+ npm: {
61
+ name: 'npm',
62
+ installCmd: 'npm install',
63
+ devCmd: 'npm run dev',
64
+ },
65
+ yarn: {
66
+ name: 'yarn',
67
+ installCmd: 'yarn install',
68
+ devCmd: 'yarn dev',
69
+ },
70
+ pnpm: {
71
+ name: 'pnpm',
72
+ installCmd: 'pnpm install',
73
+ devCmd: 'pnpm dev',
74
+ },
75
+ bun: {
76
+ name: 'bun',
77
+ installCmd: 'bun install',
78
+ devCmd: 'bun run dev',
79
+ },
80
+ };
81
+
82
+ return info[pm] || info.npm;
83
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ // Get list of available templates
9
+ export function getAvailableTemplates() {
10
+ // Try to load from templates.json first (works in published package and dev)
11
+ try {
12
+ const templatesJsonPath = path.join(__dirname, '..', '..', 'templates.json');
13
+ if (fs.existsSync(templatesJsonPath)) {
14
+ const content = fs.readFileSync(templatesJsonPath, 'utf-8');
15
+ const data = JSON.parse(content);
16
+ return data.templates || [];
17
+ }
18
+ } catch (err) {
19
+ // Fallback to directory scanning
20
+ }
21
+
22
+ // Fallback: scan template directories
23
+ const distPath = path.join(__dirname, '..', '..', 'dist');
24
+ const templateDir = fs.existsSync(distPath)
25
+ ? distPath
26
+ : path.join(__dirname, '..', '..', '..', '..', 'packages', 'template');
27
+
28
+ if (!fs.existsSync(templateDir)) {
29
+ return [];
30
+ }
31
+
32
+ const entries = fs.readdirSync(templateDir, { withFileTypes: true });
33
+ const templates = [];
34
+
35
+ for (const entry of entries) {
36
+ if (!entry.isDirectory()) continue;
37
+
38
+ const configPath = path.join(templateDir, entry.name, 'template.config.json');
39
+
40
+ if (!fs.existsSync(configPath)) continue;
41
+
42
+ try {
43
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
44
+ templates.push({
45
+ id: config.id || entry.name,
46
+ name: config.name || entry.name,
47
+ description: config.description,
48
+ features: config.features,
49
+ });
50
+ } catch {
51
+ console.error(`Failed to read template config for ${entry.name}`);
52
+ }
53
+ }
54
+
55
+ return templates;
56
+ }
57
+
58
+ // Get template by id
59
+ export function getTemplate(templateId) {
60
+ const templates = getAvailableTemplates();
61
+ return templates.find(t => t.id === templateId);
62
+ }
63
+
64
+ // Get first available template (for default)
65
+ export function getDefaultTemplate() {
66
+ const templates = getAvailableTemplates();
67
+ return templates.length > 0 ? templates[0] : null;
68
+ }
package/templates.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "templates": [
3
+ {
4
+ "id": "nextjs-vercel",
5
+ "name": "nextjs-vercel",
6
+ "description": "Modern documentation with Next.js and Vercel deployment",
7
+ "features": [
8
+ "Next.js 16",
9
+ "React 19",
10
+ "TypeScript",
11
+ "Tailwind CSS",
12
+ "MDX Support",
13
+ "Dark Mode",
14
+ "Search (Algolia)",
15
+ "Responsive Design"
16
+ ],
17
+ "url": "https://github.com/DocuBook/docubook/tree/main/packages/template/nextjs-vercel"
18
+ },
19
+ {
20
+ "id": "react-router",
21
+ "name": "react-router",
22
+ "description": "Client-side documentation with React Router",
23
+ "features": [
24
+ "React 19",
25
+ "React Router v6",
26
+ "TypeScript",
27
+ "Tailwind CSS",
28
+ "Client-side routing",
29
+ "Responsive Design"
30
+ ],
31
+ "url": "https://github.com/DocuBook/docubook/tree/main/packages/template/react-router"
32
+ }
33
+ ]
34
+ }