@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 +65 -0
- package/package.json +51 -0
- package/src/cli/program.js +75 -0
- package/src/cli/promptHandler.js +57 -0
- package/src/index.js +19 -0
- package/src/installer/projectInstaller.js +235 -0
- package/src/tui/ascii.js +169 -0
- package/src/tui/colors.js +53 -0
- package/src/tui/renderer.js +89 -0
- package/src/tui/spinners.js +18 -0
- package/src/tui/state.js +44 -0
- package/src/utils/display.js +84 -0
- package/src/utils/logger.js +10 -0
- package/src/utils/packageManager.js +54 -0
- package/src/utils/packageManagerDetect.js +83 -0
- package/src/utils/templateDetect.js +68 -0
- package/templates.json +34 -0
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
|
+
}
|
package/src/tui/ascii.js
ADDED
|
@@ -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
|
+
}
|
package/src/tui/state.js
ADDED
|
@@ -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
|
+
}
|