complete-cli 1.0.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +7 -0
  3. package/dist/commands/CheckCommand.js +141 -0
  4. package/dist/commands/InitCommand.js +64 -0
  5. package/dist/commands/NukeCommand.js +13 -0
  6. package/dist/commands/PublishCommand.js +158 -0
  7. package/dist/commands/UpdateCommand.js +16 -0
  8. package/dist/commands/check/check.test.js +86 -0
  9. package/dist/commands/check/getTruncatedText.js +139 -0
  10. package/dist/commands/init/checkIfProjectPathExists.js +21 -0
  11. package/dist/commands/init/createProject.js +123 -0
  12. package/dist/commands/init/getAuthorName.js +17 -0
  13. package/dist/commands/init/getProjectPath.js +80 -0
  14. package/dist/commands/init/packageManager.js +35 -0
  15. package/dist/commands/init/vsCodeInit.js +74 -0
  16. package/dist/constants.js +17 -0
  17. package/dist/git.js +128 -0
  18. package/dist/interfaces/GitHubCLIHostsYAML.js +1 -0
  19. package/dist/main.js +26 -0
  20. package/dist/prompt.js +46 -0
  21. package/dist/validateNoteVersion.js +25 -0
  22. package/file-templates/dynamic/.github/workflows/setup/action.yml +13 -0
  23. package/file-templates/dynamic/Node.gitignore +130 -0
  24. package/file-templates/dynamic/README.md +3 -0
  25. package/file-templates/dynamic/_gitignore +9 -0
  26. package/file-templates/dynamic/package.json +37 -0
  27. package/file-templates/static/.github/workflows/ci.yml +49 -0
  28. package/file-templates/static/.prettierignore +12 -0
  29. package/file-templates/static/.vscode/extensions.json +9 -0
  30. package/file-templates/static/.vscode/settings.json +75 -0
  31. package/file-templates/static/LICENSE +674 -0
  32. package/file-templates/static/_cspell.config.jsonc +25 -0
  33. package/file-templates/static/_gitattributes +37 -0
  34. package/file-templates/static/eslint.config.mjs +18 -0
  35. package/file-templates/static/knip.config.js +20 -0
  36. package/file-templates/static/prettier.config.mjs +24 -0
  37. package/file-templates/static/scripts/build.ts +5 -0
  38. package/file-templates/static/scripts/lint.ts +30 -0
  39. package/file-templates/static/scripts/tsconfig.json +14 -0
  40. package/file-templates/static/src/main.ts +5 -0
  41. package/file-templates/static/tsconfig.json +12 -0
  42. package/package.json +59 -0
  43. package/src/commands/CheckCommand.ts +249 -0
  44. package/src/commands/InitCommand.ts +105 -0
  45. package/src/commands/NukeCommand.ts +17 -0
  46. package/src/commands/PublishCommand.ts +242 -0
  47. package/src/commands/UpdateCommand.ts +20 -0
  48. package/src/commands/check/check.test.ts +123 -0
  49. package/src/commands/check/getTruncatedText.ts +187 -0
  50. package/src/commands/init/checkIfProjectPathExists.ts +36 -0
  51. package/src/commands/init/createProject.ts +197 -0
  52. package/src/commands/init/getAuthorName.ts +23 -0
  53. package/src/commands/init/getProjectPath.ts +112 -0
  54. package/src/commands/init/packageManager.ts +64 -0
  55. package/src/commands/init/vsCodeInit.ts +115 -0
  56. package/src/constants.ts +39 -0
  57. package/src/git.ts +182 -0
  58. package/src/interfaces/GitHubCLIHostsYAML.ts +7 -0
  59. package/src/main.ts +34 -0
  60. package/src/prompt.ts +72 -0
  61. package/src/validateNoteVersion.ts +39 -0
package/src/git.ts ADDED
@@ -0,0 +1,182 @@
1
+ import chalk from "chalk";
2
+ import { $, commandExists, isFile, readFile } 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 type { GitHubCLIHostsYAML } from "./interfaces/GitHubCLIHostsYAML.js";
7
+ import { getInputString, getInputYesNo, promptLog } from "./prompt.js";
8
+
9
+ /**
10
+ * If the GitHub CLI is installed, we can derive the user's GitHub username from their YAML
11
+ * configuration.
12
+ */
13
+ export function getGitHubUsername(): string | undefined {
14
+ if (!commandExists("gh")) {
15
+ return undefined;
16
+ }
17
+
18
+ const githubCLIHostsPath = getGithubCLIHostsPath();
19
+ if (githubCLIHostsPath === undefined) {
20
+ return undefined;
21
+ }
22
+
23
+ if (!isFile(githubCLIHostsPath)) {
24
+ return undefined;
25
+ }
26
+
27
+ const configYAMLRaw = readFile(githubCLIHostsPath);
28
+ const configYAML = yaml.parse(configYAMLRaw) as GitHubCLIHostsYAML;
29
+
30
+ const githubCom = configYAML["github.com"];
31
+ if (githubCom === undefined) {
32
+ return undefined;
33
+ }
34
+
35
+ const { user } = githubCom;
36
+ if (user === undefined || user === "") {
37
+ return undefined;
38
+ }
39
+
40
+ return user;
41
+ }
42
+
43
+ function getGithubCLIHostsPath(): string | undefined {
44
+ if (process.platform === "win32") {
45
+ const appData = process.env["APPDATA"];
46
+ if (appData === undefined || appData === "") {
47
+ return undefined;
48
+ }
49
+
50
+ return path.join(appData, "GitHub CLI", "hosts.yml");
51
+ }
52
+
53
+ // The location is the same on both macOS and Linux.
54
+ return path.join(HOME_DIR, ".config", "gh", "hosts.yml");
55
+ }
56
+
57
+ /** @returns The git remote URL. For example: git@github.com:alice/foo.git */
58
+ export async function promptGitHubRepoOrGitRemoteURL(
59
+ projectName: string,
60
+ yes: boolean,
61
+ skipGit: boolean,
62
+ ): Promise<string | undefined> {
63
+ if (skipGit) {
64
+ return undefined;
65
+ }
66
+
67
+ // Hard-code certain project names as never causing a Git repository to be initialized.
68
+ if (projectName.startsWith("test") || projectName === "foo") {
69
+ return undefined;
70
+ }
71
+
72
+ // We do not need to prompt the user if they do not have Git installed.
73
+ if (!commandExists("git")) {
74
+ promptLog(
75
+ 'Git does not seem to be installed. (The "git" command is not in the path.) Skipping Git-related things.',
76
+ );
77
+ return undefined;
78
+ }
79
+
80
+ const gitHubUsername = getGitHubUsername();
81
+ if (gitHubUsername !== undefined) {
82
+ const { exitCode } = await $`gh repo view ${projectName}`;
83
+ const gitHubRepoExists = exitCode === 0;
84
+ const url = `https://github.com/${gitHubUsername}/${projectName}`;
85
+
86
+ if (gitHubRepoExists) {
87
+ promptLog(
88
+ `Detected an existing GitHub repository at: ${chalk.green(url)}`,
89
+ );
90
+ const guessedRemoteURL = getGitRemoteURL(projectName, gitHubUsername);
91
+
92
+ if (yes) {
93
+ promptLog(
94
+ `Using a Git remote URL of: ${chalk.green(guessedRemoteURL)}`,
95
+ );
96
+ return guessedRemoteURL;
97
+ }
98
+
99
+ const shouldUseGuessedURL = await getInputYesNo(
100
+ `Do you want to use a Git remote URL of: ${chalk.green(
101
+ guessedRemoteURL,
102
+ )}`,
103
+ );
104
+ if (shouldUseGuessedURL) {
105
+ return guessedRemoteURL;
106
+ }
107
+
108
+ // Assume that since they do not want to connect this project to the existing GitHub
109
+ // repository, they do not want to initialize a remote Git URL at all.
110
+ return undefined;
111
+ }
112
+
113
+ if (yes) {
114
+ await $`gh repo create ${projectName} --public`;
115
+ promptLog(`Created a new GitHub repository at: ${chalk.green(url)}`);
116
+ return getGitRemoteURL(projectName, gitHubUsername);
117
+ }
118
+
119
+ const createNewGitHubRepo = await getInputYesNo(
120
+ `Would you like to create a new GitHub repository at: ${chalk.green(
121
+ url,
122
+ )}`,
123
+ );
124
+ if (createNewGitHubRepo) {
125
+ await $`gh repo create ${projectName} --public`;
126
+ promptLog("Successfully created a new GitHub repository.");
127
+ return getGitRemoteURL(projectName, gitHubUsername);
128
+ }
129
+
130
+ // Assume that since they do not want to create a new GitHub repository, they do not want to
131
+ // initialize a remote Git URL at all.
132
+ return undefined;
133
+ }
134
+
135
+ const gitRemoteURL =
136
+ await getInputString(`Paste in the remote Git URL for your project.
137
+ For example, if you have an SSH key, it would be something like:
138
+ ${chalk.green("git@github.com:Alice/green-candle.git")}
139
+ If you don't have an SSH key, it would be something like:
140
+ ${chalk.green("https://github.com/Alice/green-candle.git")}
141
+ If you don't want to initialize a Git repository for this project, press enter to skip.
142
+ `);
143
+
144
+ return gitRemoteURL === "" ? undefined : gitRemoteURL;
145
+ }
146
+
147
+ function getGitRemoteURL(projectName: string, gitHubUsername: string) {
148
+ return `git@github.com:${gitHubUsername}/${projectName}.git`;
149
+ }
150
+
151
+ export async function initGitRepository(
152
+ projectPath: string,
153
+ gitRemoteURL: string | undefined,
154
+ ): Promise<void> {
155
+ if (!commandExists("git")) {
156
+ return;
157
+ }
158
+
159
+ if (gitRemoteURL === undefined) {
160
+ return;
161
+ }
162
+
163
+ const $$ = $({ cwd: projectPath });
164
+
165
+ await $$`git init --initial-branch main`;
166
+ await $$`git remote add origin ${gitRemoteURL}`;
167
+
168
+ const gitNameAndEmailConfigured = await isGitNameAndEmailConfigured();
169
+ if (gitNameAndEmailConfigured) {
170
+ await $$`git add --all`;
171
+ const commitMessage = `chore: add files from ${PROJECT_NAME} ${PROJECT_VERSION} template`;
172
+ await $$`git commit --message ${commitMessage}`;
173
+ await $$`git push --set-upstream origin main`;
174
+ }
175
+ }
176
+
177
+ async function isGitNameAndEmailConfigured(): Promise<boolean> {
178
+ const { exitCode: nameExitCode } = await $`git config --global user.name`;
179
+ const { exitCode: emailExitCode } = await $`git config --global user.email`;
180
+
181
+ return nameExitCode === 0 && emailExitCode === 0;
182
+ }
@@ -0,0 +1,7 @@
1
+ export interface GitHubCLIHostsYAML {
2
+ "github.com"?: {
3
+ user?: string;
4
+ oauth_token?: string;
5
+ git_protocol?: string;
6
+ };
7
+ }
package/src/main.ts ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Builtins, Cli } from "clipanion";
4
+ import { CheckCommand } from "./commands/CheckCommand.js";
5
+ import { InitCommand } from "./commands/InitCommand.js";
6
+ import { NukeCommand } from "./commands/NukeCommand.js";
7
+ import { PublishCommand } from "./commands/PublishCommand.js";
8
+ import { UpdateCommand } from "./commands/UpdateCommand.js";
9
+ import { PROJECT_NAME, PROJECT_VERSION } from "./constants.js";
10
+ import { validateNodeVersion } from "./validateNoteVersion.js";
11
+
12
+ await main();
13
+
14
+ async function main(): Promise<void> {
15
+ validateNodeVersion();
16
+
17
+ const [_node, _app, ...args] = process.argv;
18
+
19
+ const cli = new Cli({
20
+ binaryLabel: PROJECT_NAME,
21
+ binaryName: PROJECT_NAME,
22
+ binaryVersion: PROJECT_VERSION,
23
+ });
24
+
25
+ cli.register(CheckCommand);
26
+ cli.register(InitCommand);
27
+ cli.register(NukeCommand);
28
+ cli.register(PublishCommand);
29
+ cli.register(UpdateCommand);
30
+
31
+ cli.register(Builtins.HelpCommand);
32
+
33
+ await cli.runExit(args);
34
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,72 @@
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
+
4
+ import {
5
+ cancel,
6
+ confirm,
7
+ intro,
8
+ isCancel,
9
+ log,
10
+ outro,
11
+ text,
12
+ } from "@clack/prompts";
13
+ import chalk from "chalk";
14
+ import { PROJECT_NAME } from "./constants.js";
15
+
16
+ export function promptStart(): void {
17
+ intro(chalk.inverse(PROJECT_NAME));
18
+ }
19
+
20
+ export function promptEnd(msg: string): never {
21
+ outro(msg);
22
+ process.exit();
23
+ }
24
+
25
+ export async function getInputYesNo(
26
+ msg: string,
27
+ defaultValue = true,
28
+ ): Promise<boolean> {
29
+ const input = await confirm({
30
+ message: msg,
31
+ initialValue: defaultValue,
32
+ });
33
+
34
+ if (isCancel(input)) {
35
+ cancel("Canceled.");
36
+ process.exit(1);
37
+ }
38
+
39
+ return input;
40
+ }
41
+
42
+ /** Returns trimmed input. */
43
+ export async function getInputString(
44
+ msg: string,
45
+ defaultValue?: string,
46
+ ): Promise<string> {
47
+ const input = await text({
48
+ message: msg,
49
+ initialValue: defaultValue,
50
+ });
51
+
52
+ if (isCancel(input)) {
53
+ cancel("Canceled.");
54
+ process.exit(1);
55
+ }
56
+
57
+ const trimmedInput = input.trim();
58
+ if (trimmedInput === "") {
59
+ promptError("You must enter a non-empty value.");
60
+ }
61
+
62
+ return input.trim();
63
+ }
64
+
65
+ export function promptLog(msg: string): void {
66
+ log.step(msg); // Step is a hollow green diamond.
67
+ }
68
+
69
+ export function promptError(msg: string): never {
70
+ cancel(msg);
71
+ process.exit(1);
72
+ }
@@ -0,0 +1,39 @@
1
+ import chalk from "chalk";
2
+ import { parseSemanticVersion } from "complete-common";
3
+ import { PROJECT_NAME } from "./constants.js";
4
+
5
+ // 20.11 is the minimum version that supports `import.meta.dirname`.
6
+ const REQUIRED_NODE_JS_MAJOR_VERSION = 20;
7
+ const REQUIRED_NODE_JS_MINOR_VERSION = 11;
8
+
9
+ export function validateNodeVersion(): void {
10
+ const { version } = process;
11
+
12
+ const semanticVersion = parseSemanticVersion(version);
13
+ if (semanticVersion === undefined) {
14
+ throw new Error(`Failed to parse the Node version: ${version}`);
15
+ }
16
+
17
+ const { majorVersion, minorVersion } = semanticVersion;
18
+ if (majorVersion > REQUIRED_NODE_JS_MAJOR_VERSION) {
19
+ return;
20
+ }
21
+
22
+ if (
23
+ majorVersion === REQUIRED_NODE_JS_MAJOR_VERSION &&
24
+ minorVersion >= REQUIRED_NODE_JS_MINOR_VERSION
25
+ ) {
26
+ return;
27
+ }
28
+
29
+ console.error(`Your Node.js version is: ${chalk.red(version)}`);
30
+ console.error(
31
+ `${PROJECT_NAME} requires a Node.js version of ${chalk.red(
32
+ `${REQUIRED_NODE_JS_MAJOR_VERSION}.${REQUIRED_NODE_JS_MINOR_VERSION}.0`,
33
+ )} or greater.`,
34
+ );
35
+ console.error(
36
+ `Please upgrade your version of Node.js before using ${PROJECT_NAME}.`,
37
+ );
38
+ process.exit(1);
39
+ }