create-harper 0.0.1 → 0.0.3

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.
Files changed (137) hide show
  1. package/README.md +5 -0
  2. package/index.js +37 -214
  3. package/lib/constants/defaultTargetDir.js +1 -0
  4. package/lib/constants/frameworks.js +75 -0
  5. package/lib/constants/helpMessage.js +23 -0
  6. package/lib/constants/renameFiles.js +7 -0
  7. package/lib/constants/templates.js +6 -0
  8. package/lib/fs/applyAndWriteTemplateFile.js +9 -0
  9. package/lib/fs/copy.js +11 -0
  10. package/lib/fs/copyDir.js +12 -0
  11. package/lib/fs/crawlTemplateDir.js +18 -0
  12. package/lib/fs/emptyDir.js +14 -0
  13. package/lib/fs/formatTargetDir.js +3 -0
  14. package/lib/fs/isEmpty.js +6 -0
  15. package/lib/install.js +17 -0
  16. package/lib/pkg/getInstallCommand.js +6 -0
  17. package/lib/pkg/getRunCommand.js +12 -0
  18. package/lib/pkg/isValidPackageName.js +5 -0
  19. package/lib/pkg/pkgFromUserAgent.js +9 -0
  20. package/lib/pkg/toValidPackageName.js +9 -0
  21. package/lib/run.js +14 -0
  22. package/lib/start.js +15 -0
  23. package/lib/steps/getEnvVars.js +79 -0
  24. package/lib/steps/getImmediate.js +29 -0
  25. package/lib/steps/getPackageName.js +38 -0
  26. package/lib/steps/getProjectName.js +40 -0
  27. package/lib/steps/getTemplate.js +65 -0
  28. package/lib/steps/handleExistingDir.js +61 -0
  29. package/lib/steps/scaffoldProject.js +40 -0
  30. package/lib/steps/showOutro.js +30 -0
  31. package/package.json +20 -8
  32. package/template-barebones/README.md +7 -0
  33. package/template-barebones/_aiignore +1 -0
  34. package/template-barebones/_env +3 -0
  35. package/template-barebones/_env.example +3 -0
  36. package/template-barebones/_gitignore +147 -0
  37. package/template-barebones/config.yaml +7 -0
  38. package/template-barebones/graphql.config.yml +3 -0
  39. package/template-barebones/package.json +14 -0
  40. package/template-barebones/schema.graphql +1 -0
  41. package/template-react/README.md +45 -0
  42. package/template-react/_aiignore +1 -0
  43. package/template-react/_env +3 -0
  44. package/template-react/_env.example +3 -0
  45. package/template-react/_github/workflow/deploy.yaml +40 -0
  46. package/template-react/_gitignore +147 -0
  47. package/template-react/config.yaml +23 -0
  48. package/template-react/deploy-template/config.yaml +2 -0
  49. package/template-react/deploy-template/fastify/static.js +14 -0
  50. package/template-react/deploy-template/package.json +5 -0
  51. package/template-react/graphql.config.yml +3 -0
  52. package/template-react/index.html +13 -0
  53. package/template-react/package.json +25 -0
  54. package/template-react/public/react.svg +14 -0
  55. package/template-react/public/typescript.svg +16 -0
  56. package/template-react/public/vite.svg +42 -0
  57. package/template-react/resources.js +27 -0
  58. package/template-react/schema.graphql +5 -0
  59. package/template-react/src/App.jsx +34 -0
  60. package/template-react/src/main.jsx +13 -0
  61. package/template-react/src/style.css +96 -0
  62. package/template-react/src/vite-env.d.ts +9 -0
  63. package/template-react/vite.config.js +22 -0
  64. package/template-react-ts/README.md +45 -0
  65. package/template-react-ts/_aiignore +1 -0
  66. package/template-react-ts/_env +3 -0
  67. package/template-react-ts/_env.example +3 -0
  68. package/template-react-ts/_github/workflow/deploy.yaml +40 -0
  69. package/template-react-ts/_gitignore +147 -0
  70. package/template-react-ts/config.yaml +23 -0
  71. package/template-react-ts/deploy-template/config.yaml +2 -0
  72. package/template-react-ts/deploy-template/fastify/static.js +14 -0
  73. package/template-react-ts/deploy-template/package.json +5 -0
  74. package/template-react-ts/graphql.config.yml +3 -0
  75. package/template-react-ts/index.html +13 -0
  76. package/template-react-ts/package.json +29 -0
  77. package/template-react-ts/public/react.svg +14 -0
  78. package/template-react-ts/public/typescript.svg +16 -0
  79. package/template-react-ts/public/vite.svg +42 -0
  80. package/template-react-ts/resources.ts +52 -0
  81. package/template-react-ts/schema.graphql +5 -0
  82. package/template-react-ts/src/App.tsx +34 -0
  83. package/template-react-ts/src/main.tsx +13 -0
  84. package/template-react-ts/src/style.css +96 -0
  85. package/template-react-ts/src/vite-env.d.ts +9 -0
  86. package/template-react-ts/tsconfig.json +34 -0
  87. package/template-react-ts/vite.config.ts +22 -0
  88. package/template-studio/README.md +34 -0
  89. package/template-studio/_aiignore +1 -0
  90. package/template-studio/_gitignore +147 -0
  91. package/template-studio/config.yaml +24 -0
  92. package/template-studio/graphql.config.yml +3 -0
  93. package/template-studio/package.json +14 -0
  94. package/template-studio/resources.js +27 -0
  95. package/template-studio/schema.graphql +7 -0
  96. package/template-studio/web/index.html +28 -0
  97. package/template-studio/web/index.js +18 -0
  98. package/template-studio/web/styles.css +57 -0
  99. package/template-studio-ts/README.md +34 -0
  100. package/template-studio-ts/_aiignore +1 -0
  101. package/template-studio-ts/_gitignore +147 -0
  102. package/template-studio-ts/config.yaml +24 -0
  103. package/template-studio-ts/graphql.config.yml +3 -0
  104. package/template-studio-ts/package.json +14 -0
  105. package/template-studio-ts/resources.ts +52 -0
  106. package/template-studio-ts/schema.graphql +7 -0
  107. package/template-studio-ts/tsconfig.json +10 -0
  108. package/template-studio-ts/web/index.html +28 -0
  109. package/template-studio-ts/web/index.js +18 -0
  110. package/template-studio-ts/web/styles.css +57 -0
  111. package/template-vanilla/README.md +57 -0
  112. package/template-vanilla/_aiignore +1 -0
  113. package/template-vanilla/_env +3 -0
  114. package/template-vanilla/_env.example +3 -0
  115. package/template-vanilla/_gitignore +147 -0
  116. package/template-vanilla/config.yaml +24 -0
  117. package/template-vanilla/graphql.config.yml +3 -0
  118. package/template-vanilla/package.json +14 -0
  119. package/template-vanilla/resources.js +27 -0
  120. package/template-vanilla/schema.graphql +7 -0
  121. package/template-vanilla/web/index.html +28 -0
  122. package/template-vanilla/web/index.js +18 -0
  123. package/template-vanilla/web/styles.css +57 -0
  124. package/template-vanilla-ts/README.md +57 -0
  125. package/template-vanilla-ts/_aiignore +1 -0
  126. package/template-vanilla-ts/_env +3 -0
  127. package/template-vanilla-ts/_env.example +3 -0
  128. package/template-vanilla-ts/_gitignore +147 -0
  129. package/template-vanilla-ts/config.yaml +24 -0
  130. package/template-vanilla-ts/graphql.config.yml +3 -0
  131. package/template-vanilla-ts/package.json +14 -0
  132. package/template-vanilla-ts/resources.ts +52 -0
  133. package/template-vanilla-ts/schema.graphql +7 -0
  134. package/template-vanilla-ts/tsconfig.json +10 -0
  135. package/template-vanilla-ts/web/index.html +28 -0
  136. package/template-vanilla-ts/web/index.js +18 -0
  137. package/template-vanilla-ts/web/styles.css +57 -0
package/README.md CHANGED
@@ -78,3 +78,8 @@ Currently supported template presets include:
78
78
  - `qwik-ts`
79
79
 
80
80
  You can use `.` for the project name to scaffold in the current directory.
81
+
82
+ ## Shout Out
83
+
84
+ This project is based largely on the prior work of the Vite team on Create Vite:
85
+ https://github.com/vitejs/vite/tree/main/packages/create-vite
package/index.js CHANGED
@@ -1,43 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import * as prompts from '@clack/prompts';
3
3
  import { determineAgent } from '@vercel/detect-agent';
4
- import spawn from 'cross-spawn';
5
4
  import mri from 'mri';
6
- import fs from 'node:fs';
7
- import path from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { defaultTargetDir } from './lib/constants/defaultTargetDir.js';
10
- import { FRAMEWORKS } from './lib/constants/frameworks.js';
11
5
  import { helpMessage } from './lib/constants/helpMessage.js';
12
- import { TEMPLATES } from './lib/constants/templates.js';
13
- import { crawlTemplateDir } from './lib/fs/crawlTemplateDir.js';
14
- import { emptyDir } from './lib/fs/emptyDir.js';
15
6
  import { formatTargetDir } from './lib/fs/formatTargetDir.js';
16
- import { isEmpty } from './lib/fs/isEmpty.js';
17
- import { install } from './lib/install.js';
18
- import { getFullCustomCommand } from './lib/pkg/getFullCustomCommand.js';
19
- import { getInstallCommand } from './lib/pkg/getInstallCommand.js';
20
- import { getRunCommand } from './lib/pkg/getRunCommand.js';
21
- import { isValidPackageName } from './lib/pkg/isValidPackageName.js';
22
7
  import { pkgFromUserAgent } from './lib/pkg/pkgFromUserAgent.js';
23
- import { toValidPackageName } from './lib/pkg/toValidPackageName.js';
24
- import { start } from './lib/start.js';
8
+ import { getEnvVars } from './lib/steps/getEnvVars.js';
9
+ import { getImmediate } from './lib/steps/getImmediate.js';
10
+ import { getPackageName } from './lib/steps/getPackageName.js';
11
+ import { getProjectName } from './lib/steps/getProjectName.js';
12
+ import { getTemplate } from './lib/steps/getTemplate.js';
13
+ import { handleExistingDir } from './lib/steps/handleExistingDir.js';
14
+ import { scaffoldProject } from './lib/steps/scaffoldProject.js';
15
+ import { showOutro } from './lib/steps/showOutro.js';
25
16
 
26
17
  const argv = mri(process.argv.slice(2), {
27
18
  boolean: ['help', 'overwrite', 'immediate', 'interactive'],
28
19
  alias: { h: 'help', t: 'template', i: 'immediate' },
29
- string: ['template'],
20
+ string: ['template', 'cli-target-username', 'cli-target'],
30
21
  });
31
- const cwd = process.cwd();
32
22
 
33
23
  init().catch((e) => {
34
24
  console.error(e);
35
25
  });
36
26
 
37
27
  async function init() {
38
- const argTargetDir = argv._[0]
39
- ? formatTargetDir(String(argv._[0]))
40
- : undefined;
28
+ const argTargetDir = argv._[0] ? formatTargetDir(String(argv._[0])) : undefined;
41
29
  const argTemplate = argv.template;
42
30
  const argOverwrite = argv.overwrite;
43
31
  const argImmediate = argv.immediate;
@@ -59,207 +47,42 @@ async function init() {
59
47
  );
60
48
  }
61
49
 
62
- const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
63
50
  const cancel = () => prompts.cancel('Operation cancelled');
64
51
 
65
- // 1. Get project name and target dir
66
- let targetDir = argTargetDir;
67
- let projectName = targetDir;
68
- if (!targetDir) {
69
- if (interactive) {
70
- projectName = await prompts.text({
71
- message: 'Project name:',
72
- defaultValue: defaultTargetDir,
73
- placeholder: defaultTargetDir,
74
- validate: (value) => {
75
- return !value || formatTargetDir(value).length > 0
76
- ? undefined
77
- : 'Invalid project name';
78
- },
79
- });
80
- if (prompts.isCancel(projectName)) { return cancel(); }
81
- targetDir = formatTargetDir(projectName);
82
- } else {
83
- targetDir = defaultTargetDir;
84
- }
85
- }
52
+ // 1. Get the project name and target directory
53
+ const projectNameResult = await getProjectName(argTargetDir, interactive);
54
+ if (projectNameResult.cancelled) { return cancel(); }
55
+ const { projectName, targetDir } = projectNameResult;
86
56
 
87
- // 2. Handle directory if exist and not empty
88
- if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
89
- let overwrite = argOverwrite
90
- ? 'yes'
91
- : undefined;
92
- if (!overwrite) {
93
- if (interactive) {
94
- const res = await prompts.select({
95
- message: (targetDir === '.'
96
- ? 'Current directory'
97
- : `Target directory "${targetDir}"`)
98
- + ` is not empty. Please choose how to proceed:`,
99
- options: [
100
- {
101
- label: 'Cancel operation',
102
- value: 'no',
103
- },
104
- {
105
- label: 'Remove existing files and continue',
106
- value: 'yes',
107
- },
108
- {
109
- label: 'Ignore files and continue',
110
- value: 'ignore',
111
- },
112
- ],
113
- });
114
- if (prompts.isCancel(res)) { return cancel(); }
115
- overwrite = res;
116
- } else {
117
- overwrite = 'no';
118
- }
119
- }
57
+ // 2. Handle if the directory exists and isn't empty
58
+ const handleExistingDirResult = await handleExistingDir(targetDir, argOverwrite, interactive);
59
+ if (handleExistingDirResult.cancelled) { return cancel(); }
120
60
 
121
- switch (overwrite) {
122
- case 'yes':
123
- emptyDir(targetDir);
124
- break;
125
- case 'no':
126
- cancel();
127
- return;
128
- }
129
- }
130
-
131
- // 3. Get package name
132
- let packageName = path.basename(path.resolve(targetDir));
133
- if (!isValidPackageName(packageName)) {
134
- if (interactive) {
135
- const packageNameResult = await prompts.text({
136
- message: 'Package name:',
137
- defaultValue: toValidPackageName(packageName),
138
- placeholder: toValidPackageName(packageName),
139
- validate(dir) {
140
- if (dir && !isValidPackageName(dir)) {
141
- return 'Invalid package.json name';
142
- }
143
- },
144
- });
145
- if (prompts.isCancel(packageNameResult)) { return cancel(); }
146
- packageName = packageNameResult;
147
- } else {
148
- packageName = toValidPackageName(packageName);
149
- }
150
- }
61
+ // 3. Get the package name
62
+ const packageNameResult = await getPackageName(targetDir, interactive);
63
+ if (packageNameResult.cancelled) { return cancel(); }
64
+ const { packageName } = packageNameResult;
151
65
 
152
66
  // 4. Choose a framework and variant
153
- let template = argTemplate;
154
- let hasInvalidArgTemplate = false;
155
- if (argTemplate && !TEMPLATES.includes(argTemplate)) {
156
- template = undefined;
157
- hasInvalidArgTemplate = true;
158
- }
159
- if (!template) {
160
- if (interactive) {
161
- const framework = await prompts.select({
162
- message: hasInvalidArgTemplate
163
- ? `"${argTemplate}" isn't a valid template. Please choose from below: `
164
- : 'Select a framework:',
165
- options: FRAMEWORKS
166
- .filter(framework => !framework.hidden)
167
- .map((framework) => {
168
- const frameworkColor = framework.color;
169
- return {
170
- label: frameworkColor(framework.display || framework.name),
171
- value: framework,
172
- };
173
- }),
174
- });
175
- if (prompts.isCancel(framework)) { return cancel(); }
176
-
177
- const variant = framework.variants.length === 1
178
- ? framework.variants[0].name
179
- : await prompts.select({
180
- message: 'Select a variant:',
181
- options: framework.variants.map((variant) => {
182
- const variantColor = variant.color;
183
- const command = variant.customCommand
184
- ? getFullCustomCommand(variant.customCommand, pkgInfo).replace(
185
- / TARGET_DIR$/,
186
- '',
187
- )
188
- : undefined;
189
- return {
190
- label: variantColor(variant.display || variant.name),
191
- value: variant.name,
192
- hint: command,
193
- };
194
- }),
195
- });
196
- if (prompts.isCancel(variant)) { return cancel(); }
197
-
198
- template = variant;
199
- } else {
200
- template = 'vanilla-ts';
201
- }
202
- }
67
+ const templateResult = await getTemplate(argTemplate, interactive);
68
+ if (templateResult.cancelled) { return cancel(); }
69
+ const { template } = templateResult;
203
70
 
71
+ // 5. Should we do a package manager installation?
72
+ const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent);
204
73
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm';
74
+ const immediateResult = await getImmediate(argImmediate, interactive, pkgManager);
75
+ if (immediateResult.cancelled) { return cancel(); }
76
+ const { immediate } = immediateResult;
205
77
 
206
- const root = path.join(cwd, targetDir);
78
+ // 6. Get environment variables for .env file
79
+ const envVarsResult = await getEnvVars(argv, interactive, template);
80
+ if (envVarsResult.cancelled) { return cancel(); }
81
+ const { envVars } = envVarsResult;
207
82
 
208
- const { customCommand } = FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {};
83
+ // 7. Write out the contents based on all prior steps.
84
+ const root = scaffoldProject(targetDir, projectName, packageName, template, envVars);
209
85
 
210
- if (customCommand) {
211
- const fullCustomCommand = getFullCustomCommand(customCommand, pkgInfo);
212
-
213
- const [command, ...args] = fullCustomCommand.split(' ');
214
- // we replace TARGET_DIR here because targetDir may include a space
215
- const replacedArgs = args.map((arg) => arg.replace('TARGET_DIR', () => targetDir));
216
- const { status } = spawn.sync(command, replacedArgs, {
217
- stdio: 'inherit',
218
- });
219
- process.exit(status ?? 0);
220
- }
221
-
222
- // 5. Ask about immediate install and package manager
223
- let immediate = argImmediate;
224
- if (immediate === undefined) {
225
- if (interactive) {
226
- const immediateResult = await prompts.confirm({
227
- message: `Install with ${pkgManager} and start now?`,
228
- });
229
- if (prompts.isCancel(immediateResult)) { return cancel(); }
230
- immediate = immediateResult;
231
- } else {
232
- immediate = false;
233
- }
234
- }
235
-
236
- // Only create a directory for built-in templates, not for customCommand
237
- fs.mkdirSync(root, { recursive: true });
238
- prompts.log.step(`Scaffolding project in ${root}...`);
239
-
240
- const context = {
241
- projectName,
242
- packageName,
243
- };
244
-
245
- const templateSharedDir = path.resolve(fileURLToPath(import.meta.url), '..', `template-shared`);
246
- crawlTemplateDir(root, templateSharedDir, context);
247
-
248
- const templateDir = path.resolve(fileURLToPath(import.meta.url), '..', `template-${template}`);
249
- crawlTemplateDir(root, templateDir, context);
250
-
251
- if (immediate) {
252
- install(root, pkgManager);
253
- start(root, pkgManager);
254
- } else {
255
- let doneMessage = '';
256
- const cdProjectName = path.relative(cwd, root);
257
- doneMessage += `Done. Now run:\n`;
258
- if (root !== cwd) {
259
- doneMessage += `\n cd ${cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName}`;
260
- }
261
- doneMessage += `\n ${getInstallCommand(pkgManager).join(' ')}`;
262
- doneMessage += `\n ${getRunCommand(pkgManager, 'dev').join(' ')}`;
263
- prompts.outro(doneMessage);
264
- }
86
+ // 8. Log out the next steps.
87
+ showOutro(root, pkgManager, immediate);
265
88
  }
@@ -0,0 +1 @@
1
+ export const defaultTargetDir = 'harper-project';
@@ -0,0 +1,75 @@
1
+ import colors from 'picocolors';
2
+
3
+ const {
4
+ blue,
5
+ cyan,
6
+ gray,
7
+ yellow,
8
+ } = colors;
9
+
10
+ export const FRAMEWORKS = [
11
+ {
12
+ name: 'vanilla',
13
+ display: 'Vanilla',
14
+ color: yellow,
15
+ variants: [
16
+ {
17
+ name: 'vanilla-ts',
18
+ display: 'TypeScript',
19
+ color: blue,
20
+ },
21
+ {
22
+ name: 'vanilla',
23
+ display: 'JavaScript',
24
+ color: yellow,
25
+ },
26
+ ],
27
+ },
28
+ {
29
+ name: 'react',
30
+ display: 'React',
31
+ color: cyan,
32
+ variants: [
33
+ {
34
+ name: 'react-ts',
35
+ display: 'TypeScript',
36
+ color: blue,
37
+ },
38
+ {
39
+ name: 'react',
40
+ display: 'JavaScript',
41
+ color: yellow,
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ name: 'studio',
47
+ hidden: true,
48
+ display: 'Studio',
49
+ color: gray,
50
+ variants: [
51
+ {
52
+ name: 'studio-ts',
53
+ display: 'TypeScript',
54
+ color: gray,
55
+ },
56
+ {
57
+ name: 'studio',
58
+ display: 'JavaScript',
59
+ color: gray,
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ name: 'barebones',
65
+ display: 'Barebones',
66
+ color: gray,
67
+ variants: [
68
+ {
69
+ name: 'barebones',
70
+ display: 'Barebones',
71
+ color: gray,
72
+ },
73
+ ],
74
+ },
75
+ ];
@@ -0,0 +1,23 @@
1
+ import colors from 'picocolors';
2
+
3
+ const {
4
+ cyan,
5
+ green,
6
+ yellow,
7
+ } = colors;
8
+
9
+ export const helpMessage = `\
10
+ Usage: create-harper [OPTION]... [DIRECTORY]
11
+
12
+ Create a new Harper project in JavaScript or TypeScript.
13
+ When running in TTY, the CLI will start in interactive mode.
14
+
15
+ Options:
16
+ -t, --template NAME use a specific template
17
+ -i, --immediate install dependencies and start dev
18
+ --interactive / --no-interactive force interactive / non-interactive mode
19
+
20
+ Available templates:
21
+ ${yellow('vanilla-ts vanilla')}
22
+ ${green('vue-ts vue')}
23
+ ${cyan('react-ts react')}`;
@@ -0,0 +1,7 @@
1
+ export const renameFiles = {
2
+ _aiignore: '.aiignore',
3
+ _gitignore: '.gitignore',
4
+ _github: '.github',
5
+ '_env.example': '.env.example',
6
+ '_env': '.env',
7
+ };
@@ -0,0 +1,6 @@
1
+ import { FRAMEWORKS } from './frameworks.js';
2
+
3
+ export const TEMPLATES = FRAMEWORKS.map((f) => f.variants.map((v) => v.name)).reduce(
4
+ (a, b) => a.concat(b),
5
+ [],
6
+ );
@@ -0,0 +1,9 @@
1
+ import fs from 'node:fs';
2
+
3
+ export function applyAndWriteTemplateFile(targetPath, templatePath, substitutions) {
4
+ let updatedContent = fs.readFileSync(templatePath, 'utf-8');
5
+ for (const variableName in substitutions) {
6
+ updatedContent = updatedContent.replaceAll(variableName, substitutions[variableName]);
7
+ }
8
+ fs.writeFileSync(targetPath, updatedContent);
9
+ }
package/lib/fs/copy.js ADDED
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs';
2
+ import { copyDir } from './copyDir.js';
3
+
4
+ export function copy(src, dest) {
5
+ const stat = fs.statSync(src);
6
+ if (stat.isDirectory()) {
7
+ copyDir(src, dest);
8
+ } else {
9
+ fs.copyFileSync(src, dest);
10
+ }
11
+ }
@@ -0,0 +1,12 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { copy } from './copy.js';
4
+
5
+ export function copyDir(srcDir, destDir) {
6
+ fs.mkdirSync(destDir, { recursive: true });
7
+ for (const file of fs.readdirSync(srcDir)) {
8
+ const srcFile = path.resolve(srcDir, file);
9
+ const destFile = path.resolve(destDir, file);
10
+ copy(srcFile, destFile);
11
+ }
12
+ }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { renameFiles } from '../constants/renameFiles.js';
4
+ import { applyAndWriteTemplateFile } from './applyAndWriteTemplateFile.js';
5
+
6
+ export function crawlTemplateDir(root, dir, substitutions) {
7
+ const files = fs.readdirSync(dir);
8
+ for (const file of files) {
9
+ const targetPath = path.join(root, renameFiles[file] ?? file);
10
+ const templatePath = path.join(dir, file);
11
+ if (fs.lstatSync(templatePath).isDirectory()) {
12
+ fs.mkdirSync(targetPath, { recursive: true });
13
+ crawlTemplateDir(targetPath, templatePath, substitutions);
14
+ } else {
15
+ applyAndWriteTemplateFile(targetPath, templatePath, substitutions);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function emptyDir(dir) {
5
+ if (!fs.existsSync(dir)) {
6
+ return;
7
+ }
8
+ for (const file of fs.readdirSync(dir)) {
9
+ if (file === '.git') {
10
+ continue;
11
+ }
12
+ fs.rmSync(path.resolve(dir, file), { recursive: true, force: true });
13
+ }
14
+ }
@@ -0,0 +1,3 @@
1
+ export function formatTargetDir(targetDir) {
2
+ return targetDir.trim().replace(/\/+$/g, '');
3
+ }
@@ -0,0 +1,6 @@
1
+ import fs from 'node:fs';
2
+
3
+ export function isEmpty(path) {
4
+ const files = fs.readdirSync(path);
5
+ return files.length === 0 || (files.length === 1 && files[0] === '.git');
6
+ }
package/lib/install.js ADDED
@@ -0,0 +1,17 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import { getInstallCommand } from './pkg/getInstallCommand.js';
3
+ import { run } from './run.js';
4
+
5
+ export function install(root, agent) {
6
+ if (process.env._HARPER_TEST_CLI) {
7
+ prompts.log.step(
8
+ `Installing dependencies with ${agent}... (skipped in test)`,
9
+ );
10
+ return;
11
+ }
12
+ prompts.log.step(`Installing dependencies with ${agent}...`);
13
+ run(getInstallCommand(agent), {
14
+ stdio: 'inherit',
15
+ cwd: root,
16
+ });
17
+ }
@@ -0,0 +1,6 @@
1
+ export function getInstallCommand(agent) {
2
+ if (agent === 'yarn') {
3
+ return [agent];
4
+ }
5
+ return [agent, 'install'];
6
+ }
@@ -0,0 +1,12 @@
1
+ export function getRunCommand(agent, script) {
2
+ switch (agent) {
3
+ case 'yarn':
4
+ case 'pnpm':
5
+ case 'bun':
6
+ return [agent, script];
7
+ case 'deno':
8
+ return [agent, 'task', script];
9
+ default:
10
+ return [agent, 'run', script];
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ export function isValidPackageName(projectName) {
2
+ return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
3
+ projectName,
4
+ );
5
+ }
@@ -0,0 +1,9 @@
1
+ export function pkgFromUserAgent(userAgent) {
2
+ if (!userAgent) { return undefined; }
3
+ const pkgSpec = userAgent.split(' ')[0];
4
+ const pkgSpecArr = pkgSpec.split('/');
5
+ return {
6
+ name: pkgSpecArr[0],
7
+ version: pkgSpecArr[1],
8
+ };
9
+ }
@@ -0,0 +1,9 @@
1
+ export function toValidPackageName(projectName) {
2
+ return projectName
3
+ .trim()
4
+ .toLowerCase()
5
+ .replace(/\s+/g, '-')
6
+ .replace(/^[._]/, '')
7
+ .replace(/[^a-z\d\-~]+/g, '-')
8
+ .replace(/-+$/, '');
9
+ }
package/lib/run.js ADDED
@@ -0,0 +1,14 @@
1
+ import spawn from 'cross-spawn';
2
+
3
+ export function run([command, ...args], options) {
4
+ const { status, error } = spawn.sync(command, args, options);
5
+ if (status != null && status > 0) {
6
+ process.exit(status);
7
+ }
8
+
9
+ if (error) {
10
+ console.error(`\n${command} ${args.join(' ')} error!`);
11
+ console.error(error);
12
+ process.exit(1);
13
+ }
14
+ }
package/lib/start.js ADDED
@@ -0,0 +1,15 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import { getRunCommand } from './pkg/getRunCommand.js';
3
+ import { run } from './run.js';
4
+
5
+ export function start(root, agent) {
6
+ if (process.env._HARPER_TEST_CLI) {
7
+ prompts.log.step('Starting dev server... (skipped in test)');
8
+ return;
9
+ }
10
+ prompts.log.step('Starting dev server...');
11
+ run(getRunCommand(agent, 'dev'), {
12
+ stdio: 'inherit',
13
+ cwd: root,
14
+ });
15
+ }
@@ -0,0 +1,79 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /**
7
+ * Step 5: Get environment variables for .env file
8
+ * @param {any} argv
9
+ * @param {boolean} interactive
10
+ * @param {string} template
11
+ * @returns {Promise<{envVars: {username: string, target: string, password?: string}, cancelled: boolean}>}
12
+ */
13
+ export async function getEnvVars(argv, interactive, template) {
14
+ const templateDir = path.resolve(
15
+ fileURLToPath(import.meta.url),
16
+ '..',
17
+ '..',
18
+ '..',
19
+ `template-${template}`,
20
+ );
21
+ const hasEnvFile = fs.existsSync(path.join(templateDir, '_env'));
22
+
23
+ if (!hasEnvFile) {
24
+ return {
25
+ envVars: {},
26
+ cancelled: false,
27
+ };
28
+ }
29
+
30
+ let username = argv['cli-target-username'];
31
+ let target = argv['cli-target'];
32
+ let password = '';
33
+
34
+ if (interactive) {
35
+ if (!username) {
36
+ const usernameResult = await prompts.text({
37
+ message: 'CLI Target Username:',
38
+ placeholder: 'YOUR_CLUSTER_USERNAME',
39
+ });
40
+
41
+ if (prompts.isCancel(usernameResult)) {
42
+ return { envVars: {}, cancelled: true };
43
+ }
44
+ username = usernameResult;
45
+ }
46
+
47
+ if (!target) {
48
+ const targetResult = await prompts.text({
49
+ message: 'CLI Target URL:',
50
+ placeholder: 'YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE',
51
+ });
52
+
53
+ if (prompts.isCancel(targetResult)) {
54
+ return { envVars: {}, cancelled: true };
55
+ }
56
+ target = targetResult;
57
+ }
58
+
59
+ const passwordResult = await prompts.password({
60
+ message: 'CLI Target Password:',
61
+ });
62
+
63
+ if (prompts.isCancel(passwordResult)) {
64
+ return { envVars: {}, cancelled: true };
65
+ }
66
+ password = passwordResult;
67
+ } else {
68
+ prompts.log.warn('Non-interactive mode: Please update your .env to add your CLI_TARGET_PASSWORD on your own.');
69
+ }
70
+
71
+ return {
72
+ envVars: {
73
+ username: username || 'YOUR_CLUSTER_USERNAME',
74
+ target: target || 'YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE',
75
+ password: password || 'YOUR_CLUSTER_PASSWORD',
76
+ },
77
+ cancelled: false,
78
+ };
79
+ }