create-hsi-app 0.2.0 → 0.5.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
@@ -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,99 @@ 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.2';
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);
18
- const targetArg = rawArgs.find((arg) => !arg.startsWith('--')) ?? '.';
19
+ const parsedArgs = parseCliArgs(rawArgs);
20
+ const selectedPackageManager = resolvePackageManager(parsedArgs);
21
+ const shouldInstallDependencies = !(
22
+ parsedArgs.noInstall || readNpmBooleanFlag('noinstall')
23
+ );
24
+ const shouldSkipRepoSetup = parsedArgs.noRepo || readNpmBooleanFlag('norepo');
25
+ const isInteractive = input.isTTY && output.isTTY;
26
+ const targetArg = parsedArgs.targetArg ?? '.';
19
27
  const targetPath = resolve(targetArg);
20
28
  const appName = toPackageName(basename(targetPath));
21
29
 
22
- if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
23
- fail(`Target directory is not empty: ${targetPath}`);
24
- }
30
+ main().catch((error) => {
31
+ fail(error.message);
32
+ });
33
+
34
+ async function main() {
35
+ if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
36
+ fail(`Target directory is not empty: ${targetPath}`);
37
+ }
38
+
39
+ run('git', [
40
+ '-c',
41
+ 'advice.detachedHead=false',
42
+ 'clone',
43
+ '--branch',
44
+ templateTag,
45
+ '--depth',
46
+ '1',
47
+ templateRepo,
48
+ targetPath,
49
+ ]);
50
+
51
+ rmSync(join(targetPath, '.git'), { force: true, recursive: true });
52
+ rmSync(join(targetPath, '.github'), { force: true, recursive: true });
53
+ rmSync(join(targetPath, 'docs'), { force: true, recursive: true });
54
+ rmSync(join(targetPath, 'packages'), { force: true, recursive: true });
55
+ rmSync(join(targetPath, 'scripts'), { force: true, recursive: true });
56
+
57
+ updatePackageJson();
58
+ updateBunLock();
59
+ updateAppText();
60
+ updatePackageManagerFiles();
61
+ writeAppReadme();
62
+
63
+ if (shouldInstallDependencies) {
64
+ installDependencies();
65
+ }
25
66
 
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}`);
67
+ const repoSetup = await maybeSetupRepo();
68
+
69
+ console.log(`\nCreated ${appName} in ${targetPath}\n`);
70
+ if (repoSetup === 'github') {
71
+ console.log(
72
+ 'Created a local git repository and configured GitHub origin.'
73
+ );
74
+ } else if (repoSetup === 'local') {
75
+ console.log('Initialized a local git repository.');
76
+ }
77
+ if (shouldInstallDependencies) {
78
+ console.log(`Installed dependencies with ${selectedPackageManager}.`);
79
+ }
80
+ console.log('\nNext steps:');
81
+ if (targetArg !== '.') {
82
+ console.log(` cd ${targetArg}`);
83
+ }
84
+ if (!shouldInstallDependencies) {
85
+ console.log(` ${installCommand()}`);
86
+ }
87
+ console.log(` ${devCommand()}`);
54
88
  }
55
- console.log(` ${installCommand()}`);
56
- console.log(` ${devCommand()}`);
57
89
 
58
- function run(command, args) {
90
+ function run(command, args, options = {}) {
59
91
  try {
60
- execFileSync(command, args, { stdio: 'inherit' });
61
- } catch {
62
- fail(`Failed to run: ${command} ${args.join(' ')}`);
92
+ return execFileSync(command, args, {
93
+ cwd: options.cwd,
94
+ encoding: options.capture ? 'utf8' : undefined,
95
+ stdio: options.capture ? 'pipe' : 'inherit',
96
+ });
97
+ } catch (error) {
98
+ if (options.allowFailure) {
99
+ return null;
100
+ }
101
+
102
+ const details = error.stderr?.toString().trim() || error.message;
103
+ fail(`Failed to run: ${command} ${args.join(' ')}\n${details}`);
63
104
  }
64
105
  }
65
106
 
@@ -73,6 +114,7 @@ function updatePackageJson() {
73
114
  delete packageJson.publishConfig;
74
115
  delete packageJson.packageManager;
75
116
  delete packageJson.engines;
117
+ delete packageJson.scripts.prepare;
76
118
  delete packageJson.scripts.release;
77
119
  packageJson.scripts.check =
78
120
  'tsc -p tsconfig.json --noEmit && eslint . && prettier . --check && vite build';
@@ -151,6 +193,25 @@ function updatePackageManagerFiles() {
151
193
  }
152
194
  }
153
195
 
196
+ function installDependencies() {
197
+ switch (selectedPackageManager) {
198
+ case 'bun':
199
+ run('bun', ['install'], { cwd: targetPath });
200
+ return;
201
+ case 'npm':
202
+ run('npm', ['install'], { cwd: targetPath });
203
+ return;
204
+ case 'pnpm':
205
+ run('pnpm', ['install'], { cwd: targetPath });
206
+ return;
207
+ case 'yarn':
208
+ run('yarn', ['install'], { cwd: targetPath });
209
+ return;
210
+ default:
211
+ fail(`Unsupported package manager: ${selectedPackageManager}`);
212
+ }
213
+ }
214
+
154
215
  function writeAppReadme() {
155
216
  const installLine = installCommand();
156
217
  const devLine = devCommand();
@@ -184,6 +245,133 @@ ${securityNote}
184
245
  writeFileSync(join(targetPath, 'README.md'), readme);
185
246
  }
186
247
 
248
+ async function maybeSetupRepo() {
249
+ if (shouldSkipRepoSetup || !isInteractive) {
250
+ return null;
251
+ }
252
+
253
+ const rl = readline.createInterface({ input, output });
254
+
255
+ try {
256
+ const shouldCreateRepo = await promptYesNo(
257
+ rl,
258
+ 'Create a git repository?',
259
+ true
260
+ );
261
+
262
+ if (!shouldCreateRepo) {
263
+ return null;
264
+ }
265
+
266
+ initLocalRepo();
267
+
268
+ if (!canUseGitHubCli()) {
269
+ return 'local';
270
+ }
271
+
272
+ const defaultRepoName = basename(targetPath);
273
+ const repoName = await promptWithDefault(
274
+ rl,
275
+ 'Repository name',
276
+ defaultRepoName
277
+ );
278
+ const visibility = await promptChoice(rl, 'Visibility', [
279
+ { label: 'private', value: 'private', default: true },
280
+ { label: 'public', value: 'public' },
281
+ ]);
282
+
283
+ run(
284
+ 'gh',
285
+ [
286
+ 'repo',
287
+ 'create',
288
+ repoName,
289
+ `--${visibility}`,
290
+ '--source=.',
291
+ '--remote=origin',
292
+ ],
293
+ { cwd: targetPath }
294
+ );
295
+
296
+ return 'github';
297
+ } finally {
298
+ rl.close();
299
+ }
300
+ }
301
+
302
+ function initLocalRepo() {
303
+ run('git', ['init', '-b', 'main'], { cwd: targetPath });
304
+ run('git', ['config', 'core.hooksPath', '.githooks'], { cwd: targetPath });
305
+ }
306
+
307
+ function canUseGitHubCli() {
308
+ return Boolean(
309
+ run('gh', ['auth', 'status'], {
310
+ cwd: targetPath,
311
+ capture: true,
312
+ allowFailure: true,
313
+ })
314
+ );
315
+ }
316
+
317
+ async function promptYesNo(rl, label, defaultValue) {
318
+ const hint = defaultValue ? 'Y/n' : 'y/N';
319
+
320
+ while (true) {
321
+ const answer = (await rl.question(`${label} [${hint}] `))
322
+ .trim()
323
+ .toLowerCase();
324
+
325
+ if (!answer) {
326
+ return defaultValue;
327
+ }
328
+
329
+ if (['y', 'yes'].includes(answer)) {
330
+ return true;
331
+ }
332
+
333
+ if (['n', 'no'].includes(answer)) {
334
+ return false;
335
+ }
336
+ }
337
+ }
338
+
339
+ async function promptWithDefault(rl, label, defaultValue) {
340
+ const answer = (await rl.question(`${label} (${defaultValue}): `)).trim();
341
+
342
+ return answer || defaultValue;
343
+ }
344
+
345
+ async function promptChoice(rl, label, choices) {
346
+ const renderedChoices = choices
347
+ .map((choice) =>
348
+ choice.default ? `${choice.label.toUpperCase()}` : choice.label
349
+ )
350
+ .join('/');
351
+
352
+ while (true) {
353
+ const answer = (await rl.question(`${label} (${renderedChoices}): `))
354
+ .trim()
355
+ .toLowerCase();
356
+
357
+ if (!answer) {
358
+ const defaultChoice = choices.find((choice) => choice.default);
359
+
360
+ if (defaultChoice) {
361
+ return defaultChoice.value;
362
+ }
363
+ }
364
+
365
+ const matchingChoice = choices.find(
366
+ (choice) => choice.label === answer || choice.value === answer
367
+ );
368
+
369
+ if (matchingChoice) {
370
+ return matchingChoice.value;
371
+ }
372
+ }
373
+ }
374
+
187
375
  function replaceInFile(filePath, searchValue, replacement) {
188
376
  const source = readFileSync(filePath, 'utf8');
189
377
  writeFileSync(filePath, source.replace(searchValue, replacement.with));
@@ -201,28 +389,69 @@ function toPackageName(value) {
201
389
  return name || defaultAppName;
202
390
  }
203
391
 
204
- function parsePackageManagerFlag(args) {
205
- const selectedFlags = args.filter((arg) =>
206
- ['--bun', '--npm', '--pnpm', '--yarn'].includes(arg)
207
- );
392
+ function parseCliArgs(args) {
393
+ const parsedArgs = {
394
+ noInstall: false,
395
+ noRepo: false,
396
+ packageManager: null,
397
+ targetArg: null,
398
+ };
399
+
400
+ for (const arg of args) {
401
+ switch (arg) {
402
+ case '--bun':
403
+ setPackageManagerOverride(parsedArgs, 'bun');
404
+ continue;
405
+ case '--npm':
406
+ setPackageManagerOverride(parsedArgs, 'npm');
407
+ continue;
408
+ case '--pnpm':
409
+ setPackageManagerOverride(parsedArgs, 'pnpm');
410
+ continue;
411
+ case '--yarn':
412
+ setPackageManagerOverride(parsedArgs, 'yarn');
413
+ continue;
414
+ case '--noInstall':
415
+ parsedArgs.noInstall = true;
416
+ continue;
417
+ case '--noRepo':
418
+ parsedArgs.noRepo = true;
419
+ continue;
420
+ default:
421
+ if (arg.startsWith('--')) {
422
+ fail(`Unsupported option: ${arg}`);
423
+ }
424
+
425
+ if (parsedArgs.targetArg) {
426
+ fail(`Unexpected argument: ${arg}`);
427
+ }
428
+
429
+ parsedArgs.targetArg = arg;
430
+ }
431
+ }
208
432
 
209
- if (selectedFlags.length > 1) {
433
+ return parsedArgs;
434
+ }
435
+
436
+ function setPackageManagerOverride(parsedArgs, packageManager) {
437
+ if (
438
+ parsedArgs.packageManager &&
439
+ parsedArgs.packageManager !== packageManager
440
+ ) {
210
441
  fail('Pass only one of --bun, --npm, --pnpm, or --yarn.');
211
442
  }
212
443
 
213
- switch (selectedFlags[0]) {
214
- case '--npm':
215
- return 'npm';
216
- case '--pnpm':
217
- return 'pnpm';
218
- case '--yarn':
219
- return 'yarn';
220
- case '--bun':
221
- case undefined:
222
- return 'bun';
223
- default:
224
- fail(`Unsupported package manager flag: ${selectedFlags[0]}`);
225
- }
444
+ parsedArgs.packageManager = packageManager;
445
+ }
446
+
447
+ function resolvePackageManager(parsedArgs) {
448
+ return parsedArgs.packageManager ?? 'bun';
449
+ }
450
+
451
+ function readNpmBooleanFlag(name) {
452
+ const value = process.env[`npm_config_${name}`];
453
+
454
+ return value === 'true' || value === '';
226
455
  }
227
456
 
228
457
  function packageManagerDeclaration() {
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.2",
4
4
  "type": "module",
5
5
  "description": "Create a new app from the frontend template.",
6
6
  "bin": {