@theholocron/cli 1.0.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 (95) hide show
  1. package/.alexignore +3 -0
  2. package/.editorconfig +24 -0
  3. package/.editorconfig-checker.json +20 -0
  4. package/.env +3 -0
  5. package/.gitattributes +12 -0
  6. package/.github/CODEOWNERS +1 -0
  7. package/.github/dependabot.yml +6 -0
  8. package/.github/labeler.yml +21 -0
  9. package/.github/workflows/bookkeeping-pr.yml +32 -0
  10. package/.github/workflows/greetings.yml +32 -0
  11. package/.github/workflows/lint.yml +96 -0
  12. package/.github/workflows/publish.yml +71 -0
  13. package/.github/workflows/review.yml +72 -0
  14. package/.github/workflows/stale.yml +25 -0
  15. package/.husky/commit-msg +1 -0
  16. package/.husky/pre-commit +13 -0
  17. package/.husky/prepare-commit-msg +18 -0
  18. package/LICENSE +674 -0
  19. package/README.md +17 -0
  20. package/commitlint.config.js +9 -0
  21. package/eslint.config.js +9 -0
  22. package/media/README.md +11 -0
  23. package/media/error.mp3 +0 -0
  24. package/media/success.mp3 +0 -0
  25. package/media/warning.mp3 +0 -0
  26. package/package.json +71 -0
  27. package/prettier.config.js +11 -0
  28. package/src/cli.ts +65 -0
  29. package/src/commands/README.md +13 -0
  30. package/src/commands/bootstrap.ts +102 -0
  31. package/src/commands/conf/README.md +45 -0
  32. package/src/commands/conf/add.ts +49 -0
  33. package/src/commands/conf/edit.ts +12 -0
  34. package/src/commands/conf/view.ts +40 -0
  35. package/src/commands/conf.ts +10 -0
  36. package/src/commands/log.ts +91 -0
  37. package/src/const.ts +19 -0
  38. package/src/tasks/README.md +33 -0
  39. package/src/tasks/find/README.md +49 -0
  40. package/src/tasks/find/find-project.example.ts +23 -0
  41. package/src/tasks/find/find-project.ts +76 -0
  42. package/src/tasks/find/index.ts +1 -0
  43. package/src/tasks/index.ts +2 -0
  44. package/src/tasks/replace/README.md +61 -0
  45. package/src/tasks/replace/index.ts +1 -0
  46. package/src/tasks/replace/replace.example.ts +31 -0
  47. package/src/tasks/replace/replace.ts +95 -0
  48. package/src/ui/README.md +8 -0
  49. package/src/ui/index.ts +2 -0
  50. package/src/ui/open/README.md +58 -0
  51. package/src/ui/open/index.ts +7 -0
  52. package/src/ui/open/open-browser.ts +25 -0
  53. package/src/ui/open/open-editor.example.ts +14 -0
  54. package/src/ui/open/open-editor.ts +30 -0
  55. package/src/ui/prompts/README.md +36 -0
  56. package/src/ui/prompts/autocomplete.prompt.ts +18 -0
  57. package/src/ui/prompts/confirm.prompt.ts +21 -0
  58. package/src/ui/prompts/index.ts +17 -0
  59. package/src/ui/prompts/input.prompt.ts +15 -0
  60. package/src/ui/prompts/search.prompt.ts +33 -0
  61. package/src/ui/prompts/select.prompt.ts +17 -0
  62. package/src/ui/prompts/types.ts +8 -0
  63. package/src/ui/prompts/utils.ts +11 -0
  64. package/src/utils/$/README.md +87 -0
  65. package/src/utils/$/command.test.ts +48 -0
  66. package/src/utils/$/command.ts +21 -0
  67. package/src/utils/$/directory.ts +108 -0
  68. package/src/utils/$/file.ts +98 -0
  69. package/src/utils/$/index.example.ts +37 -0
  70. package/src/utils/$/index.ts +22 -0
  71. package/src/utils/$/path.ts +13 -0
  72. package/src/utils/$/remove.ts +47 -0
  73. package/src/utils/$/spawn.ts +26 -0
  74. package/src/utils/$/utils.ts +21 -0
  75. package/src/utils/README.md +15 -0
  76. package/src/utils/config/README.md +41 -0
  77. package/src/utils/config/config.ts +26 -0
  78. package/src/utils/config/index.ts +1 -0
  79. package/src/utils/config/preferences.conf.ts +45 -0
  80. package/src/utils/env/README.md +52 -0
  81. package/src/utils/env/env.example.ts +21 -0
  82. package/src/utils/env/env.ts +57 -0
  83. package/src/utils/env/index.ts +1 -0
  84. package/src/utils/index.ts +5 -0
  85. package/src/utils/log/README.md +117 -0
  86. package/src/utils/log/index.ts +2 -0
  87. package/src/utils/log/log.example.ts +13 -0
  88. package/src/utils/log/log.ts +58 -0
  89. package/src/utils/log/logger.ts +28 -0
  90. package/src/utils/log/sound.ts +35 -0
  91. package/src/utils/node/README.md +23 -0
  92. package/src/utils/node/index.ts +5 -0
  93. package/src/utils/node/package.ts +12 -0
  94. package/tsconfig.json +15 -0
  95. package/yamllint.config.yml +20 -0
@@ -0,0 +1,23 @@
1
+ import { HOME } from "@/const";
2
+ import { find } from "@/tasks";
3
+
4
+ async function main() {
5
+ const [, , folder] = process.argv;
6
+
7
+ try {
8
+ const [err, data] = await find(folder, HOME, {});
9
+
10
+ if (err) {
11
+ console.error(err);
12
+ process.exit(1);
13
+ }
14
+
15
+ console.log("Folders Found:\n");
16
+ console.table(data);
17
+ } catch (error) {
18
+ console.error("An error occurred:", error);
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ main();
@@ -0,0 +1,76 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { type CLIOptions } from "@/cli";
4
+ import { HOME, SYSTEM_IGNORED_FOLDERS, type IgnoredFolders } from "@/const";
5
+ import { type Return } from "@/types";
6
+ import { $, config, log } from "@/utils";
7
+
8
+ export type Project = Record<string, string>;
9
+ export interface FindProjectsOpts extends CLIOptions {
10
+ maxSearchDepth?: number;
11
+ }
12
+
13
+ const FN = "find.projects";
14
+
15
+ /**
16
+ * Searches directory for any Node.js project (by detecting package.json), ignoring hidden directories.
17
+ *
18
+ * @param {string} [location] - Directory path to search within.
19
+ * @param {FindProjectsOpts} [options] - Additional options for customization.
20
+ * @returns {Promise<Return<Project[]>>} An error or an array of locations with all of the file paths found.
21
+ */
22
+ export async function findProjects(location?: string, options: FindProjectsOpts = {}): Promise<Return<Project[]>> {
23
+ log.data(FN, "arguments", { location, ...options }, options);
24
+ const start = performance.now();
25
+ const maxDepth: number = options?.maxSearchDepth || 5;
26
+ const results: Project[] = [];
27
+ const loc: string = location || HOME;
28
+
29
+ async function searchDirectory(cwd: string, depth: number = 0): Promise<void> {
30
+ log.process(FN, `searching in ${cwd}`, options);
31
+ let files: string[];
32
+ try {
33
+ files = await fs.readdir(cwd);
34
+ } catch {
35
+ return;
36
+ }
37
+
38
+ log.data(FN, "files", files, options);
39
+ for (const file of files) {
40
+ if (
41
+ file.startsWith(".") ||
42
+ SYSTEM_IGNORED_FOLDERS.includes(file as IgnoredFolders) ||
43
+ config.get("preferences.ignoredFolders").includes(file)
44
+ ) {
45
+ continue; // skip all of these files, folders
46
+ }
47
+
48
+ const filePath = path.join(cwd, file);
49
+ log.process(FN, `checking if ${filePath} is a directory`, options);
50
+ const [, dir] = await $.directory(filePath);
51
+ // if there are no files in it, its not a folder i care about
52
+ if (!dir?.files.length || !dir?.path) {
53
+ continue;
54
+ }
55
+
56
+ log.data(FN, "isValidDirectory", dir?.path ?? "", options);
57
+
58
+ if (dir.files.includes(path.join(dir.path, "package.json"))) {
59
+ // Node.js project detected
60
+ const pkg = await fs.readFile(path.join(dir.path, "package.json"), "utf-8");
61
+ const packageData = JSON.parse(pkg);
62
+ results.push({
63
+ name: packageData.name,
64
+ value: dir.path,
65
+ });
66
+ } else if (maxDepth === -1 || depth < maxDepth) {
67
+ await searchDirectory(filePath, depth + 1);
68
+ }
69
+ }
70
+ }
71
+
72
+ await searchDirectory(loc);
73
+
74
+ const end = performance.now();
75
+ return [null, results, end - start];
76
+ }
@@ -0,0 +1 @@
1
+ export * from "./find-project";
@@ -0,0 +1,2 @@
1
+ export * from "./find";
2
+ export * from "./replace";
@@ -0,0 +1,61 @@
1
+ # Find & Replace
2
+
3
+ Find a string in a file and replace it with something else.
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import { findReplace } from "@/tasks";
9
+
10
+ const filePaths = [
11
+ `/packages/developer-experience/sdk-session/src/tests/handlers.ts`,
12
+ ];
13
+ const baseString: string = "const BAM_BROWSER_SDK_VERSION =";
14
+
15
+ async function main() {
16
+ const [, , version] = process.argv;
17
+ const [major, minor] = version.split(".");
18
+
19
+ try {
20
+ const [err, data, duration] = await findReplace(filePaths, baseString, `${major}.${minor}`);
21
+
22
+ if (err) {
23
+ console.error("Error:", err);
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log("Files replaced:\n");
28
+ console.table(data);
29
+ console.log(`It took ${duration} milliseconds`);
30
+ }
31
+ catch (error) {
32
+ console.error("Error:", error);
33
+ }
34
+ }
35
+
36
+ main();
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `findReplace(filePaths: string[], searchTerm: string, replacement: string)`
42
+
43
+ Returns an array of file paths that were modified.
44
+
45
+ #### filePaths
46
+
47
+ The file paths with which to search.
48
+
49
+ Type: `string[]`
50
+
51
+ #### searchTerm
52
+
53
+ The search term to look for and replace.
54
+
55
+ Type: `string`
56
+
57
+ #### replacement
58
+
59
+ A string to replace with
60
+
61
+ Type: `string`
@@ -0,0 +1 @@
1
+ export * from "./replace";
@@ -0,0 +1,31 @@
1
+ import { HOME } from "@/const";
2
+ import { find } from "@/tasks";
3
+
4
+ const filePaths = [
5
+ `${HOME}/Code/disney/solo/apps/commerce/src/common/context/__browser-mocks__/handlers.ts`,
6
+ `${HOME}/Code/disney/solo/packages/developer-experience/sdk-session/src/tests/handlers.ts`,
7
+ `${HOME}/Code/disney/solo/packages/instrumentation/src/glimpse/hooks/use-fire-fed-purchase-event.test.tsx`,
8
+ ];
9
+ const baseString: string = "const BAM_BROWSER_SDK_VERSION =";
10
+
11
+ async function main() {
12
+ const [, , version] = process.argv;
13
+ const [major, minor] = version.split(".");
14
+
15
+ try {
16
+ const [err, data, duration] = await find.replace(filePaths, baseString, `${major}.${minor}`);
17
+
18
+ if (err) {
19
+ console.error("Error:", err);
20
+ process.exit(1);
21
+ }
22
+
23
+ console.log("Files replaced:\n");
24
+ console.table(data);
25
+ console.log(`It took ${duration} milliseconds`);
26
+ } catch (error) {
27
+ console.error("Error:", error);
28
+ }
29
+ }
30
+
31
+ main();
@@ -0,0 +1,95 @@
1
+ import { type CLIOptions } from "@/cli";
2
+ import { type Return } from "@/types";
3
+ import { $, log } from "@/utils";
4
+
5
+ const FN = "find.replace";
6
+
7
+ /**
8
+ * Perform a case-sensitive replacement in a string, keeping the casing format of each found instance.
9
+ *
10
+ * @param {string} text - The text in which to perform the replacement.
11
+ * @param {string} searchTerm - The term to search for, ignoring case.
12
+ * @param {string} replacement - The replacement term.
13
+ * @returns {string} - The updated text with replacements made.
14
+ */
15
+ function replaceWithRespectToCase(text: string, searchTerm: string, replacement: string): string {
16
+ // Create a regex to match search term with flexible spacing/hyphens between words
17
+ const words = searchTerm.split(/[- ]/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); // Escape special chars
18
+ const regex = new RegExp(words.join("[- ]?"), "gi"); // Allow for spaces or hyphens between words
19
+
20
+ return text.replace(regex, (match) => {
21
+ // Transform replacement to match the case of the original found match
22
+ const replacementWords = replacement.split(/[- ]/);
23
+ const transformed = match
24
+ .split(/[- ]/)
25
+ .map((word, index) => {
26
+ const replacementWord = replacementWords[index] || ""; // Handle possible extra words in replacement
27
+ return /[A-Z]/.test(word[0])
28
+ ? replacementWord.charAt(0).toUpperCase() + replacementWord.slice(1)
29
+ : replacementWord.toLowerCase();
30
+ })
31
+ .join(" ");
32
+
33
+ return transformed.trim();
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Update a specific string in multiple files.
39
+ *
40
+ * @param {string[]} filePaths - An array of file paths to be updated.
41
+ * @param {string} searchTerm - The base string to match.
42
+ * @param {string} replacement - The new string to replace with.
43
+ * @param {CLIOptions} [options={}] - CLI options for logging and additional processing.
44
+ * @returns {Promise<Return<string[]>>} - A promise that resolves to a tuple containing an error (if any), an object with the update status for each domain, and the time taken.
45
+ */
46
+ export async function findReplace(
47
+ filePaths: string[],
48
+ searchTerm: string,
49
+ replacement: string,
50
+ options: CLIOptions = {}
51
+ ): Promise<Return<string[]>> {
52
+ if (!filePaths || !Array.isArray(filePaths)) {
53
+ return [new Error("No file paths were provided!"), null, 0];
54
+ }
55
+
56
+ if (!searchTerm) {
57
+ return [new Error("No search term was provided!"), null, 0];
58
+ }
59
+
60
+ if (!replacement) {
61
+ return [new Error("No replacement was provided!"), null, 0];
62
+ }
63
+
64
+ const start = performance.now();
65
+ const output: string[] = [];
66
+
67
+ for (const filePath of filePaths) {
68
+ log.process(FN, `Searching ${filePath} for occurrences of "${searchTerm}"`, options);
69
+
70
+ try {
71
+ // Read the file content
72
+ const [fileErr, file] = await $.file(filePath);
73
+ if (fileErr || !file?.content) {
74
+ log.info(FN, `Cannot read file ${filePath}: skipping`, options);
75
+ continue;
76
+ }
77
+
78
+ // Perform the replacement
79
+ const updatedData = replaceWithRespectToCase(file.content, searchTerm, replacement);
80
+
81
+ // Write back only if the content changed
82
+ if (updatedData !== file.content) {
83
+ await $.file(filePath, updatedData, true); // Ensure this function overwrites the file content
84
+ output.push(filePath);
85
+ log.process(FN, `Updated ${filePath}`, options);
86
+ } else {
87
+ log.process(FN, `No changes in ${filePath}`, options);
88
+ }
89
+ } catch (error) {
90
+ log.error(FN, `Error processing file ${filePath}: ${(error as Error).message}`, options);
91
+ }
92
+ }
93
+
94
+ return [null, output, performance.now() - start];
95
+ }
@@ -0,0 +1,8 @@
1
+ # UI
2
+
3
+ A set of functions designed to be used as a UI for interacting with the client.
4
+
5
+ ## What's Available
6
+
7
+ * [open](./open/README.md) - open an app in your `$EDITOR`
8
+ * [prompts](./prompts/README.md) - prompt the user with various UI tools
@@ -0,0 +1,2 @@
1
+ export { open } from "./open";
2
+ export { prompt, type Choice } from "./prompts";
@@ -0,0 +1,58 @@
1
+ # Open an Application
2
+
3
+ Open an application after a confirmation.
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import { open } from "@/ui";
9
+
10
+ async function main () {
11
+ const [, , name, location] = process.argv;
12
+
13
+ try {
14
+ const [err, isOpened] = await open.editor(name, location);
15
+
16
+ if (err) {
17
+ console.error(err);
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(`${name} ${isOpened ? "was" : "was not"} opened.`);
22
+ }
23
+ catch (error) {
24
+ console.error('An error occurred:', error);
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ main();
30
+ ```
31
+
32
+ ## API
33
+
34
+ ### `editor(name: string, location: string)`
35
+
36
+ Returns a Promise that resolves to a boolean.
37
+
38
+ #### name
39
+
40
+ A name of the application.
41
+
42
+ Type: `string`
43
+
44
+ #### location
45
+
46
+ A path of where the application is located.
47
+
48
+ Type: `string`
49
+
50
+ ### `browser(url: string)`
51
+
52
+ Opens the URL in the users default browser.
53
+
54
+ #### url
55
+
56
+ The URL to open.
57
+
58
+ Type: `string`
@@ -0,0 +1,7 @@
1
+ import { openBrowser } from "./open-browser";
2
+ import { openApp } from "./open-editor";
3
+
4
+ export const open = {
5
+ browser: openBrowser,
6
+ editor: openApp,
7
+ };
@@ -0,0 +1,25 @@
1
+ import defaultBrowser from "default-browser";
2
+ import open, { apps } from "open";
3
+
4
+ /**
5
+ * Opens an application if the user confirms.
6
+ *
7
+ * @param {string} name - The name of the application.
8
+ * @returns {Promise<void>} - A promise that resolves to a tuple containing an error (if any), a boolean indicating if the app was opened, and the time taken to perform the operation.
9
+ */
10
+ export async function openBrowser(url: string): Promise<void> {
11
+ if (!url) {
12
+ throw new Error("No URL was provided!");
13
+ }
14
+
15
+ let browser = apps.browser;
16
+ const { name } = await defaultBrowser();
17
+ const supported = ["chrome", "firefox", "edge"];
18
+ if (!supported.includes(name.toLowerCase())) {
19
+ browser = apps.chrome;
20
+ }
21
+
22
+ await open(url, {
23
+ app: { name: browser },
24
+ });
25
+ }
@@ -0,0 +1,14 @@
1
+ import { open } from "@/ui";
2
+
3
+ async function main() {
4
+ const [, , name, location] = process.argv;
5
+
6
+ try {
7
+ await open.editor(name, location);
8
+ } catch (error) {
9
+ console.error("An error occurred:", error);
10
+ process.exit(1);
11
+ }
12
+ }
13
+
14
+ main();
@@ -0,0 +1,30 @@
1
+ import openEditor from "open-editor";
2
+ import { prompt } from "@/ui";
3
+
4
+ /**
5
+ * Opens an application if the user confirms.
6
+ *
7
+ * @param {string} name - The name of the application.
8
+ * @param {string} location - The file location of the application.
9
+ * @returns {Promise<void>}
10
+ */
11
+ export async function openApp(location: string, name?: string, bypass: boolean = false): Promise<void> {
12
+ if (!location) {
13
+ throw new Error("No app location was provided!");
14
+ }
15
+ const open = async () =>
16
+ await openEditor(
17
+ [
18
+ {
19
+ file: location,
20
+ },
21
+ ],
22
+ { wait: true }
23
+ );
24
+
25
+ if (!bypass) {
26
+ await prompt.confirm(`Do you want to open ${name}?`, open);
27
+ } else {
28
+ open();
29
+ }
30
+ }
@@ -0,0 +1,36 @@
1
+ # Prompts
2
+
3
+ Inquirer prompts
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import { prompt } from "@/ui";
9
+
10
+ async function main() {
11
+ const [, , project] = process.argv;
12
+
13
+ try {
14
+ const filepath = await prompt.search(project, undefined, undefined, {});
15
+ console.log(filepath);
16
+ }
17
+ catch (error) {
18
+ console.error("Error:", error);
19
+ }
20
+ }
21
+
22
+ main();
23
+ ```
24
+
25
+ ## API
26
+
27
+ ### `project(message?: string)`
28
+
29
+ Returns a string of the directory selected.
30
+
31
+ #### message
32
+
33
+ The message to display to the user.
34
+
35
+ Type: `string`
36
+ Default: "Enter a project:"
@@ -0,0 +1,18 @@
1
+ import autocomplete from "inquirer-autocomplete-standalone";
2
+ import { type Choice } from "./types";
3
+
4
+ export async function autocompletePrompt(source: Choice<string>[], msg?: string): Promise<string> {
5
+ return await autocomplete({
6
+ emptyText: "No results found. Please enter a term",
7
+ message: msg || "Search a term",
8
+ source: async (input?: string) =>
9
+ source.filter(({ name }) => {
10
+ const proj = name?.toLowerCase() ?? "";
11
+ if (proj.length > 0) {
12
+ return proj.includes(input?.toLowerCase() ?? "");
13
+ }
14
+
15
+ return "";
16
+ }),
17
+ });
18
+ }
@@ -0,0 +1,21 @@
1
+ import { confirm } from "@inquirer/prompts";
2
+
3
+ type Callback = () => void | Promise<void>;
4
+
5
+ export async function confirmPrompt(
6
+ message: string,
7
+ callback: Callback,
8
+ error: Callback = () => {
9
+ /* noOp */
10
+ }
11
+ ): Promise<void> {
12
+ const should = await confirm({
13
+ message,
14
+ });
15
+
16
+ if (should) {
17
+ await callback();
18
+ } else {
19
+ await error();
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ import { autocompletePrompt } from "./autocomplete.prompt";
2
+ import { confirmPrompt } from "./confirm.prompt";
3
+ import { inputPrompt } from "./input.prompt";
4
+ import { searchPrompt } from "./search.prompt";
5
+ import { selectPrompt } from "./select.prompt";
6
+ import { validate } from "./utils";
7
+
8
+ export * from "./types";
9
+
10
+ export const prompt = {
11
+ autocomplete: autocompletePrompt,
12
+ confirm: confirmPrompt,
13
+ input: inputPrompt,
14
+ search: searchPrompt,
15
+ select: selectPrompt,
16
+ validate,
17
+ };
@@ -0,0 +1,15 @@
1
+ import { input } from "@inquirer/prompts";
2
+ import { validate } from "./utils";
3
+
4
+ export async function inputPrompt(message: string, isRequired?: boolean = false, options): Promise<string> {
5
+ const opts = {
6
+ message,
7
+ ...options,
8
+ };
9
+
10
+ if (isRequired) {
11
+ opts.validate = validate.isNotEmpty;
12
+ }
13
+
14
+ return await input(opts);
15
+ }
@@ -0,0 +1,33 @@
1
+ import autocomplete from "inquirer-autocomplete-standalone";
2
+ import { type CLIOptions } from "@/cli";
3
+ import { findProjects } from "@/tasks";
4
+ import { $, getDirectoryByLevel } from "@/utils";
5
+
6
+ export async function searchPrompt(location?: string, msg?: string, options: CLIOptions): Promise<string> {
7
+ const message = msg || "Choose a project:";
8
+
9
+ const dirname: string = await autocomplete({
10
+ emptyText: "No results found. Please enter the location",
11
+ message,
12
+ pageSize: 10,
13
+ source: async (input?: string) => {
14
+ const term = input?.toLowerCase() ?? "";
15
+ const [, results] = await findProjects(location, options);
16
+ const filtered = results
17
+ .map(({ name, value }) => ({ name: `${name} (${getDirectoryByLevel(value)})`, value }))
18
+ .filter(({ name }) => {
19
+ const proj = name?.toLowerCase() ?? "";
20
+ if (proj.length > 0) {
21
+ return proj.includes(term);
22
+ }
23
+
24
+ return "";
25
+ });
26
+
27
+ return filtered;
28
+ },
29
+ });
30
+
31
+ const [, dir] = await $.directory(dirname);
32
+ return dir?.path ?? "";
33
+ }
@@ -0,0 +1,17 @@
1
+ import { checkbox } from "@inquirer/prompts";
2
+ import { type Choice } from "./types";
3
+
4
+ export async function selectPrompt(
5
+ selected: string[] | null,
6
+ message: string,
7
+ choices: Choice<string>[]
8
+ ): Promise<string[]> {
9
+ if (selected) {
10
+ return selected;
11
+ }
12
+
13
+ return await checkbox({
14
+ message,
15
+ choices,
16
+ });
17
+ }
@@ -0,0 +1,8 @@
1
+ export type Choice<Value> = {
2
+ description?: string;
3
+ disabled?: boolean | string;
4
+ checked?: boolean;
5
+ name?: string;
6
+ type?: never;
7
+ value: Value;
8
+ };
@@ -0,0 +1,11 @@
1
+ function isNotEmpty(val: string): boolean | string {
2
+ if (!val || val.trim().length === 0) {
3
+ return "Cannot be empty";
4
+ }
5
+
6
+ return true;
7
+ }
8
+
9
+ export const validate = {
10
+ isNotEmpty,
11
+ };