@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
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # Holocron
2
+
3
+ The command-line client designed to support [The Holocron](http://docs.theholocron.dev).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --global @theholocron/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ <!-- fill out the usage with examples -->
14
+
15
+ ## Documentation
16
+
17
+ Check out [The Holocron Archive](https://docs.theholocron.dev/projects/holocron/) for more information.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @see https://commitlint.js.org/reference/configuration.html
3
+ * @type {import("@commitlint/types").UserConfig}
4
+ */
5
+ const config = {
6
+ extends: ["@theholocron"],
7
+ };
8
+
9
+ export default config;
@@ -0,0 +1,9 @@
1
+ import theHolocron from "@theholocron/eslint-config";
2
+
3
+ /**
4
+ * @see https://eslint.org/docs/latest/use/configure/
5
+ * @type {import("eslint").Linter.Config}
6
+ */
7
+ const config = [...theHolocron];
8
+
9
+ export default config;
@@ -0,0 +1,11 @@
1
+ # Media
2
+
3
+ The following is a storage place for all of the media used within `holocron`.
4
+
5
+ ## What's Available
6
+
7
+ ### MP3s
8
+
9
+ - error.mp3 - used for when a command errs, and the `--sound` flag is enabled
10
+ - warning.mp3 - used for when a command has a warning, and the `--sound` flag is enabled
11
+ - success.mp3 - used for when a command is successful, and the `--sound` flag is enabled
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@theholocron/cli",
3
+ "version": "1.0.0",
4
+ "description": "The command-line client designed to support The Holocron.",
5
+ "homepage": "https://github.com/theholocron/holocron#readme",
6
+ "bugs": "https://github.com/theholocron/holocron/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/theholocron/holocron.git"
10
+ },
11
+ "license": "GPL-3.0",
12
+ "author": "Newton Koumantzelis",
13
+ "type": "module",
14
+ "main": "src/cli.ts",
15
+ "bin": {
16
+ "holocron": "./src/cli.ts"
17
+ },
18
+ "man": "./man/holocron.1",
19
+ "scripts": {
20
+ "lint": "docker run -e LOG_LEVEL=DEBUG -e RUN_LOCAL=true -v .:/tmp/lint --rm ghcr.io/super-linter/super-linter:latest",
21
+ "postbuild": "marked-man README.md > man/holocron.1",
22
+ "start": "tsx ./src/cli.ts"
23
+ },
24
+ "dependencies": {
25
+ "@inquirer/prompts": "^7.0.1",
26
+ "@theholocron/utils-string": "^0.1.0",
27
+ "chalk": "^5.3.0",
28
+ "conf": "^13.0.1",
29
+ "default-browser": "^5.2.1",
30
+ "dotenv": "^16.4.5",
31
+ "inquirer-autocomplete-standalone": "^0.8.1",
32
+ "open": "^10.1.0",
33
+ "open-editor": "^4.1.1",
34
+ "ora": "^8.1.0",
35
+ "play-sound": "^1.1.6",
36
+ "update-notifier": "^7.3.1",
37
+ "winston": "^3.15.0",
38
+ "yargs": "^17.7.2"
39
+ },
40
+ "devDependencies": {
41
+ "@theholocron/commitlint-config": "^3.4.4",
42
+ "@theholocron/eslint-config": "^3.4.4",
43
+ "@theholocron/jest-config": "^3.4.4",
44
+ "@theholocron/lint-staged-config": "^3.4.4",
45
+ "@theholocron/prettier-config": "^3.4.4",
46
+ "@theholocron/storybook-config": "^3.4.4",
47
+ "@theholocron/stylelint-config": "^3.4.4",
48
+ "@theholocron/tsconfig": "^3.4.4",
49
+ "@tsconfig/node-lts": "^20.1.3",
50
+ "@types/conf": "^2.1.0",
51
+ "@types/inquirer": "^9.0.7",
52
+ "@types/play-sound": "^1.1.2",
53
+ "@types/update-notifier": "^6.0.8",
54
+ "@types/winston": "^2.4.4",
55
+ "@types/yargs": "^17.0.33",
56
+ "alexjs": "^1.0.0",
57
+ "marked-man": "^2.1.0",
58
+ "tsx": "^4.19.2",
59
+ "typescript": "^5.6.3"
60
+ },
61
+ "packageManager": "npm@9.5.0",
62
+ "engines": {
63
+ "node": ">=23",
64
+ "npm": ">=10"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "releases": "https://github.com/theholocron/holocron/releases",
70
+ "wiki": "https://github.com/theholocron/holocron/wiki"
71
+ }
@@ -0,0 +1,11 @@
1
+ import theholocron from "@theholocron/prettier-config";
2
+
3
+ /**
4
+ * @see https://prettier.io/docs/en/configuration
5
+ * @type {import("prettier").Config}
6
+ */
7
+ const config = {
8
+ ...theholocron,
9
+ };
10
+
11
+ export default config;
package/src/cli.ts ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env npx tsx
2
+
3
+ import * as str from "@theholocron/utils-string";
4
+ import updateNotifier from "update-notifier";
5
+ import { hideBin } from "yargs/helpers";
6
+ import yargs from "yargs/yargs";
7
+ import { __cmddir } from "@/const";
8
+ import pkg from "@/package";
9
+ import * as utils from "@/utils";
10
+
11
+ const [, env] = utils.env.read();
12
+
13
+ export interface CLIOptions {
14
+ d?: boolean;
15
+ debug?: boolean;
16
+ s?: boolean;
17
+ sound?: boolean;
18
+ verbose?: boolean;
19
+ // spinner: Ora;
20
+ }
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
23
+ yargs(hideBin(process.argv))
24
+ .usage("Usage: $0 <command> [options]")
25
+ .commandDir(__cmddir("./commands"), {
26
+ extensions: ["ts"],
27
+ })
28
+ .demandCommand()
29
+ .env("HOLOCRON")
30
+ .completion()
31
+ .recommendCommands()
32
+ .options({
33
+ d: {
34
+ alias: ["debug"],
35
+ default: utils.config.get("preferences.debug") || str.toBoolean(env?.HOLOCRON_DEBUG) || false,
36
+ describe: "Turn on debugging mode",
37
+ type: "boolean",
38
+ global: true,
39
+ hidden: true,
40
+ },
41
+ s: {
42
+ alias: ["sound"],
43
+ default: utils.config.get("preferences.sound") || str.toBoolean(env?.HOLOCRON_SOUND) || false,
44
+ describe: "Turn on sound effects",
45
+ type: "boolean",
46
+ global: true,
47
+ hidden: true,
48
+ },
49
+ verbose: {
50
+ default: str.toBoolean(env?.HOLOCRON_VERBOSE) || false,
51
+ describe: "Turn on logging",
52
+ type: "boolean",
53
+ global: true,
54
+ },
55
+ })
56
+ .alias({
57
+ h: "help",
58
+ v: "version",
59
+ })
60
+ .strict()
61
+ .help("h")
62
+ .version()
63
+ .epilogue(`© 2024-${new Date().getFullYear()} The Holocron, Inc. All rights reserved.`).argv as CLIOptions;
64
+
65
+ updateNotifier({ pkg }).notify();
@@ -0,0 +1,13 @@
1
+ # Commands
2
+
3
+ A direct interaction with the command-line via `yargs`.
4
+
5
+ ## What's Available
6
+
7
+ - bootstrap - bootstrap a repository by swapping out template variables
8
+ - conf - interact with the configuration file
9
+ - log - print out the log file
10
+
11
+ ## Rules
12
+
13
+ - There should be error logging and success logging; process/warning if it make sense; data for sure
@@ -0,0 +1,102 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as str from "@theholocron/utils-string";
4
+ import { type CommandBuilder } from "yargs";
5
+ import { type CLIOptions } from "@/cli";
6
+ import { HOME } from "@/const";
7
+ import { findReplace } from "@/tasks";
8
+ import { prompt } from "@/ui";
9
+ import { getDirectoryByLevel, log, node } from "@/utils";
10
+
11
+ function getRepoName(pathname) {
12
+ const gitconfig = path.join(pathname, ".git", "config");
13
+
14
+ try {
15
+ const content = fs.readFileSync(gitconfig, "utf-8");
16
+ const match = content.match(/url = .*\/(.*)\.git/); // Regex to capture repo name
17
+ return match ? match[1] : null;
18
+ } catch (error) {
19
+ console.error("Error reading .git/config:", error);
20
+ return null;
21
+ }
22
+ }
23
+
24
+ interface BootstrapOpts extends CLIOptions {
25
+ description?: string;
26
+ name?: string;
27
+ }
28
+
29
+ interface BootstrapPositional extends CLIOptions {
30
+ filepath?: string;
31
+ }
32
+
33
+ export const builder: CommandBuilder<BootstrapOpts, BootstrapPositional> = (yargs) =>
34
+ yargs
35
+ .options({
36
+ description: {
37
+ describe: "The description for the project",
38
+ nargs: 1,
39
+ requiresArg: true,
40
+ type: "string",
41
+ },
42
+ name: {
43
+ describe: "The name of the project",
44
+ nargs: 1,
45
+ requiresArg: true,
46
+ type: "string",
47
+ },
48
+ })
49
+ .positional("filepath", {
50
+ describe: "The path to where the project is stored",
51
+ type: "string",
52
+ });
53
+ export const command: string = "bootstrap [filepath] [name] [description]";
54
+ export const desc: string = "Bootstrap a template";
55
+ export async function handler(options: CLIOptions): Promise<void> {
56
+ const FN = "bootstrap";
57
+ log.data(FN, "arguments", options, options);
58
+
59
+ // search the home directory for projects
60
+ // @TODO set this to check a configurable directory and default to HOME
61
+ const projectPath = await prompt.search(options?.filepath, HOME, options);
62
+ // from the project i need to determine if its has a package.json
63
+ // and what type of template it is based on the name on the package.json
64
+ // e.g. { name: "@theholocron/cli-template" }
65
+ const [, pkg] = await node.pkg(projectPath);
66
+ // template type, if any
67
+ const type = getDirectoryByLevel(pkg.name, -1) ?? "react-template";
68
+
69
+ const name =
70
+ options?.name ??
71
+ (await prompt.input("The name. What are we calling this thing?", true, {
72
+ default: getRepoName(projectPath),
73
+ transformer: str.toKebabCase,
74
+ }));
75
+
76
+ const description =
77
+ options?.description ??
78
+ (await prompt.input("A brief description. What does it do?", true, {
79
+ transformer: str.toSentenceCase,
80
+ }));
81
+
82
+ const readme = `${projectPath}/README.md`;
83
+ const pkgJson = `${projectPath}/package.json`;
84
+
85
+ await findReplace([readme, `${projectPath}/tsconfig.json`], type, str.toTitleCase(name));
86
+ await findReplace(
87
+ [`${projectPath}/src/utils/config/config.ts`, pkgJson, `${projectPath}/vite.config.ts`],
88
+ type,
89
+ str.toKebabCase(name)
90
+ );
91
+ await findReplace([readme, pkgJson], "<description>", str.toSentenceCase(description));
92
+
93
+ if (type === "cli-template") {
94
+ await findReplace(
95
+ [`${projectPath}/src/cli.ts`, `${projectPath}/.env`],
96
+ "CLI_TEMPLATE",
97
+ str.toConstantCase(name)
98
+ );
99
+ }
100
+
101
+ process.exit(0);
102
+ }
@@ -0,0 +1,45 @@
1
+ # Configuration File
2
+
3
+ An action to interact with the configuration file.
4
+
5
+ ## How Do I Use This?
6
+
7
+ Run the `--help` or `-h` command to find out how to use the command.
8
+
9
+ ```sh
10
+ Usage: tsx ./src/cli.ts conf <command> [options]
11
+
12
+ Commands:
13
+ tsx ./src/cli.ts conf add <key> <value> [options]
14
+ tsx ./src/cli.ts conf edit [options]
15
+ tsx ./src/cli.ts conf view [key] [options]
16
+
17
+ Examples:
18
+ tsx ./src/cli.ts conf add
19
+ tsx ./src/cli.ts conf edit
20
+ tsx ./src/cli.ts conf view
21
+ tsx ./src/cli.ts conf view domains
22
+ ```
23
+
24
+ All [global options](/README.md#options) are supported as well.
25
+
26
+ ## API
27
+
28
+ ### Return
29
+
30
+ If command is valid, it will exit with a `0` code and the file path. Otherwise, it will exit with `1` code and an error message.
31
+
32
+ ### `add`
33
+
34
+ Adds a key-value pair to the configuration and returns the updated configuration.
35
+
36
+ ### `edit`
37
+
38
+ Opens the configuration file in the default editor for editing.
39
+
40
+ ### `view`
41
+
42
+ Retrieves and prints the configuration settings based on provided keys or all configurations if no keys are specified.
43
+
44
+ * `options:CLIOptions` - The options containing configuration details and the keys to be retrieved.
45
+ * @returns {Record<string, any> | Record<string, any>[]} - The requested configuration settings.
@@ -0,0 +1,49 @@
1
+ import type { CommandBuilder } from "yargs";
2
+ import { CLIOptions } from "@/cli";
3
+ import { config, log } from "@/utils";
4
+
5
+ interface AddConfOpts extends CLIOptions {
6
+ name: string;
7
+ value: any;
8
+ }
9
+
10
+ export const builder: CommandBuilder<AddConfOpts, AddConfOpts> = (yargs) =>
11
+ yargs
12
+ .positional("name", {
13
+ demandOption: true,
14
+ describe: "A key to store as within the configuration file",
15
+ type: "string",
16
+ })
17
+ .positional("value", {
18
+ demandOption: true,
19
+ describe: "The value to store within the configuration file",
20
+ type: "string",
21
+ });
22
+ export const command: string = "add <name> <value>";
23
+ export const desc: string = "Add to the configuration";
24
+ export function handler(options: AddConfOpts): void {
25
+ const FN = "conf add";
26
+ log.data(FN, "arguments", options, options);
27
+
28
+ const { name, value } = options;
29
+ const conf = config.get(name);
30
+
31
+ if (Array.isArray(conf)) {
32
+ config.set(name, Array.from(new Set([...conf, value])));
33
+ const obj = config.get(name);
34
+ log.success(FN, obj);
35
+ return obj;
36
+ }
37
+
38
+ if (Object.prototype.toString.call(conf) === "[object Object]") {
39
+ config.set(name, { ...conf, ...value });
40
+ const obj = config.get(name);
41
+ log.success(FN, obj);
42
+ return obj;
43
+ }
44
+
45
+ config.set(name, value);
46
+ const obj = config.get(name);
47
+ log.success(FN, obj);
48
+ return obj;
49
+ }
@@ -0,0 +1,12 @@
1
+ import { type CLIOptions } from "@/cli";
2
+ import { open } from "@/ui";
3
+ import { config, log } from "@/utils";
4
+
5
+ export const command: string = "edit";
6
+ export const desc: string = "Edit the configuration";
7
+ export async function handler(options: CLIOptions): Promise<void> {
8
+ const FN = "conf edit";
9
+ log.data(FN, "arguments", options, options);
10
+
11
+ await open.editor(config.path, undefined, true);
12
+ }
@@ -0,0 +1,40 @@
1
+ import type { CommandBuilder } from "yargs";
2
+ import { CLIOptions } from "@/cli";
3
+ import { config, log, str } from "@/utils";
4
+
5
+ interface ViewConfOpts extends CLIOptions {
6
+ name?: string[];
7
+ }
8
+
9
+ export const builder: CommandBuilder<ViewConfOpts, ViewConfOpts> = (yargs) =>
10
+ yargs.positional("name", {
11
+ coerce: str.toArray,
12
+ describe: "Any key within the configuration file",
13
+ type: "string",
14
+ });
15
+ export const command: string = "view [name..]";
16
+ export const desc: string = "View the configuration";
17
+ export function handler(options: ViewConfOpts): Record<string, any> | Record<string, any>[] {
18
+ const FN = "conf.view";
19
+ log.data(FN, "arguments", options, options);
20
+
21
+ const { name } = options;
22
+
23
+ if (name && name.length > 0) {
24
+ const items = name.reduce(
25
+ (acc, item) => {
26
+ acc[item] = config.get(item);
27
+ return acc;
28
+ },
29
+ {} as Record<string, any>
30
+ );
31
+
32
+ console.log(items);
33
+ return items;
34
+ }
35
+
36
+ const allConfig = config.get();
37
+ const obj = { ...allConfig };
38
+ console.log(obj);
39
+ return obj;
40
+ }
@@ -0,0 +1,10 @@
1
+ import { type CommandBuilder } from "yargs";
2
+
3
+ interface ConfArgs {
4
+ command: string;
5
+ }
6
+
7
+ export const builder: CommandBuilder<ConfArgs, ConfArgs> = (yargs) =>
8
+ yargs.commandDir("conf", { extensions: ["js", "ts"] });
9
+ export const command: string = "conf <command>";
10
+ export const desc: string = "Interact with the configuration";
@@ -0,0 +1,91 @@
1
+ import fs from "fs";
2
+ import readline from "readline";
3
+ import chalk from "chalk";
4
+ import type { CommandBuilder } from "yargs";
5
+ import { type CLIOptions } from "@/cli";
6
+ import { config, type LogLevel } from "@/utils";
7
+
8
+ export interface LogOpts extends CLIOptions {
9
+ level?: LogLevel;
10
+ maxLines?: number;
11
+ }
12
+
13
+ export const levels: Record<LogLevel, string> = {
14
+ all: "log",
15
+ error: "error.log",
16
+ warn: "warn.log",
17
+ info: "info.log",
18
+ verbose: "verbose.log",
19
+ debug: "debug.log",
20
+ };
21
+
22
+ export const builder: CommandBuilder<LogOpts, LogOpts> = (yargs) =>
23
+ yargs.options({
24
+ l: {
25
+ alias: ["level", "log-level"],
26
+ choices: Object.keys(levels) as LogLevel[],
27
+ default: "all",
28
+ demandOption: true,
29
+ describe: "The log level to show",
30
+ },
31
+ o: {
32
+ alias: ["output", "max-lines"],
33
+ default: 20,
34
+ describe: "Maximum number of lines to show, from the end of the file",
35
+ type: "number",
36
+ },
37
+ });
38
+ export const command: string = "log";
39
+ export const desc: string = "Print out the logs";
40
+
41
+ /**
42
+ * Reads and displays the last `maxLines` lines from a log file with enhanced formatting.
43
+ *
44
+ * @param {string} [level='all'] - The log level to read (e.g., 'all', 'error'). Defaults to 'all'.
45
+ * @param {number} [maxLines] - The number of lines to display from the end of the log file. If not provided, displays the entire file.
46
+ */
47
+ export async function handler(options: LogOpts): Promise<void> {
48
+ const { level, maxLines } = options;
49
+ const logFilePath = `${config.get("preferences.logs")}/${levels[level || "all"] || levels.all}`;
50
+ const fileStream = fs.createReadStream(logFilePath);
51
+ const rl = readline.createInterface({
52
+ input: fileStream,
53
+ crlfDelay: Infinity,
54
+ });
55
+
56
+ const lines: { number: number; content: string }[] = [];
57
+ let lineNumber = 0;
58
+
59
+ rl.on("line", (line: string) => {
60
+ lineNumber++;
61
+ lines.push({ number: lineNumber, content: line });
62
+ if (maxLines !== undefined && lines.length > maxLines) {
63
+ lines.shift();
64
+ }
65
+ });
66
+
67
+ await new Promise<void>((resolve) => rl.on("close", resolve));
68
+
69
+ // Define chalk styles
70
+ const timestampStyle = chalk.yellow;
71
+ const levelStyle = chalk.bold.red;
72
+ const messageStyle = chalk.white;
73
+ const bracketStyle = chalk.magenta;
74
+
75
+ lines.forEach(({ number, content }) => {
76
+ const match = content.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (\w+): (.+)/);
77
+ if (match) {
78
+ let [, timestamp, level, message] = match;
79
+
80
+ // Apply magenta style to bracketed content
81
+ message = message.replace(/\[([^\]]+)\]/g, (match, p1) => `${bracketStyle(`[${p1}]`)}`);
82
+ console.log(
83
+ `${chalk.gray(number)}: ${timestampStyle(`[${timestamp}]`)} ${levelStyle(level)}: ${messageStyle(message)}`
84
+ );
85
+ } else {
86
+ // Apply magenta style to bracketed content in lines that don't match the main pattern
87
+ content = content.replace(/\[([^\]]+)\]/g, (match, p1) => `${bracketStyle(`[${p1}]`)}`);
88
+ console.log(`${chalk.gray(number)}: ${content}`); // If the line doesn't match the expected format, print it as is with line number
89
+ }
90
+ });
91
+ }
package/src/const.ts ADDED
@@ -0,0 +1,19 @@
1
+ import os from "node:os";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const __filename = fileURLToPath(import.meta.url);
6
+ export const __dirname = path.dirname(__filename);
7
+ export const __cmddir = (dir: string) => path.resolve(__dirname, dir);
8
+ export const HOME = os.homedir();
9
+ export const OS = process.platform;
10
+ export const SYSTEM_IGNORED_FOLDERS = [
11
+ "Applications",
12
+ "Library",
13
+ "Movies",
14
+ "Music",
15
+ "Pictures",
16
+ "Public",
17
+ "Users",
18
+ ] as const;
19
+ export type IgnoredFolders = (typeof SYSTEM_IGNORED_FOLDERS)[number];
@@ -0,0 +1,33 @@
1
+ # Tasks
2
+
3
+ A set of functions designed to be strung together to create actions. An API of sorts, to power the command-line client.
4
+
5
+ ## What's Available
6
+
7
+ - [find](./find/README.md) - find things
8
+ - [replace](./replace/README.md) - replace things
9
+
10
+ ## Rules
11
+
12
+ A set of rules for how I decide when to create a task, as opposed to creating an action.
13
+
14
+ ### A single responsibility
15
+
16
+ It should only have a single responsibility. It shouldn't interact with the user in any sort of decision tree (no interaction with `inquirer`). Testing this through the provided examples should allow for a few inputs with some expected outputs. For example:
17
+
18
+ - I clone a project; I get a clone somewhere
19
+ - I search a folder; I get the location of that folder
20
+ - I search a project; I get the location of that project
21
+ - I create a directory; I get a directory somewhere
22
+
23
+ In the case of actions, these are where I may ask it do several things while having it interact with the user. This also means that the return type is typically consistent; meaning it will be an array of consistent types, etc. If you have forks in your code that change the type from say a string to a number you might need to split them up into separate tasks.
24
+
25
+ ### Return type
26
+
27
+ It should return in the form of <code>[null | Error, null | data, null | duration]</code>. Tasks returns data; actions return process error codes or voids or logs.
28
+
29
+ ### Other rules
30
+
31
+ - Tasks are designed to be used to compose an action; this means the output of one task will be input for another
32
+ - Tasks are atomic; as close to bare metal Node processes as possible
33
+ - Most of the arguments in the function should be required unless where sensible defaults make sense
@@ -0,0 +1,49 @@
1
+ # Find a Folder
2
+
3
+ Find a folder recursively.
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import { findFolder } from "@/tasks";
9
+
10
+ async function main() {
11
+ const [, , folder] = process.argv;
12
+
13
+ try {
14
+ const [err, data] = await findFolder(folder);
15
+
16
+ if (err) {
17
+ console.error(err);
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log("Folders Found:\n");
22
+ console.table(data);
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
+ ### `findFolder(folder: string, location?: string)`
35
+
36
+ Returns an array of file paths that were found.
37
+
38
+ #### folder
39
+
40
+ The folder with which to search.
41
+
42
+ Type: `string`
43
+
44
+ #### location
45
+
46
+ The directory path to where to search for the folder.
47
+
48
+ Type: `string`
49
+ Default: `os.homedir()`