@tukuyomil032/bricklayer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # bricklayer
2
+
3
+ 🧱 Interactive TypeScript CLI project scaffolder
4
+
5
+ Quickly generate a well-structured TypeScript CLI project with best practices built-in.
6
+
7
+ ## Features
8
+
9
+ ✨ **Interactive Setup** - Guided prompts for project configuration
10
+ 📦 **Latest Packages** - Automatically fetches the latest npm package versions
11
+ 🎨 **Optional Tools** - Choose Prettier and ESLint during setup
12
+ 🪝 **Git Hooks** - Pre-configured Husky hooks with lint-staged
13
+ 🏗️ **Clean Structure** - Role-separated files and commands
14
+ ⚡ **Multiple Package Managers** - Support for pnpm, npm, yarn, and bun
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install -g bricklayer
20
+ # or
21
+ pnpm add -g bricklayer
22
+ # or
23
+ yarn global add bricklayer
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ bricklayer create
30
+ ```
31
+
32
+ Follow the interactive prompts to configure your project:
33
+
34
+ - Project name
35
+ - Module system (ESM / CommonJS)
36
+ - Package manager
37
+ - Git repository details
38
+ - Optional tools (Prettier, ESLint)
39
+
40
+ ## Generated Project Structure
41
+
42
+ ```
43
+ your-cli/
44
+ ├── src/
45
+ │ ├── commands/
46
+ │ │ └── hello.ts
47
+ │ └── index.ts
48
+ ├── .husky/
49
+ │ ├── pre-commit
50
+ │ └── pre-push
51
+ ├── .gitignore
52
+ ├── .prettierrc
53
+ ├── .prettierignore
54
+ ├── .editorconfig
55
+ ├── .npmignore
56
+ ├── eslint.config.js
57
+ ├── package.json
58
+ ├── tsconfig.json
59
+ └── README.md
60
+ ```
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ # Clone the repository
66
+ git clone https://github.com/tukuyomil032/bricklayer.git
67
+ cd bricklayer
68
+
69
+ # Install dependencies
70
+ pnpm install
71
+
72
+ # Build
73
+ pnpm run build
74
+
75
+ # Test locally
76
+ node dist/index.js create
77
+ ```
78
+
79
+ ## Scripts
80
+
81
+ - `pnpm run build` - Build the TypeScript project
82
+ - `pnpm run dev` - Run in development mode
83
+ - `pnpm run lint` - Lint the code
84
+ - `pnpm run format` - Format the code with Prettier
85
+
86
+ ## Requirements
87
+
88
+ - Node.js >= 18.0.0
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,96 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import cliProgress from 'cli-progress';
4
+ import * as templates from './templates.js';
5
+ export async function writeProjectFiles(targetDir, answers, versions) {
6
+ const progressBar = new cliProgress.SingleBar({
7
+ format: 'Creating files |{bar}| {percentage}% | {value}/{total} files',
8
+ barCompleteChar: '\u2588',
9
+ barIncompleteChar: '\u2591',
10
+ hideCursor: true,
11
+ });
12
+ const tasks = [
13
+ 'package.json',
14
+ 'tsconfig.json',
15
+ 'src/index.ts',
16
+ 'src/commands/hello.ts',
17
+ 'README.md',
18
+ '.gitignore',
19
+ // husky hooks are optional and added conditionally below
20
+ '.prettierignore',
21
+ '.npmignore',
22
+ '.editorconfig',
23
+ 'LICENSE',
24
+ ];
25
+ // Always include ESLint and Prettier config files by default
26
+ tasks.push('.prettierrc');
27
+ tasks.push('eslint.config.js');
28
+ // Add .npmrc when using pnpm
29
+ const shouldAddNpmrc = answers.packageManager === 'pnpm';
30
+ if (shouldAddNpmrc)
31
+ tasks.push('.npmrc');
32
+ // Add husky hooks entries only if requested
33
+ if (answers.useHusky) {
34
+ tasks.unshift('.husky/pre-push');
35
+ tasks.unshift('.husky/pre-commit');
36
+ }
37
+ progressBar.start(tasks.length, 0);
38
+ let completed = 0;
39
+ // Create directory structure
40
+ await fs.mkdir(targetDir, { recursive: true });
41
+ await fs.mkdir(path.join(targetDir, 'src', 'commands'), { recursive: true });
42
+ if (answers.useHusky) {
43
+ await fs.mkdir(path.join(targetDir, '.husky'), { recursive: true });
44
+ }
45
+ // Write package.json
46
+ const pkg = templates.generatePackageJson(answers, versions);
47
+ await fs.writeFile(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
48
+ progressBar.update(++completed);
49
+ // Write tsconfig.json
50
+ const tsconfig = templates.generateTsConfig(answers);
51
+ await fs.writeFile(path.join(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
52
+ progressBar.update(++completed);
53
+ // Write source files
54
+ await fs.writeFile(path.join(targetDir, 'src', 'index.ts'), templates.generateIndexTs(answers));
55
+ progressBar.update(++completed);
56
+ await fs.writeFile(path.join(targetDir, 'src', 'commands', 'hello.ts'), templates.generateHelloCommandTs());
57
+ progressBar.update(++completed);
58
+ // Write README
59
+ await fs.writeFile(path.join(targetDir, 'README.md'), templates.generateReadme(answers));
60
+ progressBar.update(++completed);
61
+ // Write .gitignore
62
+ await fs.writeFile(path.join(targetDir, '.gitignore'), templates.generateGitignore());
63
+ progressBar.update(++completed);
64
+ // Write Husky hooks (only if requested)
65
+ if (answers.useHusky) {
66
+ await fs.writeFile(path.join(targetDir, '.husky', 'pre-commit'), templates.generatePreCommitHook());
67
+ progressBar.update(++completed);
68
+ await fs.writeFile(path.join(targetDir, '.husky', 'pre-push'), templates.generatePrePushHook());
69
+ progressBar.update(++completed);
70
+ }
71
+ // Always add .prettierignore
72
+ await fs.writeFile(path.join(targetDir, '.prettierignore'), templates.generatePrettierIgnore());
73
+ progressBar.update(++completed);
74
+ // Always add .npmignore
75
+ await fs.writeFile(path.join(targetDir, '.npmignore'), templates.generateNpmIgnore());
76
+ progressBar.update(++completed);
77
+ // Conditionally add .npmrc for pnpm
78
+ if (shouldAddNpmrc) {
79
+ await fs.writeFile(path.join(targetDir, '.npmrc'), templates.generateNpmrc());
80
+ progressBar.update(++completed);
81
+ }
82
+ // Always add .editorconfig
83
+ await fs.writeFile(path.join(targetDir, '.editorconfig'), templates.generateEditorConfig());
84
+ progressBar.update(++completed);
85
+ // Write LICENSE
86
+ const licenseText = await templates.generateLicenseText(answers.license, answers.author, new Date().getFullYear());
87
+ await fs.writeFile(path.join(targetDir, 'LICENSE'), licenseText);
88
+ progressBar.update(++completed);
89
+ // Write Prettier and ESLint configs (default included)
90
+ await fs.writeFile(path.join(targetDir, '.prettierrc'), templates.generatePrettierConfig());
91
+ progressBar.update(++completed);
92
+ await fs.writeFile(path.join(targetDir, 'eslint.config.js'), templates.generateEslintConfig());
93
+ progressBar.update(++completed);
94
+ progressBar.stop();
95
+ console.log('');
96
+ }
@@ -0,0 +1,91 @@
1
+ import path from 'path';
2
+ import os from 'os';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { Command } from 'commander';
6
+ import { promptProjectDetails } from './prompts.js';
7
+ import { writeProjectFiles } from './file-writer.js';
8
+ import { installDependencies } from './installer.js';
9
+ import { getLatestVersions } from './package-versions.js';
10
+ export function createCommand() {
11
+ const cmd = new Command('create');
12
+ cmd
13
+ .description('Create a new TypeScript CLI project (interactive)')
14
+ .option('-d, --destination [path]', 'Project destination directory');
15
+ cmd.action(async (options) => {
16
+ console.log(chalk.green('Welcome to bricklayer — TypeScript CLI scaffold generator'));
17
+ // Show spinner
18
+ const initSpinner = ora('Initializing project setup...').start();
19
+ // Small delay for better UX
20
+ await new Promise((resolve) => setTimeout(resolve, 500));
21
+ initSpinner.stop();
22
+ // Determine destination behavior
23
+ const flagProvided = Boolean(options.destination);
24
+ const flagHasArg = typeof options.destination === 'string';
25
+ const askDestination = flagProvided && !flagHasArg;
26
+ // Prompt once: if -d present, always skip the `name` question; if -d without arg, also ask destination interactively first
27
+ const answers = await promptProjectDetails({ skipName: flagProvided, askDestination });
28
+ // Resolve target directory
29
+ let target;
30
+ if (flagHasArg) {
31
+ const dest = options.destination.replace(/^~/, os.homedir());
32
+ target = path.resolve(dest);
33
+ if (!answers.name)
34
+ answers.name = path.basename(target);
35
+ }
36
+ else if (answers.destination) {
37
+ const dest = answers.destination.replace(/^~/, os.homedir());
38
+ target = path.resolve(dest);
39
+ if (!answers.name)
40
+ answers.name = path.basename(target);
41
+ }
42
+ else {
43
+ const baseDir = process.cwd();
44
+ target = path.resolve(baseDir, answers.name);
45
+ }
46
+ // Fetch latest package versions
47
+ const versionSpinner = ora('Fetching latest package versions...').start();
48
+ let versions;
49
+ try {
50
+ versions = await getLatestVersions();
51
+ versionSpinner.succeed('Fetched latest package versions');
52
+ }
53
+ catch (err) {
54
+ versionSpinner.warn('Failed to fetch latest versions, using defaults');
55
+ console.debug(err);
56
+ }
57
+ const fileSpinner = ora('Creating project files...').start();
58
+ try {
59
+ await writeProjectFiles(target, answers, versions);
60
+ fileSpinner.succeed('Project scaffold created at ' + target);
61
+ // Install dependencies if user opted in
62
+ if (answers.autoInstall) {
63
+ await installDependencies(target, answers.packageManager);
64
+ }
65
+ else {
66
+ console.log(chalk.yellow('Dependencies were not installed automatically.'));
67
+ }
68
+ // Show next steps
69
+ console.log(chalk.blue('Next steps:'));
70
+ console.log(` - cd ${answers.name}`);
71
+ const buildCmd = answers.packageManager === 'pnpm'
72
+ ? 'pnpm run build'
73
+ : answers.packageManager === 'yarn'
74
+ ? 'yarn build'
75
+ : answers.packageManager === 'bun'
76
+ ? 'bun run build'
77
+ : 'npm run build';
78
+ console.log(' - Build: ' + buildCmd);
79
+ if (!answers.autoInstall) {
80
+ const installCmd = answers.packageManager === 'yarn' ? 'yarn install' : `${answers.packageManager} install`;
81
+ console.log(' - Install dependencies: ' + installCmd);
82
+ }
83
+ }
84
+ catch (err) {
85
+ fileSpinner.fail('Failed to create project');
86
+ console.error(err);
87
+ process.exit(1);
88
+ }
89
+ });
90
+ return cmd;
91
+ }
@@ -0,0 +1,167 @@
1
+ import { exec as execCb, spawn } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import ora from 'ora';
4
+ import cliProgress from 'cli-progress';
5
+ const exec = promisify(execCb);
6
+ const INSTALL_COMMANDS = {
7
+ npm: 'npm install',
8
+ pnpm: 'pnpm install',
9
+ yarn: 'yarn install',
10
+ bun: 'bun install',
11
+ };
12
+ async function isCommandAvailable(cmd) {
13
+ try {
14
+ await exec(`command -v ${cmd}`);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export async function installDependencies(targetDir, packageManager) {
22
+ const mgr = packageManager || 'pnpm';
23
+ const installCmd = INSTALL_COMMANDS[mgr] || 'pnpm install';
24
+ const spinner = ora(`Running ${installCmd}...`);
25
+ const bin = mgr;
26
+ if (!(await isCommandAvailable(bin))) {
27
+ spinner.fail(`${bin} not found on PATH.`);
28
+ // Try sensible fallback order
29
+ const fallbacks = ['pnpm', 'npm'];
30
+ let usedFallback = null;
31
+ for (const f of fallbacks) {
32
+ if (await isCommandAvailable(f)) {
33
+ usedFallback = f;
34
+ break;
35
+ }
36
+ }
37
+ if (usedFallback) {
38
+ const fallbackCmd = `${usedFallback} install`;
39
+ const fallbackSpinner = ora(`Attempting fallback: ${fallbackCmd}`);
40
+ try {
41
+ // use spawn to stream and show progress
42
+ await runCommandWithProgress(fallbackCmd, targetDir);
43
+ fallbackSpinner.succeed('Dependencies installed (fallback)');
44
+ return;
45
+ }
46
+ catch (e) {
47
+ fallbackSpinner.fail('Fallback install failed — please run manually');
48
+ console.error(e);
49
+ return;
50
+ }
51
+ }
52
+ else {
53
+ console.error(`Package manager '${bin}' not found. Please install it or run 'npm install' in ${targetDir} manually.`);
54
+ return;
55
+ }
56
+ }
57
+ try {
58
+ await runCommandWithProgress(installCmd, targetDir);
59
+ spinner.succeed('Dependencies installed');
60
+ }
61
+ catch (e) {
62
+ spinner.fail('Dependency installation failed — please run manually');
63
+ console.error(e);
64
+ }
65
+ }
66
+ function runCommandWithProgress(command, cwd) {
67
+ const parts = command.split(' ').filter(Boolean);
68
+ return new Promise((resolve, reject) => {
69
+ const bar = new cliProgress.SingleBar({
70
+ format: 'Installing dependencies |{bar}| {percentage}%',
71
+ hideCursor: true,
72
+ barsize: 40,
73
+ }, cliProgress.Presets.shades_classic);
74
+ bar.start(100, 0);
75
+ // Progress state
76
+ let progress = 0;
77
+ let lastRenderedFloor = 0;
78
+ const start = Date.now();
79
+ // Interval drives internal progress target; we only redraw when integer percent changes
80
+ let lastOutputAt = 0;
81
+ const tickInterval = 150; // ms
82
+ const maxHold = 95; // hold at 95% until process completes
83
+ const timer = setInterval(() => {
84
+ const elapsed = Date.now() - start;
85
+ // Ease-out target that slowly approaches maxHold
86
+ const target = maxHold * (1 - Math.exp(-elapsed / 6000));
87
+ // Advance progress a bit toward target, ensuring monotonic increase
88
+ // If we've recently seen installer output, move faster
89
+ const sinceOutput = lastOutputAt ? (Date.now() - lastOutputAt) : Infinity;
90
+ const speedMultiplier = sinceOutput < 1000 ? 2.0 : 1.0;
91
+ progress = Math.min(target, progress + 0.6 * speedMultiplier);
92
+ const floor = Math.floor(progress);
93
+ if (floor > lastRenderedFloor) {
94
+ lastRenderedFloor = floor;
95
+ try {
96
+ bar.update(floor);
97
+ }
98
+ catch { }
99
+ }
100
+ }, tickInterval);
101
+ const child = spawn(parts[0], parts.slice(1), { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
102
+ const stdoutChunks = [];
103
+ const stderrChunks = [];
104
+ const onOutput = () => {
105
+ lastOutputAt = Date.now();
106
+ // Aggressively advance progress toward maxHold on real installer output
107
+ const remaining = maxHold - progress;
108
+ if (remaining <= 0)
109
+ return;
110
+ // Add a chunk that's a fraction of the remaining, clamped
111
+ const advance = Math.min(remaining, Math.max(4, Math.round(remaining * 0.18)));
112
+ progress = progress + advance;
113
+ const floor = Math.floor(progress);
114
+ if (floor > lastRenderedFloor) {
115
+ lastRenderedFloor = floor;
116
+ try {
117
+ bar.update(floor);
118
+ }
119
+ catch { }
120
+ }
121
+ };
122
+ if (child.stdout)
123
+ child.stdout.on('data', (c) => { stdoutChunks.push(Buffer.from(c)); onOutput(); });
124
+ if (child.stderr)
125
+ child.stderr.on('data', (c) => { stderrChunks.push(Buffer.from(c)); onOutput(); });
126
+ child.on('error', (err) => {
127
+ clearInterval(timer);
128
+ try {
129
+ bar.stop();
130
+ }
131
+ catch { }
132
+ reject(err);
133
+ });
134
+ child.on('close', (code) => {
135
+ clearInterval(timer);
136
+ // Smoothly ramp to 100% to avoid a sudden jump
137
+ const finishInterval = 40; // ms
138
+ const finishTimer = setInterval(() => {
139
+ const remaining = 100 - progress;
140
+ if (remaining <= 0.5) {
141
+ try {
142
+ bar.update(100);
143
+ bar.stop();
144
+ }
145
+ catch { }
146
+ clearInterval(finishTimer);
147
+ if (code === 0)
148
+ return resolve();
149
+ const out = Buffer.concat(stdoutChunks).toString('utf8');
150
+ const errOut = Buffer.concat(stderrChunks).toString('utf8');
151
+ const e = new Error(`Command exited with code ${code}\n${errOut || out}`);
152
+ return reject(e);
153
+ }
154
+ // advance by a fraction of remaining to create ease-out
155
+ progress = progress + Math.max(1, Math.round(remaining * 0.18));
156
+ const floor = Math.floor(progress);
157
+ if (floor > lastRenderedFloor) {
158
+ lastRenderedFloor = floor;
159
+ try {
160
+ bar.update(floor);
161
+ }
162
+ catch { }
163
+ }
164
+ }, finishInterval);
165
+ });
166
+ });
167
+ }