create-harper 0.0.2 → 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.
- package/README.md +5 -0
- package/index.js +37 -214
- package/lib/constants/renameFiles.js +1 -0
- package/lib/fs/applyAndWriteTemplateFile.js +5 -5
- package/lib/fs/crawlTemplateDir.js +3 -3
- package/lib/pkg/toValidPackageName.js +2 -1
- package/lib/steps/getEnvVars.js +79 -0
- package/lib/steps/getImmediate.js +29 -0
- package/lib/steps/getPackageName.js +38 -0
- package/lib/steps/getProjectName.js +40 -0
- package/lib/steps/getTemplate.js +65 -0
- package/lib/steps/handleExistingDir.js +61 -0
- package/lib/steps/scaffoldProject.js +40 -0
- package/lib/steps/showOutro.js +30 -0
- package/package.json +12 -8
- package/template-barebones/README.md +1 -1
- package/template-barebones/_env +3 -0
- package/template-barebones/_env.example +3 -0
- package/template-barebones/package.json +1 -1
- package/template-react/README.md +1 -1
- package/template-react/_aiignore +1 -0
- package/template-react/_env +3 -0
- package/template-react/_env.example +3 -0
- package/template-react/_github/workflow/deploy.yaml +1 -1
- package/template-react/_gitignore +147 -0
- package/template-react/graphql.config.yml +3 -0
- package/template-react/index.html +1 -1
- package/template-react/package.json +5 -4
- package/template-react-ts/README.md +1 -1
- package/template-react-ts/_aiignore +1 -0
- package/template-react-ts/_env +3 -0
- package/template-react-ts/_env.example +3 -0
- package/template-react-ts/_github/workflow/deploy.yaml +1 -1
- package/template-react-ts/_gitignore +147 -0
- package/template-react-ts/graphql.config.yml +3 -0
- package/template-react-ts/index.html +1 -1
- package/template-react-ts/package.json +5 -4
- package/template-studio/README.md +1 -1
- package/template-studio/_aiignore +1 -0
- package/template-studio/_gitignore +147 -0
- package/template-studio/graphql.config.yml +3 -0
- package/template-studio/package.json +1 -1
- package/template-studio/web/index.html +2 -2
- package/template-studio-ts/README.md +1 -1
- package/template-studio-ts/_aiignore +1 -0
- package/template-studio-ts/_gitignore +147 -0
- package/template-studio-ts/graphql.config.yml +3 -0
- package/template-studio-ts/package.json +1 -1
- package/template-studio-ts/web/index.html +2 -2
- package/template-vanilla/README.md +1 -1
- package/template-vanilla/_aiignore +1 -0
- package/template-vanilla/_env +3 -0
- package/template-vanilla/_env.example +3 -0
- package/template-vanilla/_gitignore +147 -0
- package/template-vanilla/graphql.config.yml +3 -0
- package/template-vanilla/package.json +1 -1
- package/template-vanilla/web/index.html +2 -2
- package/template-vanilla-ts/README.md +1 -1
- package/template-vanilla-ts/_aiignore +1 -0
- package/template-vanilla-ts/_env +3 -0
- package/template-vanilla-ts/_env.example +3 -0
- package/template-vanilla-ts/_gitignore +147 -0
- package/template-vanilla-ts/graphql.config.yml +3 -0
- package/template-vanilla-ts/package.json +1 -1
- package/template-vanilla-ts/web/index.html +2 -2
- package/lib/fs/editFile.js +0 -6
- package/lib/pkg/getFullCustomCommand.js +0 -47
- package/template-shared/_env.example +0 -3
- /package/{template-shared → template-barebones}/_aiignore +0 -0
- /package/{template-shared → template-barebones}/_gitignore +0 -0
- /package/{template-shared → template-barebones}/graphql.config.yml +0 -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 {
|
|
24
|
-
import {
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
// 7. Write out the contents based on all prior steps.
|
|
84
|
+
const root = scaffoldProject(targetDir, projectName, packageName, template, envVars);
|
|
209
85
|
|
|
210
|
-
|
|
211
|
-
|
|
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
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import template from 'lodash/template.js';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
|
|
4
|
-
export function applyAndWriteTemplateFile(targetPath, templatePath,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
8
|
fs.writeFileSync(targetPath, updatedContent);
|
|
9
9
|
}
|
|
@@ -3,16 +3,16 @@ import path from 'node:path';
|
|
|
3
3
|
import { renameFiles } from '../constants/renameFiles.js';
|
|
4
4
|
import { applyAndWriteTemplateFile } from './applyAndWriteTemplateFile.js';
|
|
5
5
|
|
|
6
|
-
export function crawlTemplateDir(root, dir,
|
|
6
|
+
export function crawlTemplateDir(root, dir, substitutions) {
|
|
7
7
|
const files = fs.readdirSync(dir);
|
|
8
8
|
for (const file of files) {
|
|
9
9
|
const targetPath = path.join(root, renameFiles[file] ?? file);
|
|
10
10
|
const templatePath = path.join(dir, file);
|
|
11
11
|
if (fs.lstatSync(templatePath).isDirectory()) {
|
|
12
12
|
fs.mkdirSync(targetPath, { recursive: true });
|
|
13
|
-
crawlTemplateDir(targetPath, templatePath,
|
|
13
|
+
crawlTemplateDir(targetPath, templatePath, substitutions);
|
|
14
14
|
} else {
|
|
15
|
-
applyAndWriteTemplateFile(targetPath, templatePath,
|
|
15
|
+
applyAndWriteTemplateFile(targetPath, templatePath, substitutions);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as prompts from '@clack/prompts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Step 5: Ask about immediate install and package manager
|
|
5
|
+
* @param {boolean | undefined} argImmediate
|
|
6
|
+
* @param {boolean} interactive
|
|
7
|
+
* @param {string} pkgManager
|
|
8
|
+
* @returns {Promise<{immediate: boolean, cancelled: boolean}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function getImmediate(argImmediate, interactive, pkgManager) {
|
|
11
|
+
let immediate = argImmediate;
|
|
12
|
+
|
|
13
|
+
if (immediate === undefined) {
|
|
14
|
+
if (interactive) {
|
|
15
|
+
const immediateResult = await prompts.confirm({
|
|
16
|
+
message: `Install with ${pkgManager} and start now?`,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (prompts.isCancel(immediateResult)) {
|
|
20
|
+
return { immediate: false, cancelled: true };
|
|
21
|
+
}
|
|
22
|
+
immediate = immediateResult;
|
|
23
|
+
} else {
|
|
24
|
+
immediate = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { immediate, cancelled: false };
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as prompts from '@clack/prompts';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isValidPackageName } from '../pkg/isValidPackageName.js';
|
|
4
|
+
import { toValidPackageName } from '../pkg/toValidPackageName.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 3: Get package name
|
|
8
|
+
* @param {string} targetDir
|
|
9
|
+
* @param {boolean} interactive
|
|
10
|
+
* @returns {Promise<{packageName: string, cancelled: boolean}>}
|
|
11
|
+
*/
|
|
12
|
+
export async function getPackageName(targetDir, interactive) {
|
|
13
|
+
let packageName = path.basename(path.resolve(targetDir));
|
|
14
|
+
|
|
15
|
+
if (!isValidPackageName(packageName)) {
|
|
16
|
+
if (interactive) {
|
|
17
|
+
const packageNameResult = await prompts.text({
|
|
18
|
+
message: 'Package name:',
|
|
19
|
+
defaultValue: toValidPackageName(packageName),
|
|
20
|
+
placeholder: toValidPackageName(packageName),
|
|
21
|
+
validate(dir) {
|
|
22
|
+
if (dir && !isValidPackageName(dir)) {
|
|
23
|
+
return 'Invalid package.json name';
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (prompts.isCancel(packageNameResult)) {
|
|
29
|
+
return { packageName: '', cancelled: true };
|
|
30
|
+
}
|
|
31
|
+
packageName = packageNameResult;
|
|
32
|
+
} else {
|
|
33
|
+
packageName = toValidPackageName(packageName);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { packageName, cancelled: false };
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as prompts from '@clack/prompts';
|
|
2
|
+
import { defaultTargetDir } from '../constants/defaultTargetDir.js';
|
|
3
|
+
import { formatTargetDir } from '../fs/formatTargetDir.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Step 1: Get project name and target directory
|
|
7
|
+
* @param {string | undefined} argTargetDir
|
|
8
|
+
* @param {boolean} interactive
|
|
9
|
+
* @returns {Promise<{projectName: string, targetDir: string, cancelled: boolean}>}
|
|
10
|
+
*/
|
|
11
|
+
export async function getProjectName(argTargetDir, interactive) {
|
|
12
|
+
let targetDir = argTargetDir;
|
|
13
|
+
let projectName = targetDir;
|
|
14
|
+
|
|
15
|
+
if (!targetDir) {
|
|
16
|
+
if (interactive) {
|
|
17
|
+
projectName = await prompts.text({
|
|
18
|
+
message: 'Project name:',
|
|
19
|
+
defaultValue: defaultTargetDir,
|
|
20
|
+
placeholder: defaultTargetDir,
|
|
21
|
+
validate: (value) => {
|
|
22
|
+
return !value || formatTargetDir(value).length > 0
|
|
23
|
+
? undefined
|
|
24
|
+
: 'Invalid project name';
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (prompts.isCancel(projectName)) {
|
|
29
|
+
return { projectName: '', targetDir: '', cancelled: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
targetDir = formatTargetDir(projectName);
|
|
33
|
+
} else {
|
|
34
|
+
targetDir = defaultTargetDir;
|
|
35
|
+
projectName = targetDir;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { projectName, targetDir, cancelled: false };
|
|
40
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as prompts from '@clack/prompts';
|
|
2
|
+
import { FRAMEWORKS } from '../constants/frameworks.js';
|
|
3
|
+
import { TEMPLATES } from '../constants/templates.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Step 4: Choose a framework and variant
|
|
7
|
+
* @param {string | undefined} argTemplate
|
|
8
|
+
* @param {boolean} interactive
|
|
9
|
+
* @returns {Promise<{template: string, cancelled: boolean}>}
|
|
10
|
+
*/
|
|
11
|
+
export async function getTemplate(argTemplate, interactive) {
|
|
12
|
+
let template = argTemplate;
|
|
13
|
+
let hasInvalidArgTemplate = false;
|
|
14
|
+
|
|
15
|
+
if (argTemplate && !TEMPLATES.includes(argTemplate)) {
|
|
16
|
+
template = undefined;
|
|
17
|
+
hasInvalidArgTemplate = true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!template) {
|
|
21
|
+
if (interactive) {
|
|
22
|
+
const framework = await prompts.select({
|
|
23
|
+
message: hasInvalidArgTemplate
|
|
24
|
+
? `"${argTemplate}" isn't a valid template. Please choose from below: `
|
|
25
|
+
: 'Select a framework:',
|
|
26
|
+
options: FRAMEWORKS
|
|
27
|
+
.filter(framework => !framework.hidden)
|
|
28
|
+
.map((framework) => {
|
|
29
|
+
const frameworkColor = framework.color;
|
|
30
|
+
return {
|
|
31
|
+
label: frameworkColor(framework.display),
|
|
32
|
+
value: framework,
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (prompts.isCancel(framework)) {
|
|
38
|
+
return { template: '', cancelled: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const variant = framework.variants.length === 1
|
|
42
|
+
? framework.variants[0].name
|
|
43
|
+
: await prompts.select({
|
|
44
|
+
message: 'Select a variant:',
|
|
45
|
+
options: framework.variants.map((variant) => {
|
|
46
|
+
const variantColor = variant.color;
|
|
47
|
+
return {
|
|
48
|
+
label: variantColor(variant.display || variant.name),
|
|
49
|
+
value: variant.name,
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (prompts.isCancel(variant)) {
|
|
55
|
+
return { template: '', cancelled: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
template = variant;
|
|
59
|
+
} else {
|
|
60
|
+
template = 'vanilla-ts';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { template, cancelled: false };
|
|
65
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as prompts from '@clack/prompts';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { emptyDir } from '../fs/emptyDir.js';
|
|
4
|
+
import { isEmpty } from '../fs/isEmpty.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Step 2: Handle directory if exist and not empty
|
|
8
|
+
* @param {string} targetDir
|
|
9
|
+
* @param {boolean | undefined} argOverwrite
|
|
10
|
+
* @param {boolean} interactive
|
|
11
|
+
* @returns {Promise<{cancelled: boolean}>}
|
|
12
|
+
*/
|
|
13
|
+
export async function handleExistingDir(targetDir, argOverwrite, interactive) {
|
|
14
|
+
if (fs.existsSync(targetDir) && !isEmpty(targetDir)) {
|
|
15
|
+
let overwrite = argOverwrite
|
|
16
|
+
? 'yes'
|
|
17
|
+
: undefined;
|
|
18
|
+
|
|
19
|
+
if (!overwrite) {
|
|
20
|
+
if (interactive) {
|
|
21
|
+
const res = await prompts.select({
|
|
22
|
+
message: (targetDir === '.'
|
|
23
|
+
? 'Current directory'
|
|
24
|
+
: `Target directory "${targetDir}"`)
|
|
25
|
+
+ ` is not empty. Please choose how to proceed:`,
|
|
26
|
+
options: [
|
|
27
|
+
{
|
|
28
|
+
label: 'Cancel operation',
|
|
29
|
+
value: 'no',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: 'Remove existing files and continue',
|
|
33
|
+
value: 'yes',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: 'Ignore files and continue',
|
|
37
|
+
value: 'ignore',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (prompts.isCancel(res)) {
|
|
43
|
+
return { cancelled: true };
|
|
44
|
+
}
|
|
45
|
+
overwrite = res;
|
|
46
|
+
} else {
|
|
47
|
+
overwrite = 'no';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch (overwrite) {
|
|
52
|
+
case 'yes':
|
|
53
|
+
emptyDir(targetDir);
|
|
54
|
+
break;
|
|
55
|
+
case 'no':
|
|
56
|
+
return { cancelled: true };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { cancelled: false };
|
|
61
|
+
}
|