complete-cli 1.0.6 → 1.0.8

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 (50) hide show
  1. package/dist/commands/CheckCommand.js +141 -0
  2. package/dist/commands/InitCommand.js +64 -0
  3. package/dist/commands/NukeCommand.js +12 -0
  4. package/dist/commands/PublishCommand.js +162 -0
  5. package/dist/commands/UpdateCommand.js +15 -0
  6. package/dist/commands/check/check.test.js +86 -0
  7. package/dist/commands/check/getTruncatedText.js +139 -0
  8. package/dist/commands/init/checkIfProjectPathExists.js +21 -0
  9. package/dist/commands/init/createProject.js +130 -0
  10. package/dist/commands/init/getAuthorName.js +17 -0
  11. package/dist/commands/init/getProjectPath.js +80 -0
  12. package/dist/commands/init/packageManager.js +35 -0
  13. package/dist/commands/init/vsCodeInit.js +82 -0
  14. package/dist/constants.js +17 -0
  15. package/dist/git.js +132 -0
  16. package/dist/interfaces/GitHubCLIHostsYAML.js +1 -0
  17. package/dist/main.js +24 -0
  18. package/dist/prompt.js +53 -0
  19. package/package.json +10 -10
  20. package/src/main.ts +0 -3
  21. package/dist/main.cjs +0 -65140
  22. package/dist/main.cjs.map +0 -1
  23. package/dist/vendors-node_modules_prettier_plugins_acorn_mjs.main.cjs +0 -38
  24. package/dist/vendors-node_modules_prettier_plugins_acorn_mjs.main.cjs.map +0 -1
  25. package/dist/vendors-node_modules_prettier_plugins_angular_mjs.main.cjs +0 -25
  26. package/dist/vendors-node_modules_prettier_plugins_angular_mjs.main.cjs.map +0 -1
  27. package/dist/vendors-node_modules_prettier_plugins_babel_mjs.main.cjs +0 -38
  28. package/dist/vendors-node_modules_prettier_plugins_babel_mjs.main.cjs.map +0 -1
  29. package/dist/vendors-node_modules_prettier_plugins_estree_mjs.main.cjs +0 -61
  30. package/dist/vendors-node_modules_prettier_plugins_estree_mjs.main.cjs.map +0 -1
  31. package/dist/vendors-node_modules_prettier_plugins_flow_mjs.main.cjs +0 -42
  32. package/dist/vendors-node_modules_prettier_plugins_flow_mjs.main.cjs.map +0 -1
  33. package/dist/vendors-node_modules_prettier_plugins_glimmer_mjs.main.cjs +0 -55
  34. package/dist/vendors-node_modules_prettier_plugins_glimmer_mjs.main.cjs.map +0 -1
  35. package/dist/vendors-node_modules_prettier_plugins_graphql_mjs.main.cjs +0 -55
  36. package/dist/vendors-node_modules_prettier_plugins_graphql_mjs.main.cjs.map +0 -1
  37. package/dist/vendors-node_modules_prettier_plugins_html_mjs.main.cjs +0 -48
  38. package/dist/vendors-node_modules_prettier_plugins_html_mjs.main.cjs.map +0 -1
  39. package/dist/vendors-node_modules_prettier_plugins_markdown_mjs.main.cjs +0 -89
  40. package/dist/vendors-node_modules_prettier_plugins_markdown_mjs.main.cjs.map +0 -1
  41. package/dist/vendors-node_modules_prettier_plugins_meriyah_mjs.main.cjs +0 -27
  42. package/dist/vendors-node_modules_prettier_plugins_meriyah_mjs.main.cjs.map +0 -1
  43. package/dist/vendors-node_modules_prettier_plugins_postcss_mjs.main.cjs +0 -80
  44. package/dist/vendors-node_modules_prettier_plugins_postcss_mjs.main.cjs.map +0 -1
  45. package/dist/vendors-node_modules_prettier_plugins_typescript_mjs.main.cjs +0 -43
  46. package/dist/vendors-node_modules_prettier_plugins_typescript_mjs.main.cjs.map +0 -1
  47. package/dist/vendors-node_modules_prettier_plugins_yaml_mjs.main.cjs +0 -187
  48. package/dist/vendors-node_modules_prettier_plugins_yaml_mjs.main.cjs.map +0 -1
  49. package/dist/vendors-node_modules_typanion_lib_index_js.main.cjs +0 -1323
  50. package/dist/vendors-node_modules_typanion_lib_index_js.main.cjs.map +0 -1
@@ -0,0 +1,130 @@
1
+ import chalk from "chalk";
2
+ import { repeat } from "complete-common";
3
+ import { $q, copyFileOrDirectory, getFileNamesInDirectory, getPackageManagerInstallCICommand, getPackageManagerInstallCommand, isFile, makeDirectory, readFile, renameFile, updatePackageJSONDependencies, writeFile, } from "complete-node";
4
+ import path from "node:path";
5
+ import { ACTION_YML, ACTION_YML_TEMPLATE_PATH, TEMPLATES_DYNAMIC_DIR, TEMPLATES_STATIC_DIR, } from "../../constants.js";
6
+ import { initGitRepository } from "../../git.js";
7
+ import { promptError, promptLog, promptSpinnerStart } from "../../prompt.js";
8
+ export async function createProject(projectName, authorName, projectPath, createNewDir, gitRemoteURL, skipInstall, packageManager) {
9
+ if (createNewDir) {
10
+ makeDirectory(projectPath);
11
+ }
12
+ copyStaticFiles(projectPath);
13
+ copyDynamicFiles(projectName, authorName, projectPath, packageManager);
14
+ // There is no package manager lock files yet, so we have to pass "false" to this function.
15
+ const updated = await updatePackageJSONDependencies(projectPath, false, true);
16
+ if (!updated) {
17
+ promptError('Failed to update the dependencies in the "package.json" file.');
18
+ }
19
+ await installNodeModules(projectPath, skipInstall, packageManager);
20
+ await formatFiles(projectPath);
21
+ // Only make the initial commit once all of the files have been copied and formatted.
22
+ await initGitRepository(projectPath, gitRemoteURL);
23
+ promptLog(`Successfully created project: ${chalk.green(projectName)}`);
24
+ }
25
+ /** Copy static files, like "eslint.config.mjs", "tsconfig.json", etc. */
26
+ function copyStaticFiles(projectPath) {
27
+ copyTemplateDirectoryWithoutOverwriting(TEMPLATES_STATIC_DIR, projectPath);
28
+ // Rename "_gitattributes" to ".gitattributes". (If it is kept as ".gitattributes", then it won't
29
+ // be committed to git.)
30
+ const gitAttributesPath = path.join(projectPath, "_gitattributes");
31
+ const correctGitAttributesPath = path.join(projectPath, ".gitattributes");
32
+ renameFile(gitAttributesPath, correctGitAttributesPath);
33
+ // Rename "_cspell.config.jsonc" to "cspell.config.jsonc". (If it is kept as
34
+ // "cspell.config.jsonc", then local spell checking will fail.)
35
+ const cSpellConfigPath = path.join(projectPath, "_cspell.config.jsonc");
36
+ const correctCSpellConfigPath = path.join(projectPath, "cspell.config.jsonc");
37
+ renameFile(cSpellConfigPath, correctCSpellConfigPath);
38
+ }
39
+ function copyTemplateDirectoryWithoutOverwriting(templateDirPath, projectPath) {
40
+ const fileNames = getFileNamesInDirectory(templateDirPath);
41
+ for (const fileName of fileNames) {
42
+ const templateFilePath = path.join(templateDirPath, fileName);
43
+ const destinationFilePath = path.join(projectPath, fileName);
44
+ if (!isFile(destinationFilePath)) {
45
+ copyFileOrDirectory(templateFilePath, destinationFilePath);
46
+ }
47
+ }
48
+ }
49
+ /** Copy files that need to have text replaced inside of them. */
50
+ function copyDynamicFiles(projectName, authorName, projectPath, packageManager) {
51
+ // `.github/workflows/setup/action.yml`
52
+ {
53
+ const fileName = ACTION_YML;
54
+ const templatePath = ACTION_YML_TEMPLATE_PATH;
55
+ const template = readFile(templatePath);
56
+ const installCommand = getPackageManagerInstallCICommand(packageManager);
57
+ const actionYML = template
58
+ .replaceAll("PACKAGE_MANAGER_NAME", packageManager)
59
+ .replaceAll("PACKAGE_MANAGER_INSTALL_COMMAND", installCommand);
60
+ const setupPath = path.join(projectPath, ".github", "workflows", "setup");
61
+ makeDirectory(setupPath);
62
+ const destinationPath = path.join(setupPath, fileName);
63
+ writeFile(destinationPath, actionYML);
64
+ }
65
+ // `.gitignore`
66
+ {
67
+ const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "_gitignore");
68
+ const template = readFile(templatePath);
69
+ // Prepend a header with the project name.
70
+ let separatorLine = "# ";
71
+ repeat(projectName.length, () => {
72
+ separatorLine += "-";
73
+ });
74
+ separatorLine += "\n";
75
+ const gitIgnoreHeader = `${separatorLine}# ${projectName}\n${separatorLine}\n`;
76
+ const nodeGitIgnorePath = path.join(TEMPLATES_DYNAMIC_DIR, "Node.gitignore");
77
+ const nodeGitIgnore = readFile(nodeGitIgnorePath);
78
+ // eslint-disable-next-line prefer-template
79
+ const gitignore = gitIgnoreHeader + template + "\n" + nodeGitIgnore;
80
+ // We need to replace the underscore with a period.
81
+ const destinationPath = path.join(projectPath, ".gitignore");
82
+ writeFile(destinationPath, gitignore);
83
+ }
84
+ // `package.json`
85
+ {
86
+ const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "package.json");
87
+ const template = readFile(templatePath);
88
+ const packageJSON = template
89
+ .replaceAll("project-name", projectName)
90
+ .replaceAll("author-name", authorName ?? "unknown");
91
+ const destinationPath = path.join(projectPath, "package.json");
92
+ writeFile(destinationPath, packageJSON);
93
+ }
94
+ // `README.md`
95
+ {
96
+ const templatePath = path.join(TEMPLATES_DYNAMIC_DIR, "README.md");
97
+ const template = readFile(templatePath);
98
+ // "PROJECT-NAME" must be hyphenated, as using an underscore will break Prettier for some
99
+ // reason.
100
+ const command = getPackageManagerInstallCICommand(packageManager);
101
+ const readmeMD = template
102
+ .replaceAll("PROJECT-NAME", projectName)
103
+ .replaceAll("PACKAGE-MANAGER-INSTALL-COMMAND", command);
104
+ const destinationPath = path.join(projectPath, "README.md");
105
+ writeFile(destinationPath, readmeMD);
106
+ }
107
+ const srcPath = path.join(projectPath, "src");
108
+ makeDirectory(srcPath);
109
+ }
110
+ async function installNodeModules(projectPath, skipInstall, packageManager) {
111
+ if (skipInstall) {
112
+ return;
113
+ }
114
+ const command = getPackageManagerInstallCommand(packageManager);
115
+ const s = promptSpinnerStart(`Installing the project dependencies with "${command}". (This can take a long time.)`);
116
+ try {
117
+ const $$q = $q({ cwd: projectPath });
118
+ const commandParts = command.split(" ");
119
+ await $$q `${commandParts}`;
120
+ s.stop("Installed.");
121
+ }
122
+ catch {
123
+ s.stop("Failed to install.");
124
+ promptError("Exiting.");
125
+ }
126
+ }
127
+ async function formatFiles(projectPath) {
128
+ const $$q = $q({ cwd: projectPath });
129
+ await $$q `prettier --write ${projectPath}`;
130
+ }
@@ -0,0 +1,17 @@
1
+ import { getGitHubUsername } from "../../git.js";
2
+ import { getInputString, promptError, promptLog } from "../../prompt.js";
3
+ export async function getAuthorName() {
4
+ const gitHubUsername = await getGitHubUsername();
5
+ if (gitHubUsername !== undefined) {
6
+ return gitHubUsername;
7
+ }
8
+ return await getNewAuthorName();
9
+ }
10
+ async function getNewAuthorName() {
11
+ promptLog("The author name was not found from the GitHub CLI configuration file.");
12
+ const authorName = await getInputString("Enter the author of the project:");
13
+ if (authorName === "") {
14
+ promptError("You must enter an author name.");
15
+ }
16
+ return authorName;
17
+ }
@@ -0,0 +1,80 @@
1
+ import chalk from "chalk";
2
+ import { hasWhitespace, isKebabCase } from "complete-common";
3
+ import path from "node:path";
4
+ import { CURRENT_DIRECTORY_NAME, CWD } from "../../constants.js";
5
+ import { getInputString, getInputYesNo, promptError, promptLog, } from "../../prompt.js";
6
+ // From: https://gist.github.com/doctaphred/d01d05291546186941e1b7ddc02034d3
7
+ const ILLEGAL_CHARACTERS_FOR_WINDOWS_FILENAMES = [
8
+ "<",
9
+ ">",
10
+ ":",
11
+ '"',
12
+ "/",
13
+ "\\",
14
+ "|",
15
+ "?",
16
+ "*",
17
+ ];
18
+ export async function getProjectPath(name, useCurrentDirectory, customDirectory, yes, forceName) {
19
+ let projectName = name;
20
+ let projectPath;
21
+ let createNewDir;
22
+ if (useCurrentDirectory) {
23
+ // The "--use-current-directory" command-line flag was specified, so there is no need to prompt
24
+ // the user.
25
+ projectName = CURRENT_DIRECTORY_NAME;
26
+ projectPath = CWD;
27
+ createNewDir = false;
28
+ }
29
+ else if (projectName !== undefined) {
30
+ // The project name was specified on the command-line.
31
+ const baseDirectory = customDirectory === undefined ? CWD : path.join(CWD, customDirectory);
32
+ projectPath = path.join(baseDirectory, projectName);
33
+ createNewDir = true;
34
+ }
35
+ else if (yes) {
36
+ // The "--yes" command-line flag was specified and the project name was not specified on the
37
+ // command-line, so default to using the current directory.
38
+ projectName = CURRENT_DIRECTORY_NAME;
39
+ projectPath = CWD;
40
+ createNewDir = false;
41
+ }
42
+ else {
43
+ // The project name was not specified on the command-line, so prompt the user for it.
44
+ [projectName, projectPath, createNewDir] = await getNewProjectName();
45
+ }
46
+ validateProjectName(projectName, forceName);
47
+ promptLog(`Using a project name of: ${chalk.green(projectName)}`);
48
+ return { projectPath, createNewDir };
49
+ }
50
+ async function getNewProjectName() {
51
+ promptLog("You did not specify a project name as a command-line argument.");
52
+ const shouldUseCurrentDir = await getInputYesNo(`Would you like to create a new project using the current directory "${chalk.green(CURRENT_DIRECTORY_NAME)}" as the root?`);
53
+ if (shouldUseCurrentDir) {
54
+ return [CURRENT_DIRECTORY_NAME, CWD, false];
55
+ }
56
+ const projectName = await getInputString("Enter the name of the project:");
57
+ const projectPath = path.join(CWD, projectName);
58
+ return [projectName, projectPath, true];
59
+ }
60
+ function validateProjectName(projectName, forceName) {
61
+ if (projectName === "") {
62
+ promptError("You cannot have a blank project name.");
63
+ }
64
+ if (process.platform === "win32") {
65
+ for (const character of ILLEGAL_CHARACTERS_FOR_WINDOWS_FILENAMES) {
66
+ if (projectName.includes(character)) {
67
+ promptError(`The "${character}" character is not allowed in a Windows file name.`);
68
+ }
69
+ }
70
+ }
71
+ if (forceName) {
72
+ return;
73
+ }
74
+ if (hasWhitespace(projectName)) {
75
+ promptError('The project name has whitespace in it, which is not allowed. Use kebab-case for your project name. (e.g. "green-candle")');
76
+ }
77
+ if (!isKebabCase(projectName)) {
78
+ promptError('The project name is not in kebab-case. (Kebab-case is the style of using all lowercase letters, with words separated by hyphens.) Project names must use kebab-case to match GitHub repository standards. If necessary, you can override this check with the "--force-name" flag.');
79
+ }
80
+ }
@@ -0,0 +1,35 @@
1
+ import chalk from "chalk";
2
+ import { commandExists, PackageManager } from "complete-node";
3
+ import { DEFAULT_PACKAGE_MANAGER } from "../../constants.js";
4
+ import { promptError } from "../../prompt.js";
5
+ export async function getPackageManagerUsedForNewProject(options) {
6
+ const packageManagerFromOptions = await getPackageManagerFromOptions(options);
7
+ if (packageManagerFromOptions !== undefined) {
8
+ return packageManagerFromOptions;
9
+ }
10
+ return DEFAULT_PACKAGE_MANAGER;
11
+ }
12
+ async function getPackageManagerFromOptions(options) {
13
+ if (options.npm) {
14
+ const npmExists = await commandExists("npm");
15
+ if (!npmExists) {
16
+ promptError(`You specified the "--npm" option, but "${chalk.green("npm")}" does not seem to be a valid command.`);
17
+ }
18
+ return PackageManager.npm;
19
+ }
20
+ if (options.yarn) {
21
+ const yarnExists = await commandExists("yarn");
22
+ if (!yarnExists) {
23
+ promptError(`You specified the "--yarn" option, but "${chalk.green("yarn")}" does not seem to be a valid command.`);
24
+ }
25
+ return PackageManager.yarn;
26
+ }
27
+ if (options.pnpm) {
28
+ const pnpmExists = await commandExists("pnpm");
29
+ if (!pnpmExists) {
30
+ promptError(`You specified the "--pnpm" option, but "${chalk.green("pnpm")}" does not seem to be a valid command.`);
31
+ }
32
+ return PackageManager.pnpm;
33
+ }
34
+ return undefined;
35
+ }
@@ -0,0 +1,82 @@
1
+ import { $, $q, commandExists, getJSONC, isFile } from "complete-node";
2
+ import path from "node:path";
3
+ import { getInputYesNo, promptError, promptLog } from "../../prompt.js";
4
+ const VS_CODE_COMMANDS = [
5
+ "code",
6
+ "codium",
7
+ "code-oss",
8
+ "code-insiders",
9
+ ];
10
+ export async function vsCodeInit(projectPath, vscode, yes) {
11
+ const VSCodeCommand = await getVSCodeCommand();
12
+ if (VSCodeCommand === undefined) {
13
+ promptLog('VSCode does not seem to be installed. (The "code" command is not in the path.) Skipping VSCode-related things.');
14
+ return;
15
+ }
16
+ await installVSCodeExtensions(projectPath, VSCodeCommand);
17
+ await promptVSCode(projectPath, VSCodeCommand, vscode, yes);
18
+ }
19
+ async function getVSCodeCommand() {
20
+ const commandCheckPromises = VS_CODE_COMMANDS.map((command) => ({
21
+ command,
22
+ existsPromise: commandExists(command),
23
+ }));
24
+ const commandChecks = await Promise.all(commandCheckPromises.map(async (check) => ({
25
+ command: check.command,
26
+ exists: await check.existsPromise,
27
+ })));
28
+ const existingCommands = commandChecks
29
+ .filter((check) => check.exists)
30
+ .map((check) => check.command);
31
+ return existingCommands[0];
32
+ }
33
+ async function installVSCodeExtensions(projectPath, vsCodeCommand) {
34
+ // Installing extensions from inside WSL on Windows will result in the VSCode process never
35
+ // exiting for some reason. Thus, skip this step on Linux. (Linux users will probably be smart
36
+ // enough to install the extensions on their own.)
37
+ if (process.platform === "linux") {
38
+ return;
39
+ }
40
+ const extensions = await getExtensionsFromJSON(projectPath);
41
+ await Promise.all(extensions.map(async (extension) => await $q `${vsCodeCommand} --install-extension ${extension}`));
42
+ }
43
+ async function getExtensionsFromJSON(projectPath) {
44
+ const extensionsJSONPath = path.join(projectPath, ".vscode", "extensions.json");
45
+ if (!isFile(extensionsJSONPath)) {
46
+ return [];
47
+ }
48
+ const extensionsJSON = await getJSONC(extensionsJSONPath);
49
+ const { recommendations } = extensionsJSON;
50
+ if (!Array.isArray(recommendations)) {
51
+ promptError('The "recommendations" field in the "extensions.json" file is not an array.');
52
+ }
53
+ for (const recommendation of recommendations) {
54
+ if (typeof recommendation !== "string") {
55
+ promptError('One of the entries in the "recommendations" field in the "extensions.json" file is not a string.');
56
+ }
57
+ }
58
+ return recommendations;
59
+ }
60
+ async function promptVSCode(projectPath, VSCodeCommand, vscode, yes) {
61
+ if (vscode) {
62
+ // They supplied the "--vscode" command-line flag, so there is no need to prompt the user.
63
+ await openVSCode(projectPath, VSCodeCommand);
64
+ return;
65
+ }
66
+ if (yes) {
67
+ // They supplied the "--yes" command-line flag, which implies that they want a silent install,
68
+ // so skip opening VSCode.
69
+ return;
70
+ }
71
+ // The VSCode command does not work properly inside WSL on Windows.
72
+ if (process.platform === "linux") {
73
+ return;
74
+ }
75
+ const shouldOpenVSCode = await getInputYesNo("Do you want to open your new project in VSCode now?");
76
+ if (shouldOpenVSCode) {
77
+ await openVSCode(projectPath, VSCodeCommand);
78
+ }
79
+ }
80
+ async function openVSCode(projectPath, VSCodeCommand) {
81
+ await $ `${VSCodeCommand} ${projectPath}`;
82
+ }
@@ -0,0 +1,17 @@
1
+ import { findPackageRoot, getPackageJSONFieldsMandatory, PackageManager, } from "complete-node";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export const CWD = process.cwd();
5
+ export const CURRENT_DIRECTORY_NAME = path.basename(CWD);
6
+ export const HOME_DIR = os.homedir();
7
+ const packageRoot = findPackageRoot();
8
+ const { name, version } = getPackageJSONFieldsMandatory(packageRoot, "name", "version");
9
+ export const PROJECT_NAME = name;
10
+ export const PROJECT_VERSION = version;
11
+ export const DEFAULT_PACKAGE_MANAGER = PackageManager.npm;
12
+ // ---------
13
+ const TEMPLATES_DIR = path.join(packageRoot, "file-templates");
14
+ export const TEMPLATES_STATIC_DIR = path.join(TEMPLATES_DIR, "static");
15
+ export const TEMPLATES_DYNAMIC_DIR = path.join(TEMPLATES_DIR, "dynamic");
16
+ export const ACTION_YML = "action.yml";
17
+ export const ACTION_YML_TEMPLATE_PATH = path.join(TEMPLATES_DYNAMIC_DIR, ".github", "workflows", "setup", ACTION_YML);
package/dist/git.js ADDED
@@ -0,0 +1,132 @@
1
+ import chalk from "chalk";
2
+ import { $, commandExists, isFileAsync, readFileAsync } from "complete-node";
3
+ import path from "node:path";
4
+ import yaml from "yaml";
5
+ import { HOME_DIR, PROJECT_NAME, PROJECT_VERSION } from "./constants.js";
6
+ import { getInputString, getInputYesNo, promptLog } from "./prompt.js";
7
+ /**
8
+ * If the GitHub CLI is installed, we can derive the user's GitHub username from their YAML
9
+ * configuration.
10
+ */
11
+ export async function getGitHubUsername() {
12
+ const ghExists = await commandExists("gh");
13
+ if (!ghExists) {
14
+ return undefined;
15
+ }
16
+ const githubCLIHostsPath = getGithubCLIHostsPath();
17
+ if (githubCLIHostsPath === undefined) {
18
+ return undefined;
19
+ }
20
+ const hostsPathExists = await isFileAsync(githubCLIHostsPath);
21
+ if (!hostsPathExists) {
22
+ return undefined;
23
+ }
24
+ const configYAMLRaw = await readFileAsync(githubCLIHostsPath);
25
+ const configYAML = yaml.parse(configYAMLRaw);
26
+ const githubCom = configYAML["github.com"];
27
+ if (githubCom === undefined) {
28
+ return undefined;
29
+ }
30
+ const { user } = githubCom;
31
+ if (user === undefined || user === "") {
32
+ return undefined;
33
+ }
34
+ return user;
35
+ }
36
+ function getGithubCLIHostsPath() {
37
+ if (process.platform === "win32") {
38
+ const appData = process.env["APPDATA"];
39
+ if (appData === undefined || appData === "") {
40
+ return undefined;
41
+ }
42
+ return path.join(appData, "GitHub CLI", "hosts.yml");
43
+ }
44
+ // The location is the same on both macOS and Linux.
45
+ return path.join(HOME_DIR, ".config", "gh", "hosts.yml");
46
+ }
47
+ /** @returns The git remote URL. For example: git@github.com:alice/foo.git */
48
+ export async function promptGitHubRepoOrGitRemoteURL(projectName, yes, skipGit) {
49
+ if (skipGit) {
50
+ return undefined;
51
+ }
52
+ // Hard-code certain project names as never causing a Git repository to be initialized.
53
+ if (projectName.startsWith("test") || projectName === "foo") {
54
+ return undefined;
55
+ }
56
+ // We do not need to prompt the user if they do not have Git installed.
57
+ const gitExists = await commandExists("git");
58
+ if (!gitExists) {
59
+ promptLog('Git does not seem to be installed. (The "git" command is not in the path.) Skipping Git-related things.');
60
+ return undefined;
61
+ }
62
+ const gitHubUsername = await getGitHubUsername();
63
+ if (gitHubUsername !== undefined) {
64
+ const { exitCode } = await $ `gh repo view ${projectName}`;
65
+ const gitHubRepoExists = exitCode === 0;
66
+ const url = `https://github.com/${gitHubUsername}/${projectName}`;
67
+ if (gitHubRepoExists) {
68
+ promptLog(`Detected an existing GitHub repository at: ${chalk.green(url)}`);
69
+ const guessedRemoteURL = getGitRemoteURL(projectName, gitHubUsername);
70
+ if (yes) {
71
+ promptLog(`Using a Git remote URL of: ${chalk.green(guessedRemoteURL)}`);
72
+ return guessedRemoteURL;
73
+ }
74
+ const shouldUseGuessedURL = await getInputYesNo(`Do you want to use a Git remote URL of: ${chalk.green(guessedRemoteURL)}`);
75
+ if (shouldUseGuessedURL) {
76
+ return guessedRemoteURL;
77
+ }
78
+ // Assume that since they do not want to connect this project to the existing GitHub
79
+ // repository, they do not want to initialize a remote Git URL at all.
80
+ return undefined;
81
+ }
82
+ if (yes) {
83
+ await $ `gh repo create ${projectName} --public`;
84
+ promptLog(`Created a new GitHub repository at: ${chalk.green(url)}`);
85
+ return getGitRemoteURL(projectName, gitHubUsername);
86
+ }
87
+ const createNewGitHubRepo = await getInputYesNo(`Would you like to create a new GitHub repository at: ${chalk.green(url)}`);
88
+ if (createNewGitHubRepo) {
89
+ await $ `gh repo create ${projectName} --public`;
90
+ promptLog("Successfully created a new GitHub repository.");
91
+ return getGitRemoteURL(projectName, gitHubUsername);
92
+ }
93
+ // Assume that since they do not want to create a new GitHub repository, they do not want to
94
+ // initialize a remote Git URL at all.
95
+ return undefined;
96
+ }
97
+ const gitRemoteURL = await getInputString(`Paste in the remote Git URL for your project.
98
+ For example, if you have an SSH key, it would be something like:
99
+ ${chalk.green("git@github.com:Alice/green-candle.git")}
100
+ If you don't have an SSH key, it would be something like:
101
+ ${chalk.green("https://github.com/Alice/green-candle.git")}
102
+ If you don't want to initialize a Git repository for this project, press enter to skip.
103
+ `);
104
+ return gitRemoteURL === "" ? undefined : gitRemoteURL;
105
+ }
106
+ function getGitRemoteURL(projectName, gitHubUsername) {
107
+ return `git@github.com:${gitHubUsername}/${projectName}.git`;
108
+ }
109
+ export async function initGitRepository(projectPath, gitRemoteURL) {
110
+ const gitExists = await commandExists("git");
111
+ if (!gitExists) {
112
+ return;
113
+ }
114
+ if (gitRemoteURL === undefined) {
115
+ return;
116
+ }
117
+ const $$ = $({ cwd: projectPath });
118
+ await $$ `git init --initial-branch main`;
119
+ await $$ `git remote add origin ${gitRemoteURL}`;
120
+ const gitNameAndEmailConfigured = await isGitNameAndEmailConfigured();
121
+ if (gitNameAndEmailConfigured) {
122
+ await $$ `git add --all`;
123
+ const commitMessage = `chore: add files from ${PROJECT_NAME} ${PROJECT_VERSION} template`;
124
+ await $$ `git commit --message ${commitMessage}`;
125
+ await $$ `git push --set-upstream origin main`;
126
+ }
127
+ }
128
+ async function isGitNameAndEmailConfigured() {
129
+ const { exitCode: nameExitCode } = await $ `git config --global user.name`;
130
+ const { exitCode: emailExitCode } = await $ `git config --global user.email`;
131
+ return nameExitCode === 0 && emailExitCode === 0;
132
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { Builtins, Cli } from "clipanion";
3
+ import { CheckCommand } from "./commands/CheckCommand.js";
4
+ import { InitCommand } from "./commands/InitCommand.js";
5
+ import { NukeCommand } from "./commands/NukeCommand.js";
6
+ import { PublishCommand } from "./commands/PublishCommand.js";
7
+ import { UpdateCommand } from "./commands/UpdateCommand.js";
8
+ import { PROJECT_NAME, PROJECT_VERSION } from "./constants.js";
9
+ await main();
10
+ async function main() {
11
+ const [_node, _app, ...args] = process.argv;
12
+ const cli = new Cli({
13
+ binaryLabel: PROJECT_NAME,
14
+ binaryName: PROJECT_NAME,
15
+ binaryVersion: PROJECT_VERSION,
16
+ });
17
+ cli.register(CheckCommand);
18
+ cli.register(InitCommand);
19
+ cli.register(NukeCommand);
20
+ cli.register(PublishCommand);
21
+ cli.register(UpdateCommand);
22
+ cli.register(Builtins.HelpCommand);
23
+ await cli.runExit(args);
24
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,53 @@
1
+ // Both the Inquirer.js library and the Prompts library have a bug where text is duplicated in a Git
2
+ // Bash terminal. Thus, we revert to using the simpler Prompt library.
3
+ import { cancel, confirm, intro, isCancel, log, outro, spinner, text, } from "@clack/prompts";
4
+ import chalk from "chalk";
5
+ import { PROJECT_NAME } from "./constants.js";
6
+ export function promptStart() {
7
+ intro(chalk.inverse(PROJECT_NAME));
8
+ }
9
+ export function promptEnd(msg) {
10
+ outro(msg);
11
+ process.exit();
12
+ }
13
+ export async function getInputYesNo(msg, defaultValue = true) {
14
+ const input = await confirm({
15
+ message: msg,
16
+ initialValue: defaultValue,
17
+ });
18
+ if (isCancel(input)) {
19
+ cancel("Canceled.");
20
+ process.exit(1);
21
+ }
22
+ return input;
23
+ }
24
+ /** Returns trimmed input. */
25
+ export async function getInputString(msg, defaultValue) {
26
+ const input = await text({
27
+ message: msg,
28
+ initialValue: defaultValue,
29
+ });
30
+ if (isCancel(input)) {
31
+ cancel("Canceled.");
32
+ process.exit(1);
33
+ }
34
+ const trimmedInput = input.trim();
35
+ if (trimmedInput === "") {
36
+ promptError("You must enter a non-empty value.");
37
+ }
38
+ return input.trim();
39
+ }
40
+ export function promptLog(msg) {
41
+ log.step(msg); // Step is a hollow green diamond.
42
+ }
43
+ export function promptSpinnerStart(msg) {
44
+ const s = spinner({
45
+ indicator: "timer",
46
+ });
47
+ s.start(msg);
48
+ return s;
49
+ }
50
+ export function promptError(msg) {
51
+ cancel(msg);
52
+ process.exit(1);
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "complete-cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "A command line tool for bootstrapping TypeScript projects.",
5
5
  "keywords": [
6
6
  "typescript"
@@ -17,7 +17,7 @@
17
17
  "author": "Zamiell",
18
18
  "type": "module",
19
19
  "bin": {
20
- "complete-cli": "./dist/main.cjs"
20
+ "complete-cli": "./dist/main.js"
21
21
  },
22
22
  "files": [
23
23
  "dist",
@@ -33,26 +33,26 @@
33
33
  "start": "tsx ./src/main.ts",
34
34
  "test": "glob \"./src/**/*.test.ts\" --cmd=\"node --import tsx --test --test-reporter spec\""
35
35
  },
36
- "devDependencies": {
36
+ "dependencies": {
37
37
  "@clack/prompts": "0.10.0",
38
- "@types/klaw-sync": "6.0.5",
39
- "@types/node": "22.13.5",
40
- "@types/source-map-support": "^0.5.10",
41
38
  "chalk": "5.4.1",
42
39
  "clipanion": "4.0.0-rc.4",
43
40
  "complete-common": "^1.1.1",
44
41
  "complete-node": "^3.0.1",
45
- "glob": "11.0.1",
46
42
  "klaw-sync": "6.0.0",
47
- "source-map-support": "^0.5.21",
43
+ "yaml": "2.7.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/klaw-sync": "6.0.5",
47
+ "@types/node": "22.13.5",
48
+ "glob": "11.0.1",
48
49
  "ts-loader": "9.5.2",
49
50
  "tsconfig-paths-webpack-plugin": "^4.2.0",
50
51
  "typescript": "5.7.3",
51
52
  "typescript-eslint": "8.24.1",
52
53
  "webpack": "5.98.0",
53
54
  "webpack-cli": "6.0.1",
54
- "webpack-shebang-plugin": "^1.1.8",
55
- "yaml": "2.7.0"
55
+ "webpack-shebang-plugin": "^1.1.8"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">= 20.11.0"
package/src/main.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Builtins, Cli } from "clipanion";
4
- import { install } from "source-map-support";
5
4
  import { CheckCommand } from "./commands/CheckCommand.js";
6
5
  import { InitCommand } from "./commands/InitCommand.js";
7
6
  import { NukeCommand } from "./commands/NukeCommand.js";
@@ -12,8 +11,6 @@ import { PROJECT_NAME, PROJECT_VERSION } from "./constants.js";
12
11
  await main();
13
12
 
14
13
  async function main(): Promise<void> {
15
- install();
16
-
17
14
  const [_node, _app, ...args] = process.argv;
18
15
 
19
16
  const cli = new Cli({