create-hsi-app 0.2.0 → 0.5.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 CHANGED
@@ -28,8 +28,5 @@ pnpm create hsi-app@latest
28
28
  bun create hsi-app@latest
29
29
  ```
30
30
 
31
- Pass the package manager flag after the initializer:
32
-
33
- ```bash
34
- npm create hsi-app@latest -- --pnpm
35
- ```
31
+ Full CLI usage, flags, and repo/install behavior:
32
+ [docs/create-hsi-app.md](https://github.com/Hsiii/frontend-template/blob/main/docs/create-hsi-app.md)
@@ -8,58 +8,96 @@ import {
8
8
  writeFileSync,
9
9
  } from 'node:fs';
10
10
  import { basename, join, resolve } from 'node:path';
11
+ import { stdin as input, stdout as output } from 'node:process';
12
+ import readline from 'node:readline/promises';
11
13
 
12
14
  const templateRepo = 'https://github.com/Hsiii/frontend-template.git';
13
- const templateTag = 'v0.2.0';
15
+ const templateTag = 'v0.5.0';
14
16
  const defaultAppName = 'my-app';
15
17
  const packageManagers = ['bun', 'npm', 'pnpm', 'yarn'];
16
18
  const rawArgs = process.argv.slice(2);
17
- const selectedPackageManager = parsePackageManagerFlag(rawArgs);
19
+ const selectedPackageManager = resolvePackageManager(rawArgs);
20
+ const shouldInstallDependencies = !rawArgs.includes('--noInstall');
21
+ const shouldSkipRepoSetup = rawArgs.includes('--noRepo');
22
+ const isInteractive = input.isTTY && output.isTTY;
18
23
  const targetArg = rawArgs.find((arg) => !arg.startsWith('--')) ?? '.';
19
24
  const targetPath = resolve(targetArg);
20
25
  const appName = toPackageName(basename(targetPath));
21
26
 
22
- if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
23
- fail(`Target directory is not empty: ${targetPath}`);
24
- }
27
+ main().catch((error) => {
28
+ fail(error.message);
29
+ });
30
+
31
+ async function main() {
32
+ if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
33
+ fail(`Target directory is not empty: ${targetPath}`);
34
+ }
35
+
36
+ run('git', [
37
+ '-c',
38
+ 'advice.detachedHead=false',
39
+ 'clone',
40
+ '--branch',
41
+ templateTag,
42
+ '--depth',
43
+ '1',
44
+ templateRepo,
45
+ targetPath,
46
+ ]);
47
+
48
+ rmSync(join(targetPath, '.git'), { force: true, recursive: true });
49
+ rmSync(join(targetPath, '.github'), { force: true, recursive: true });
50
+ rmSync(join(targetPath, 'docs'), { force: true, recursive: true });
51
+ rmSync(join(targetPath, 'packages'), { force: true, recursive: true });
52
+ rmSync(join(targetPath, 'scripts'), { force: true, recursive: true });
53
+
54
+ updatePackageJson();
55
+ updateBunLock();
56
+ updateAppText();
57
+ updatePackageManagerFiles();
58
+ writeAppReadme();
59
+
60
+ if (shouldInstallDependencies) {
61
+ installDependencies();
62
+ }
25
63
 
26
- run('git', [
27
- '-c',
28
- 'advice.detachedHead=false',
29
- 'clone',
30
- '--branch',
31
- templateTag,
32
- '--depth',
33
- '1',
34
- templateRepo,
35
- targetPath,
36
- ]);
37
-
38
- rmSync(join(targetPath, '.git'), { force: true, recursive: true });
39
- rmSync(join(targetPath, '.github'), { force: true, recursive: true });
40
- rmSync(join(targetPath, 'docs'), { force: true, recursive: true });
41
- rmSync(join(targetPath, 'packages'), { force: true, recursive: true });
42
- rmSync(join(targetPath, 'scripts'), { force: true, recursive: true });
43
-
44
- updatePackageJson();
45
- updateBunLock();
46
- updateAppText();
47
- updatePackageManagerFiles();
48
- writeAppReadme();
49
-
50
- console.log(`\nCreated ${appName} in ${targetPath}\n`);
51
- console.log('Next steps:');
52
- if (targetArg !== '.') {
53
- console.log(` cd ${targetArg}`);
64
+ const repoSetup = await maybeSetupRepo();
65
+
66
+ console.log(`\nCreated ${appName} in ${targetPath}\n`);
67
+ if (repoSetup === 'github') {
68
+ console.log(
69
+ 'Created a local git repository and configured GitHub origin.'
70
+ );
71
+ } else if (repoSetup === 'local') {
72
+ console.log('Initialized a local git repository.');
73
+ }
74
+ if (shouldInstallDependencies) {
75
+ console.log(`Installed dependencies with ${selectedPackageManager}.`);
76
+ }
77
+ console.log('\nNext steps:');
78
+ if (targetArg !== '.') {
79
+ console.log(` cd ${targetArg}`);
80
+ }
81
+ if (!shouldInstallDependencies) {
82
+ console.log(` ${installCommand()}`);
83
+ }
84
+ console.log(` ${devCommand()}`);
54
85
  }
55
- console.log(` ${installCommand()}`);
56
- console.log(` ${devCommand()}`);
57
86
 
58
- function run(command, args) {
87
+ function run(command, args, options = {}) {
59
88
  try {
60
- execFileSync(command, args, { stdio: 'inherit' });
61
- } catch {
62
- fail(`Failed to run: ${command} ${args.join(' ')}`);
89
+ return execFileSync(command, args, {
90
+ cwd: options.cwd,
91
+ encoding: options.capture ? 'utf8' : undefined,
92
+ stdio: options.capture ? 'pipe' : 'inherit',
93
+ });
94
+ } catch (error) {
95
+ if (options.allowFailure) {
96
+ return null;
97
+ }
98
+
99
+ const details = error.stderr?.toString().trim() || error.message;
100
+ fail(`Failed to run: ${command} ${args.join(' ')}\n${details}`);
63
101
  }
64
102
  }
65
103
 
@@ -151,6 +189,25 @@ function updatePackageManagerFiles() {
151
189
  }
152
190
  }
153
191
 
192
+ function installDependencies() {
193
+ switch (selectedPackageManager) {
194
+ case 'bun':
195
+ run('bun', ['install'], { cwd: targetPath });
196
+ return;
197
+ case 'npm':
198
+ run('npm', ['install'], { cwd: targetPath });
199
+ return;
200
+ case 'pnpm':
201
+ run('pnpm', ['install'], { cwd: targetPath });
202
+ return;
203
+ case 'yarn':
204
+ run('yarn', ['install'], { cwd: targetPath });
205
+ return;
206
+ default:
207
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
208
+ }
209
+ }
210
+
154
211
  function writeAppReadme() {
155
212
  const installLine = installCommand();
156
213
  const devLine = devCommand();
@@ -184,6 +241,133 @@ ${securityNote}
184
241
  writeFileSync(join(targetPath, 'README.md'), readme);
185
242
  }
186
243
 
244
+ async function maybeSetupRepo() {
245
+ if (shouldSkipRepoSetup || !isInteractive) {
246
+ return null;
247
+ }
248
+
249
+ const rl = readline.createInterface({ input, output });
250
+
251
+ try {
252
+ const shouldCreateRepo = await promptYesNo(
253
+ rl,
254
+ 'Create a git repository?',
255
+ true
256
+ );
257
+
258
+ if (!shouldCreateRepo) {
259
+ return null;
260
+ }
261
+
262
+ initLocalRepo();
263
+
264
+ if (!canUseGitHubCli()) {
265
+ return 'local';
266
+ }
267
+
268
+ const defaultRepoName = basename(targetPath);
269
+ const repoName = await promptWithDefault(
270
+ rl,
271
+ 'Repository name',
272
+ defaultRepoName
273
+ );
274
+ const visibility = await promptChoice(rl, 'Visibility', [
275
+ { label: 'private', value: 'private', default: true },
276
+ { label: 'public', value: 'public' },
277
+ ]);
278
+
279
+ run(
280
+ 'gh',
281
+ [
282
+ 'repo',
283
+ 'create',
284
+ repoName,
285
+ `--${visibility}`,
286
+ '--source=.',
287
+ '--remote=origin',
288
+ ],
289
+ { cwd: targetPath }
290
+ );
291
+
292
+ return 'github';
293
+ } finally {
294
+ rl.close();
295
+ }
296
+ }
297
+
298
+ function initLocalRepo() {
299
+ run('git', ['init', '-b', 'main'], { cwd: targetPath });
300
+ run('git', ['config', 'core.hooksPath', '.githooks'], { cwd: targetPath });
301
+ }
302
+
303
+ function canUseGitHubCli() {
304
+ return Boolean(
305
+ run('gh', ['auth', 'status'], {
306
+ cwd: targetPath,
307
+ capture: true,
308
+ allowFailure: true,
309
+ })
310
+ );
311
+ }
312
+
313
+ async function promptYesNo(rl, label, defaultValue) {
314
+ const hint = defaultValue ? 'Y/n' : 'y/N';
315
+
316
+ while (true) {
317
+ const answer = (await rl.question(`${label} [${hint}] `))
318
+ .trim()
319
+ .toLowerCase();
320
+
321
+ if (!answer) {
322
+ return defaultValue;
323
+ }
324
+
325
+ if (['y', 'yes'].includes(answer)) {
326
+ return true;
327
+ }
328
+
329
+ if (['n', 'no'].includes(answer)) {
330
+ return false;
331
+ }
332
+ }
333
+ }
334
+
335
+ async function promptWithDefault(rl, label, defaultValue) {
336
+ const answer = (await rl.question(`${label} (${defaultValue}): `)).trim();
337
+
338
+ return answer || defaultValue;
339
+ }
340
+
341
+ async function promptChoice(rl, label, choices) {
342
+ const renderedChoices = choices
343
+ .map((choice) =>
344
+ choice.default ? `${choice.label.toUpperCase()}` : choice.label
345
+ )
346
+ .join('/');
347
+
348
+ while (true) {
349
+ const answer = (await rl.question(`${label} (${renderedChoices}): `))
350
+ .trim()
351
+ .toLowerCase();
352
+
353
+ if (!answer) {
354
+ const defaultChoice = choices.find((choice) => choice.default);
355
+
356
+ if (defaultChoice) {
357
+ return defaultChoice.value;
358
+ }
359
+ }
360
+
361
+ const matchingChoice = choices.find(
362
+ (choice) => choice.label === answer || choice.value === answer
363
+ );
364
+
365
+ if (matchingChoice) {
366
+ return matchingChoice.value;
367
+ }
368
+ }
369
+ }
370
+
187
371
  function replaceInFile(filePath, searchValue, replacement) {
188
372
  const source = readFileSync(filePath, 'utf8');
189
373
  writeFileSync(filePath, source.replace(searchValue, replacement.with));
@@ -201,7 +385,7 @@ function toPackageName(value) {
201
385
  return name || defaultAppName;
202
386
  }
203
387
 
204
- function parsePackageManagerFlag(args) {
388
+ function resolvePackageManager(args) {
205
389
  const selectedFlags = args.filter((arg) =>
206
390
  ['--bun', '--npm', '--pnpm', '--yarn'].includes(arg)
207
391
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-hsi-app",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Create a new app from the frontend template.",
6
6
  "bin": {