@tukuyomil032/bricklayer 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,17 +16,33 @@ Quickly generate a well-structured TypeScript CLI project with best practices bu
16
16
  ## Installation
17
17
 
18
18
  ```bash
19
- npm install -g bricklayer
19
+ npm install -g @tukuyomil032/bricklayer
20
+
21
+ # or
22
+ pnpm add -g @tukuyomil032/bricklayer
23
+
20
24
  # or
21
- pnpm add -g bricklayer
25
+ yarn global add @tukuyomil032/bricklayer
26
+
22
27
  # or
23
- yarn global add bricklayer
28
+ bun add -g @tukuyomil032/bricklayer
24
29
  ```
25
30
 
26
31
  ## Usage
27
32
 
28
33
  ```bash
34
+ # When treating the current directory during command execution as the project's root folder:
29
35
  bricklayer create
36
+
37
+ # If you want to specify the project's root folder yourself (we generally recommend using this option):
38
+ # Use the arrow keys (up and down) and the Enter key to navigate to the project's root folder.
39
+ bricklayer create -d
40
+
41
+ # You can also specify a project folder directly by entering a relative path after the `-d` option.
42
+ bricklayer create -d ~/Documents/dev/CLI/my-test-project
43
+
44
+ # available aliases
45
+ bl create
30
46
  ```
31
47
 
32
48
  Follow the interactive prompts to configure your project:
@@ -34,8 +50,10 @@ Follow the interactive prompts to configure your project:
34
50
  - Project name
35
51
  - Module system (ESM / CommonJS)
36
52
  - Package manager
53
+ - Automatically install dependencies(create lockfile)
37
54
  - Git repository details
38
55
  - Optional tools (Prettier, ESLint)
56
+ - husky(pre-commit, pre-push)
39
57
 
40
58
  ## Generated Project Structure
41
59
 
@@ -45,7 +63,7 @@ your-cli/
45
63
  │ ├── commands/
46
64
  │ │ └── hello.ts
47
65
  │ └── index.ts
48
- ├── .husky/
66
+ ├── .husky/ #options
49
67
  │ ├── pre-commit
50
68
  │ └── pre-push
51
69
  ├── .gitignore
@@ -16,77 +16,62 @@ export async function writeProjectFiles(targetDir, answers, versions) {
16
16
  'src/commands/hello.ts',
17
17
  'README.md',
18
18
  '.gitignore',
19
- // husky hooks are optional and added conditionally below
20
19
  '.prettierignore',
21
20
  '.npmignore',
22
21
  '.editorconfig',
23
22
  'LICENSE',
24
23
  ];
25
- // Always include ESLint and Prettier config files by default
26
24
  tasks.push('.prettierrc');
27
25
  tasks.push('eslint.config.js');
28
26
  // Add .npmrc when using pnpm
29
27
  const shouldAddNpmrc = answers.packageManager === 'pnpm';
30
- if (shouldAddNpmrc)
28
+ if (shouldAddNpmrc) {
31
29
  tasks.push('.npmrc');
32
- // Add husky hooks entries only if requested
30
+ }
33
31
  if (answers.useHusky) {
34
32
  tasks.unshift('.husky/pre-push');
35
33
  tasks.unshift('.husky/pre-commit');
36
34
  }
37
35
  progressBar.start(tasks.length, 0);
38
36
  let completed = 0;
39
- // Create directory structure
40
37
  await fs.mkdir(targetDir, { recursive: true });
41
38
  await fs.mkdir(path.join(targetDir, 'src', 'commands'), { recursive: true });
42
39
  if (answers.useHusky) {
43
40
  await fs.mkdir(path.join(targetDir, '.husky'), { recursive: true });
44
41
  }
45
- // Write package.json
46
42
  const pkg = templates.generatePackageJson(answers, versions);
47
43
  await fs.writeFile(path.join(targetDir, 'package.json'), JSON.stringify(pkg, null, 2));
48
44
  progressBar.update(++completed);
49
- // Write tsconfig.json
50
45
  const tsconfig = templates.generateTsConfig(answers);
51
46
  await fs.writeFile(path.join(targetDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
52
47
  progressBar.update(++completed);
53
- // Write source files
54
48
  await fs.writeFile(path.join(targetDir, 'src', 'index.ts'), templates.generateIndexTs(answers));
55
49
  progressBar.update(++completed);
56
50
  await fs.writeFile(path.join(targetDir, 'src', 'commands', 'hello.ts'), templates.generateHelloCommandTs());
57
51
  progressBar.update(++completed);
58
- // Write README
59
52
  await fs.writeFile(path.join(targetDir, 'README.md'), templates.generateReadme(answers));
60
53
  progressBar.update(++completed);
61
- // Write .gitignore
62
54
  await fs.writeFile(path.join(targetDir, '.gitignore'), templates.generateGitignore());
63
55
  progressBar.update(++completed);
64
- // Write Husky hooks (only if requested)
65
56
  if (answers.useHusky) {
66
57
  await fs.writeFile(path.join(targetDir, '.husky', 'pre-commit'), templates.generatePreCommitHook());
67
58
  progressBar.update(++completed);
68
59
  await fs.writeFile(path.join(targetDir, '.husky', 'pre-push'), templates.generatePrePushHook());
69
60
  progressBar.update(++completed);
70
61
  }
71
- // Always add .prettierignore
72
62
  await fs.writeFile(path.join(targetDir, '.prettierignore'), templates.generatePrettierIgnore());
73
63
  progressBar.update(++completed);
74
- // Always add .npmignore
75
64
  await fs.writeFile(path.join(targetDir, '.npmignore'), templates.generateNpmIgnore());
76
65
  progressBar.update(++completed);
77
- // Conditionally add .npmrc for pnpm
78
66
  if (shouldAddNpmrc) {
79
67
  await fs.writeFile(path.join(targetDir, '.npmrc'), templates.generateNpmrc());
80
68
  progressBar.update(++completed);
81
69
  }
82
- // Always add .editorconfig
83
70
  await fs.writeFile(path.join(targetDir, '.editorconfig'), templates.generateEditorConfig());
84
71
  progressBar.update(++completed);
85
- // Write LICENSE
86
72
  const licenseText = await templates.generateLicenseText(answers.license, answers.author, new Date().getFullYear());
87
73
  await fs.writeFile(path.join(targetDir, 'LICENSE'), licenseText);
88
74
  progressBar.update(++completed);
89
- // Write Prettier and ESLint configs (default included)
90
75
  await fs.writeFile(path.join(targetDir, '.prettierrc'), templates.generatePrettierConfig());
91
76
  progressBar.update(++completed);
92
77
  await fs.writeFile(path.join(targetDir, 'eslint.config.js'), templates.generateEslintConfig());
@@ -14,36 +14,32 @@ export function createCommand() {
14
14
  .option('-d, --destination [path]', 'Project destination directory');
15
15
  cmd.action(async (options) => {
16
16
  console.log(chalk.green('Welcome to bricklayer — TypeScript CLI scaffold generator'));
17
- // Show spinner
18
17
  const initSpinner = ora('Initializing project setup...').start();
19
- // Small delay for better UX
20
18
  await new Promise((resolve) => setTimeout(resolve, 500));
21
19
  initSpinner.stop();
22
- // Determine destination behavior
23
20
  const flagProvided = Boolean(options.destination);
24
21
  const flagHasArg = typeof options.destination === 'string';
25
22
  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
23
  const answers = await promptProjectDetails({ skipName: flagProvided, askDestination });
28
- // Resolve target directory
29
24
  let target;
30
25
  if (flagHasArg) {
31
26
  const dest = options.destination.replace(/^~/, os.homedir());
32
27
  target = path.resolve(dest);
33
- if (!answers.name)
28
+ if (!answers.name) {
34
29
  answers.name = path.basename(target);
30
+ }
35
31
  }
36
32
  else if (answers.destination) {
37
33
  const dest = answers.destination.replace(/^~/, os.homedir());
38
34
  target = path.resolve(dest);
39
- if (!answers.name)
35
+ if (!answers.name) {
40
36
  answers.name = path.basename(target);
37
+ }
41
38
  }
42
39
  else {
43
40
  const baseDir = process.cwd();
44
41
  target = path.resolve(baseDir, answers.name);
45
42
  }
46
- // Fetch latest package versions
47
43
  const versionSpinner = ora('Fetching latest package versions...').start();
48
44
  let versions;
49
45
  try {
@@ -58,14 +54,12 @@ export function createCommand() {
58
54
  try {
59
55
  await writeProjectFiles(target, answers, versions);
60
56
  fileSpinner.succeed('Project scaffold created at ' + target);
61
- // Install dependencies if user opted in
62
57
  if (answers.autoInstall) {
63
58
  await installDependencies(target, answers.packageManager);
64
59
  }
65
60
  else {
66
61
  console.log(chalk.yellow('Dependencies were not installed automatically.'));
67
62
  }
68
- // Show next steps
69
63
  console.log(chalk.blue('Next steps:'));
70
64
  console.log(` - cd ${answers.name}`);
71
65
  const buildCmd = answers.packageManager === 'pnpm'
@@ -25,7 +25,6 @@ export async function installDependencies(targetDir, packageManager) {
25
25
  const bin = mgr;
26
26
  if (!(await isCommandAvailable(bin))) {
27
27
  spinner.fail(`${bin} not found on PATH.`);
28
- // Try sensible fallback order
29
28
  const fallbacks = ['pnpm', 'npm'];
30
29
  let usedFallback = null;
31
30
  for (const f of fallbacks) {
@@ -72,20 +71,15 @@ function runCommandWithProgress(command, cwd) {
72
71
  barsize: 40,
73
72
  }, cliProgress.Presets.shades_classic);
74
73
  bar.start(100, 0);
75
- // Progress state
76
74
  let progress = 0;
77
75
  let lastRenderedFloor = 0;
78
76
  const start = Date.now();
79
- // Interval drives internal progress target; we only redraw when integer percent changes
80
77
  let lastOutputAt = 0;
81
- const tickInterval = 150; // ms
82
- const maxHold = 95; // hold at 95% until process completes
78
+ const tickInterval = 150;
79
+ const maxHold = 95;
83
80
  const timer = setInterval(() => {
84
81
  const elapsed = Date.now() - start;
85
- // Ease-out target that slowly approaches maxHold
86
82
  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
83
  const sinceOutput = lastOutputAt ? Date.now() - lastOutputAt : Infinity;
90
84
  const speedMultiplier = sinceOutput < 1000 ? 2.0 : 1.0;
91
85
  progress = Math.min(target, progress + 0.6 * speedMultiplier);
@@ -103,11 +97,10 @@ function runCommandWithProgress(command, cwd) {
103
97
  const stderrChunks = [];
104
98
  const onOutput = () => {
105
99
  lastOutputAt = Date.now();
106
- // Aggressively advance progress toward maxHold on real installer output
107
100
  const remaining = maxHold - progress;
108
- if (remaining <= 0)
101
+ if (remaining <= 0) {
109
102
  return;
110
- // Add a chunk that's a fraction of the remaining, clamped
103
+ }
111
104
  const advance = Math.min(remaining, Math.max(4, Math.round(remaining * 0.18)));
112
105
  progress = progress + advance;
113
106
  const floor = Math.floor(progress);
@@ -139,7 +132,6 @@ function runCommandWithProgress(command, cwd) {
139
132
  });
140
133
  child.on('close', (code) => {
141
134
  clearInterval(timer);
142
- // Smoothly ramp to 100% to avoid a sudden jump
143
135
  const finishInterval = 40; // ms
144
136
  const finishTimer = setInterval(() => {
145
137
  const remaining = 100 - progress;
@@ -150,14 +142,14 @@ function runCommandWithProgress(command, cwd) {
150
142
  }
151
143
  catch { }
152
144
  clearInterval(finishTimer);
153
- if (code === 0)
145
+ if (code === 0) {
154
146
  return resolve();
147
+ }
155
148
  const out = Buffer.concat(stdoutChunks).toString('utf8');
156
149
  const errOut = Buffer.concat(stderrChunks).toString('utf8');
157
150
  const e = new Error(`Command exited with code ${code}\n${errOut || out}`);
158
151
  return reject(e);
159
152
  }
160
- // advance by a fraction of remaining to create ease-out
161
153
  progress = progress + Math.max(1, Math.round(remaining * 0.18));
162
154
  const floor = Math.floor(progress);
163
155
  if (floor > lastRenderedFloor) {
@@ -37,7 +37,6 @@ export async function getLatestVersions() {
37
37
  'chalk',
38
38
  'ora',
39
39
  'yargs',
40
- // include package manager packages to record their latest versions
41
40
  'pnpm',
42
41
  'npm',
43
42
  'yarn',
@@ -50,7 +49,6 @@ export async function getLatestVersions() {
50
49
  versions[pkg] = await fetchLatestVersion(pkg);
51
50
  }
52
51
  catch (err) {
53
- // Fallback to a reasonable default if fetch fails
54
52
  console.warn(`Failed to fetch version for ${pkg}, using fallback:`, err);
55
53
  versions[pkg] = 'latest';
56
54
  }
@@ -88,7 +88,7 @@ export async function promptProjectDetails(opts = {}) {
88
88
  ];
89
89
  const totalQuestions = questions.length;
90
90
  const answers = {};
91
- console.log(''); // Blank line before prompts
91
+ console.log('');
92
92
  const uiWith = inquirer;
93
93
  const bottomBar = uiWith.ui && uiWith.ui.BottomBar
94
94
  ? new uiWith.ui.BottomBar()
@@ -100,22 +100,18 @@ export async function promptProjectDetails(opts = {}) {
100
100
  }
101
101
  catch (err) {
102
102
  void err;
103
- // fallback: write using readline
104
103
  readline.clearLine(process.stdout, 0);
105
104
  readline.cursorTo(process.stdout, 0);
106
105
  process.stdout.write(progressLine + '\n');
107
106
  }
108
107
  };
109
- // Compute effective total: subtract skipped name, add destination prompt if requested
110
108
  const effectiveTotal = totalQuestions - (opts.skipName ? 1 : 0) + (opts.askDestination ? 1 : 0);
111
109
  let progressCount = 0;
112
110
  updateProgress(progressCount);
113
- // If askDestination is requested, prompt for it first using an interactive tree navigator
114
111
  if (opts.askDestination) {
115
112
  const chooseDirectoryInteractive = async (startDir) => {
116
113
  let current = startDir;
117
114
  while (true) {
118
- // list visible directories (exclude hidden)
119
115
  let entries = [];
120
116
  try {
121
117
  entries = fs.readdirSync(current).filter((name) => {
@@ -135,16 +131,12 @@ export async function promptProjectDetails(opts = {}) {
135
131
  { display: '.. (go up)', value: '__UP__' },
136
132
  ...entries.map((e) => ({ display: e + path.sep, value: e })),
137
133
  ];
138
- // Add a small circle at the start of each displayed choice to indicate it's selectable
139
134
  choices.forEach((c) => {
140
135
  c.display = `◯ ${c.display}`;
141
136
  });
142
- // Enquirer expects choice objects with `name` (unique id) and `message` (display)
143
137
  const select = new Select({
144
138
  name: 'dir',
145
139
  message: `destination: (navigate folders, Enter to choose)`,
146
- // Enquirer Select returns the `name` of the chosen item.
147
- // Use `name` as the internal id (value) and `message` for display.
148
140
  choices: choices.map((c) => ({ name: c.value, message: c.display })),
149
141
  pageSize: 15,
150
142
  });
@@ -153,12 +145,10 @@ export async function promptProjectDetails(opts = {}) {
153
145
  val = await select.run();
154
146
  }
155
147
  catch (err) {
156
- // Enquirer may throw when TTY not available; surface a friendly error
157
148
  console.error('Selection aborted or failed:', err instanceof Error ? err.message : String(err));
158
149
  throw err;
159
150
  }
160
151
  if (val === '__SELECT__') {
161
- // Use Enquirer's Input so the default/current path is actual editable text
162
152
  while (true) {
163
153
  const input = new Input({
164
154
  message: 'destination: (edit or accept)',
@@ -184,29 +174,29 @@ export async function promptProjectDetails(opts = {}) {
184
174
  ],
185
175
  },
186
176
  ]);
187
- if (confirm.confirmSel === 'confirm')
177
+ if (confirm.confirmSel === 'confirm') {
188
178
  return proposed;
189
- if (confirm.confirmSel === 'reenter')
179
+ }
180
+ if (confirm.confirmSel === 'reenter') {
190
181
  continue;
191
- if (confirm.confirmSel === 'back')
192
- break; // return to navigation
182
+ }
183
+ if (confirm.confirmSel === 'back') {
184
+ break;
185
+ }
193
186
  }
194
187
  continue;
195
188
  }
196
189
  if (val === '__UP__') {
197
190
  const parent = path.dirname(current);
198
191
  if (parent === current) {
199
- // already root
200
192
  continue;
201
193
  }
202
194
  current = parent;
203
195
  continue;
204
196
  }
205
- // descend into selected subdirectory
206
197
  current = path.join(current, val);
207
198
  }
208
199
  };
209
- // Count the destination question once and show progress before navigation
210
200
  progressCount++;
211
201
  updateProgress(progressCount);
212
202
  const dest = await chooseDirectoryInteractive(process.cwd());
@@ -215,16 +205,13 @@ export async function promptProjectDetails(opts = {}) {
215
205
  console.log('→ Selected: ' + chalk.green(dest));
216
206
  }
217
207
  }
218
- // Build list of questions to ask (skip name if requested)
219
208
  const toAsk = opts.skipName ? questions.slice(1) : questions.slice();
220
209
  for (let i = 0; i < toAsk.length; i++) {
221
210
  const question = toAsk[i];
222
- // Update the single progress bottom bar just before each prompt
223
211
  progressCount++;
224
212
  updateProgress(progressCount);
225
213
  const answer = await inquirer.prompt([question]);
226
214
  Object.assign(answers, answer);
227
- // Display selected value in color for clarity (above the bottom bar)
228
215
  const key = question.name;
229
216
  const val = answer[key];
230
217
  if (typeof val === 'string') {
@@ -237,7 +224,6 @@ export async function promptProjectDetails(opts = {}) {
237
224
  console.log('→ Selected: ' + (val ? chalk.green('yes') : chalk.yellow('no')));
238
225
  }
239
226
  }
240
- // Finalize progress
241
227
  updateProgress(effectiveTotal);
242
228
  try {
243
229
  bottomBar.updateBottomBar(`Project Scaffolding Progress: [${effectiveTotal}/${effectiveTotal}] Done.`);
@@ -246,6 +232,6 @@ export async function promptProjectDetails(opts = {}) {
246
232
  catch (err) {
247
233
  void err;
248
234
  }
249
- console.log(''); // Blank line after completion
235
+ console.log('');
250
236
  return answers;
251
237
  }
@@ -208,9 +208,8 @@ export function generatePackageJson(answers, versions) {
208
208
  return 'bun run build';
209
209
  return `${m} run build`;
210
210
  }
211
- const prepareScript = answers.useHusky
212
- ? `husky install && ${buildCmdForManager(mgr)}`
213
- : buildCmdForManager(mgr);
211
+ // When Husky is enabled, keep prepare as just 'husky' (user requested).
212
+ const prepareScript = answers.useHusky ? 'husky' : buildCmdForManager(mgr);
214
213
  return {
215
214
  name: answers.npmPackageName || answers.name,
216
215
  private: false,
@@ -232,6 +231,7 @@ export function generatePackageJson(answers, versions) {
232
231
  typecheck: 'tsc --noEmit',
233
232
  lint: 'eslint "src/**/*.ts"',
234
233
  'lint:fix': 'eslint "src/**/*.ts" --fix',
234
+ 'lint-staged': 'lint-staged',
235
235
  format: 'prettier --write "src/**/*.ts"',
236
236
  'format:check': 'prettier --check "src/**/*.ts"',
237
237
  }, answers.useHusky ? {} : {}),
@@ -355,26 +355,41 @@ export function generateEslintConfig() {
355
355
  import globals from 'globals';
356
356
  import tseslint from 'typescript-eslint';
357
357
  import json from '@eslint/json';
358
- import { defineConfig } from 'eslint/config';
359
358
 
360
- export default defineConfig({
361
- ignores: ['dist/'],
362
- overrides: [
363
- {
364
- files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
365
- plugins: { js },
366
- extends: ['js/recommended'],
367
- languageOptions: { globals: globals.node },
359
+ export default [
360
+ {
361
+ ignores: ['dist/**'],
362
+ },
363
+
364
+ {
365
+ files: ['**/*.{js,mjs,cjs}'],
366
+ languageOptions: {
367
+ globals: globals.node,
368
+ },
369
+ ...js.configs.recommended,
370
+ },
371
+
372
+ {
373
+ files: ['**/*.{ts,mts,cts}'],
374
+ languageOptions: {
375
+ globals: globals.node,
376
+ parser: tseslint.parser,
368
377
  },
369
- Object.assign({ files: ['**/*.{ts,mts,cts}'] }, tseslint.configs.recommended),
370
- {
371
- files: ['**/*.json'],
372
- plugins: { json },
373
- language: 'json/json',
374
- extends: ['json/recommended'],
378
+ plugins: {
379
+ '@typescript-eslint': tseslint.plugin,
375
380
  },
376
- ],
377
- });
381
+ rules: {
382
+ ...tseslint.configs.recommended.rules,
383
+ },
384
+ },
385
+
386
+ {
387
+ files: ['**/*.json'],
388
+ language: 'json/json',
389
+ plugins: { json },
390
+ ...json.configs.recommended,
391
+ },
392
+ ];
378
393
  `;
379
394
  }
380
395
  export async function generateLicenseText(license, author, year) {
@@ -402,4 +417,3 @@ export async function generateLicenseText(license, author, year) {
402
417
  .replace(fullnameRegex, author)
403
418
  .replace(nameRegex, author);
404
419
  }
405
- // Note: .eslintignore generation removed in favor of eslint.config.js ignores
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { Command } from 'commander';
3
2
  import { createCommand } from './create/index.js';
4
3
  import { sampleCommand } from './sample.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tukuyomil032/bricklayer",
3
3
  "private": false,
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "description": "Interactive TypeScript CLI project scaffolder",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",