@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 +92 -0
- package/dist/create/file-writer.js +96 -0
- package/dist/create/index.js +91 -0
- package/dist/create/installer.js +167 -0
- package/dist/create/licenses.js +1024 -0
- package/dist/create/package-versions.js +63 -0
- package/dist/create/prompts.js +249 -0
- package/dist/create/templates.js +410 -0
- package/dist/create/types.js +1 -0
- package/dist/index.js +16 -0
- package/dist/sample.js +11 -0
- package/package.json +81 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
export async function fetchLatestVersion(packageName) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const url = `https://registry.npmjs.org/${packageName}/latest`;
|
|
5
|
+
https
|
|
6
|
+
.get(url, (res) => {
|
|
7
|
+
let data = '';
|
|
8
|
+
res.on('data', (chunk) => (data += chunk));
|
|
9
|
+
res.on('end', () => {
|
|
10
|
+
try {
|
|
11
|
+
const json = JSON.parse(data);
|
|
12
|
+
resolve(`^${json.version}`);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
reject(e);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
})
|
|
19
|
+
.on('error', reject);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function getLatestVersions() {
|
|
23
|
+
const packages = [
|
|
24
|
+
'typescript',
|
|
25
|
+
'ts-node',
|
|
26
|
+
'husky',
|
|
27
|
+
'@types/node',
|
|
28
|
+
'eslint',
|
|
29
|
+
'eslint-config-prettier',
|
|
30
|
+
'eslint-plugin-prettier',
|
|
31
|
+
'@typescript-eslint/parser',
|
|
32
|
+
'@typescript-eslint/eslint-plugin',
|
|
33
|
+
'prettier',
|
|
34
|
+
'lint-staged',
|
|
35
|
+
'commander',
|
|
36
|
+
'inquirer',
|
|
37
|
+
'chalk',
|
|
38
|
+
'ora',
|
|
39
|
+
'yargs',
|
|
40
|
+
// include package manager packages to record their latest versions
|
|
41
|
+
'pnpm',
|
|
42
|
+
'npm',
|
|
43
|
+
'yarn',
|
|
44
|
+
'bun',
|
|
45
|
+
];
|
|
46
|
+
const versions = {};
|
|
47
|
+
try {
|
|
48
|
+
await Promise.all(packages.map(async (pkg) => {
|
|
49
|
+
try {
|
|
50
|
+
versions[pkg] = await fetchLatestVersion(pkg);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// Fallback to a reasonable default if fetch fails
|
|
54
|
+
console.warn(`Failed to fetch version for ${pkg}, using fallback:`, err);
|
|
55
|
+
versions[pkg] = 'latest';
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.warn('Failed to fetch some package versions', err);
|
|
61
|
+
}
|
|
62
|
+
return versions;
|
|
63
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import Enquirer from 'enquirer';
|
|
3
|
+
const Select = Enquirer.Select || Enquirer.default?.Select;
|
|
4
|
+
const Input = Enquirer.Input || Enquirer.default?.Input;
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import readline from 'readline';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
export async function promptProjectDetails(opts = {}) {
|
|
10
|
+
const questions = [
|
|
11
|
+
{
|
|
12
|
+
type: 'input',
|
|
13
|
+
name: 'name',
|
|
14
|
+
message: 'Project folder name (package name):',
|
|
15
|
+
default: 'my-cli',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'list',
|
|
19
|
+
name: 'moduleType',
|
|
20
|
+
message: 'Module system:',
|
|
21
|
+
choices: ['ESM', 'CommonJS'],
|
|
22
|
+
default: 'ESM',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'list',
|
|
26
|
+
name: 'packageManager',
|
|
27
|
+
message: 'Package manager to use:',
|
|
28
|
+
choices: ['npm', 'pnpm', 'yarn', 'bun'],
|
|
29
|
+
default: 'npm',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'confirm',
|
|
33
|
+
name: 'autoInstall',
|
|
34
|
+
message: 'Automatically install dependencies after scaffold? (creates lockfile)',
|
|
35
|
+
default: false,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'gitOwner',
|
|
40
|
+
message: "Git repository owner's user id:",
|
|
41
|
+
default: 'owner',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'input',
|
|
45
|
+
name: 'gitRepo',
|
|
46
|
+
message: 'Git repository name:',
|
|
47
|
+
default: 'my-cli',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'input',
|
|
51
|
+
name: 'npmPackageName',
|
|
52
|
+
message: 'npm package name (scoped or unscoped):',
|
|
53
|
+
default: (answers) => answers.name || 'my-cli',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'description',
|
|
58
|
+
message: 'Project description:',
|
|
59
|
+
default: 'A minimal TypeScript CLI',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'input',
|
|
63
|
+
name: 'author',
|
|
64
|
+
message: 'Author:',
|
|
65
|
+
default: 'Anonymous',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'list',
|
|
69
|
+
name: 'license',
|
|
70
|
+
message: 'License:',
|
|
71
|
+
choices: [
|
|
72
|
+
'MIT',
|
|
73
|
+
'Apache-2.0',
|
|
74
|
+
'GPL-3.0',
|
|
75
|
+
'GPL-2.0',
|
|
76
|
+
'BSD-1-Clause',
|
|
77
|
+
'BSD-2-Clause',
|
|
78
|
+
'BSD-3-Clause',
|
|
79
|
+
],
|
|
80
|
+
default: 'MIT',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'confirm',
|
|
84
|
+
name: 'useHusky',
|
|
85
|
+
message: 'Enable Husky git hooks (pre-commit / pre-push)?',
|
|
86
|
+
default: true,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
const totalQuestions = questions.length;
|
|
90
|
+
const answers = {};
|
|
91
|
+
console.log(''); // Blank line before prompts
|
|
92
|
+
const uiWith = inquirer;
|
|
93
|
+
const bottomBar = uiWith.ui && uiWith.ui.BottomBar ? new uiWith.ui.BottomBar() : { updateBottomBar: () => { }, close: () => { } };
|
|
94
|
+
const updateProgress = (n) => {
|
|
95
|
+
const progressLine = `Project Scaffolding Progress: [${n}/${totalQuestions}]`;
|
|
96
|
+
try {
|
|
97
|
+
bottomBar.updateBottomBar(progressLine);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
void err;
|
|
101
|
+
// fallback: write using readline
|
|
102
|
+
readline.clearLine(process.stdout, 0);
|
|
103
|
+
readline.cursorTo(process.stdout, 0);
|
|
104
|
+
process.stdout.write(progressLine + '\n');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
// Compute effective total: subtract skipped name, add destination prompt if requested
|
|
108
|
+
const effectiveTotal = totalQuestions - (opts.skipName ? 1 : 0) + (opts.askDestination ? 1 : 0);
|
|
109
|
+
let progressCount = 0;
|
|
110
|
+
updateProgress(progressCount);
|
|
111
|
+
// If askDestination is requested, prompt for it first using an interactive tree navigator
|
|
112
|
+
if (opts.askDestination) {
|
|
113
|
+
const chooseDirectoryInteractive = async (startDir) => {
|
|
114
|
+
let current = startDir;
|
|
115
|
+
while (true) {
|
|
116
|
+
// list visible directories (exclude hidden)
|
|
117
|
+
let entries = [];
|
|
118
|
+
try {
|
|
119
|
+
entries = fs.readdirSync(current).filter((name) => {
|
|
120
|
+
try {
|
|
121
|
+
return !name.startsWith('.') && fs.lstatSync(path.join(current, name)).isDirectory();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
entries = [];
|
|
130
|
+
}
|
|
131
|
+
const choices = [
|
|
132
|
+
{ display: `Select this directory: ${current}`, value: '__SELECT__' },
|
|
133
|
+
{ display: '.. (go up)', value: '__UP__' },
|
|
134
|
+
...entries.map((e) => ({ display: e + path.sep, value: e })),
|
|
135
|
+
];
|
|
136
|
+
// Add a small circle at the start of each displayed choice to indicate it's selectable
|
|
137
|
+
choices.forEach((c) => {
|
|
138
|
+
c.display = `◯ ${c.display}`;
|
|
139
|
+
});
|
|
140
|
+
// Enquirer expects choice objects with `name` (unique id) and `message` (display)
|
|
141
|
+
const select = new Select({
|
|
142
|
+
name: 'dir',
|
|
143
|
+
message: `destination: (navigate folders, Enter to choose)`,
|
|
144
|
+
// Enquirer Select returns the `name` of the chosen item.
|
|
145
|
+
// Use `name` as the internal id (value) and `message` for display.
|
|
146
|
+
choices: choices.map((c) => ({ name: c.value, message: c.display })),
|
|
147
|
+
pageSize: 15,
|
|
148
|
+
});
|
|
149
|
+
let val;
|
|
150
|
+
try {
|
|
151
|
+
val = await select.run();
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
// Enquirer may throw when TTY not available; surface a friendly error
|
|
155
|
+
console.error('Selection aborted or failed:', err instanceof Error ? err.message : String(err));
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
if (val === '__SELECT__') {
|
|
159
|
+
// Use Enquirer's Input so the default/current path is actual editable text
|
|
160
|
+
while (true) {
|
|
161
|
+
const input = new Input({
|
|
162
|
+
message: 'destination: (edit or accept)',
|
|
163
|
+
initial: current,
|
|
164
|
+
});
|
|
165
|
+
let proposed;
|
|
166
|
+
try {
|
|
167
|
+
proposed = (await input.run()).trim();
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.error('Input aborted:', err instanceof Error ? err.message : String(err));
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
const confirm = await inquirer.prompt([
|
|
174
|
+
{
|
|
175
|
+
type: 'list',
|
|
176
|
+
name: 'confirmSel',
|
|
177
|
+
message: `Create project at ${proposed}?`,
|
|
178
|
+
choices: [
|
|
179
|
+
{ name: 'Confirm and create here', value: 'confirm' },
|
|
180
|
+
{ name: 'Re-enter destination', value: 'reenter' },
|
|
181
|
+
{ name: 'Go back to folder navigation', value: 'back' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
if (confirm.confirmSel === 'confirm')
|
|
186
|
+
return proposed;
|
|
187
|
+
if (confirm.confirmSel === 'reenter')
|
|
188
|
+
continue;
|
|
189
|
+
if (confirm.confirmSel === 'back')
|
|
190
|
+
break; // return to navigation
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (val === '__UP__') {
|
|
195
|
+
const parent = path.dirname(current);
|
|
196
|
+
if (parent === current) {
|
|
197
|
+
// already root
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
current = parent;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// descend into selected subdirectory
|
|
204
|
+
current = path.join(current, val);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
// Count the destination question once and show progress before navigation
|
|
208
|
+
progressCount++;
|
|
209
|
+
updateProgress(progressCount);
|
|
210
|
+
const dest = await chooseDirectoryInteractive(process.cwd());
|
|
211
|
+
if (dest) {
|
|
212
|
+
answers.destination = dest;
|
|
213
|
+
console.log('→ Selected: ' + chalk.green(dest));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Build list of questions to ask (skip name if requested)
|
|
217
|
+
const toAsk = opts.skipName ? questions.slice(1) : questions.slice();
|
|
218
|
+
for (let i = 0; i < toAsk.length; i++) {
|
|
219
|
+
const question = toAsk[i];
|
|
220
|
+
// Update the single progress bottom bar just before each prompt
|
|
221
|
+
progressCount++;
|
|
222
|
+
updateProgress(progressCount);
|
|
223
|
+
const answer = await inquirer.prompt([question]);
|
|
224
|
+
Object.assign(answers, answer);
|
|
225
|
+
// Display selected value in color for clarity (above the bottom bar)
|
|
226
|
+
const key = question.name;
|
|
227
|
+
const val = answer[key];
|
|
228
|
+
if (typeof val === 'string') {
|
|
229
|
+
console.log('→ Selected: ' + chalk.green(val));
|
|
230
|
+
}
|
|
231
|
+
else if (Array.isArray(val)) {
|
|
232
|
+
console.log('→ Selected: ' + val.map((v) => chalk.green(v)).join(', '));
|
|
233
|
+
}
|
|
234
|
+
else if (typeof val === 'boolean') {
|
|
235
|
+
console.log('→ Selected: ' + (val ? chalk.green('yes') : chalk.yellow('no')));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Finalize progress
|
|
239
|
+
updateProgress(effectiveTotal);
|
|
240
|
+
try {
|
|
241
|
+
bottomBar.updateBottomBar(`Project Scaffolding Progress: [${effectiveTotal}/${effectiveTotal}] Done.`);
|
|
242
|
+
bottomBar.close();
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
void err;
|
|
246
|
+
}
|
|
247
|
+
console.log(''); // Blank line after completion
|
|
248
|
+
return answers;
|
|
249
|
+
}
|