@mbsi/mkcmd 0.2.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.
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@mbsi/mkcmd",
3
+ "version": "0.2.0",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mkcmd": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "bun build --target=bun --outfile=dist/index.js src/index.ts",
12
+ "build:exe": "bun build --compile --outfile=dist/mkcmd src/index.ts",
13
+ "prepack": "bun run build"
14
+ },
15
+ "devDependencies": {
16
+ "@types/bun": "latest",
17
+ "typescript": "^5"
18
+ },
19
+ "dependencies": {
20
+ "@clack/prompts": "^0.11.0",
21
+ "@types/figlet": "^1.7.0",
22
+ "figlet": "^1.9.4"
23
+ }
24
+ }
@@ -0,0 +1,13 @@
1
+ import { registerCommand } from "../core/cli";
2
+ import { orchestrateScaffold } from "../functions/orchestrate-scaffold";
3
+
4
+ export function registerCommands() {
5
+ registerCommand({
6
+ name: "init",
7
+ description: "Initialize a new CLI project",
8
+ instructions: "You will be prompted for project details",
9
+ run: async () => {
10
+ await orchestrateScaffold();
11
+ },
12
+ });
13
+ }
package/src/config.ts ADDED
@@ -0,0 +1,6 @@
1
+ export const config = {
2
+ about_text:
3
+ "mkcmd is a remote node executable for scaffolding other remote node executables with sensible defaults.",
4
+ more_info_text:
5
+ "See https://github.com/mackenziebowes/mkcmd for more details.",
6
+ };
@@ -0,0 +1,93 @@
1
+ import log from "./log";
2
+ import { join } from "node:path";
3
+ import { config } from "../config";
4
+
5
+ export type Command = {
6
+ name: string;
7
+ description: string;
8
+ instructions: string;
9
+ run: (args: string[]) => Promise<void> | void;
10
+ };
11
+
12
+ const commands = new Map<string, Command>();
13
+
14
+ export function registerCommand(cmd: Command) {
15
+ commands.set(cmd.name, cmd);
16
+ }
17
+
18
+ export async function runCLI(argv = Bun.argv.slice(2)) {
19
+ const [name, ...args] = argv;
20
+ // -- TS Defense --
21
+ if (!name) {
22
+ log.single.err("ARGS", "No Argument Supplied");
23
+ return;
24
+ }
25
+ if (["-h", "--help"].includes(name)) {
26
+ const multiLog: any[] = [];
27
+ multiLog.push({
28
+ t: "About",
29
+ m: config.about_text,
30
+ });
31
+ multiLog.push({
32
+ t: "Commands",
33
+ m: "Available Commands",
34
+ });
35
+ for (const cmd of commands.values()) {
36
+ multiLog.push({
37
+ t: cmd.name,
38
+ m: `${cmd.description}\n |-> [Instructions]: ${cmd.instructions}\n`,
39
+ });
40
+ }
41
+ multiLog.push({
42
+ t: "More Info",
43
+ m: config.more_info_text,
44
+ });
45
+ log.multi.info(multiLog);
46
+ return;
47
+ }
48
+
49
+ if (["-v", "--version"].includes(name)) {
50
+ const pkgText = await Bun.file(join(process.cwd(), "package.json")).text();
51
+ const pkg = JSON.parse(pkgText);
52
+ log.multi.info([
53
+ {
54
+ t: "Package Name",
55
+ m: pkg.name,
56
+ },
57
+ {
58
+ t: "Package Version",
59
+ m: pkg.version,
60
+ },
61
+ ]);
62
+ return;
63
+ }
64
+
65
+ const command = commands.get(name);
66
+ if (!command) {
67
+ log.single.err("Command", "No Command Supplied");
68
+ process.exit(1);
69
+ }
70
+
71
+ try {
72
+ await command.run(args);
73
+ } catch (err) {
74
+ const multilog: any[] = [];
75
+ multilog.push({
76
+ t: "Panic",
77
+ m: `Failed to run ${name}`,
78
+ });
79
+ if (err instanceof Error) {
80
+ multilog.push({
81
+ t: "Error",
82
+ m: err.message,
83
+ });
84
+ } else {
85
+ multilog.push({
86
+ t: "Unknown Error",
87
+ m: JSON.stringify(err),
88
+ });
89
+ }
90
+ log.multi.err(multilog);
91
+ process.exit(1);
92
+ }
93
+ }
@@ -0,0 +1,19 @@
1
+ export function line(content: string, depth: number): string {
2
+ return `\n${"\t".repeat(depth)}${content}`;
3
+ }
4
+
5
+ export class FileBuilder {
6
+ private lines: string[] = [];
7
+
8
+ addLine(content: string, depth: number = 0): void {
9
+ this.lines.push(`${"\t".repeat(depth)}${content}`);
10
+ }
11
+
12
+ addEmptyLine(): void {
13
+ this.lines.push("");
14
+ }
15
+
16
+ build(): string {
17
+ return this.lines.join("\n");
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ import { mkdir, realpath } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ const pathCache = new Map<string, string>();
5
+
6
+ export async function getRealPath(dir: string): Promise<string> {
7
+ if (!pathCache.has(dir)) {
8
+ const real = await realpath(dir);
9
+ pathCache.set(dir, real);
10
+ }
11
+ return pathCache.get(dir)!;
12
+ }
13
+
14
+ export async function writeFileTuple([targetDir, relativePath, content]: [string, string, string]): Promise<void> {
15
+ const realDir = await getRealPath(targetDir);
16
+ const fullPath = join(realDir, relativePath);
17
+ await mkdir(join(fullPath, ".."), { recursive: true });
18
+ await Bun.write(fullPath, content);
19
+ }
@@ -0,0 +1,82 @@
1
+ import figlet from "figlet";
2
+
3
+ const infoLogSingle = (title: string, message: string) => {
4
+ console.log("");
5
+ console.info(" [INFO]");
6
+ console.info(` [${title}]: ${message}`);
7
+ console.log("");
8
+ };
9
+
10
+ type MultiLogArgs = {
11
+ t: string;
12
+ m: string;
13
+ }[];
14
+
15
+ const infoLogMulti = (args: MultiLogArgs) => {
16
+ console.log("");
17
+ console.info(" [INFO]");
18
+ for (const arg of args) {
19
+ console.info(` [${arg.t}]: ${arg.m}`);
20
+ }
21
+ console.log("");
22
+ };
23
+
24
+ const warnLogSingle = (title: string, message: string) => {
25
+ console.log("");
26
+ console.warn(" [WARNING]");
27
+ console.warn(` [${title}]: ${message}`);
28
+ console.log("");
29
+ };
30
+
31
+ const warnLogMulti = (args: MultiLogArgs) => {
32
+ console.log("");
33
+ console.warn(" [WARNING]");
34
+ for (const arg of args) {
35
+ console.warn(` [${arg.t}]: ${arg.m}`);
36
+ }
37
+ console.log("");
38
+ };
39
+
40
+ const errLogSingle = (title: string, message: string) => {
41
+ console.log("");
42
+ console.error(" [ERROR]");
43
+ console.error(` [${title}]: ${message}`);
44
+ console.log("");
45
+ };
46
+
47
+ const errLogMulti = (args: MultiLogArgs) => {
48
+ console.log("");
49
+ console.error(" [ERROR]");
50
+ for (const arg of args) {
51
+ console.error(` [${arg.t}]: ${arg.m}`);
52
+ }
53
+ console.log("");
54
+ };
55
+
56
+ const title = (title: string, subtitle?: string) => {
57
+ console.clear();
58
+ console.log();
59
+ console.log(
60
+ figlet.textSync(title, {
61
+ font: "ANSI Regular",
62
+ }),
63
+ );
64
+ console.log(` [${subtitle}]`);
65
+ console.log();
66
+ };
67
+
68
+ const log = {
69
+ single: {
70
+ info: infoLogSingle,
71
+ warn: warnLogSingle,
72
+ err: errLogSingle,
73
+ },
74
+ multi: {
75
+ info: infoLogMulti,
76
+ warn: warnLogMulti,
77
+ err: errLogMulti,
78
+ },
79
+ title,
80
+ };
81
+
82
+ export default log;
@@ -0,0 +1,102 @@
1
+ import { FileBuilder } from "../core/helpers/file-builder";
2
+
3
+ export const commands_init = () => {
4
+ const file = new FileBuilder();
5
+ file.addLine(`import { registerCommand } from "../core/cli";`, 0);
6
+ file.addEmptyLine();
7
+ file.addLine(`export function registerCommands() {}`, 0);
8
+ return file.build();
9
+ };
10
+
11
+ export const config_init = (about: string) => {
12
+ const file = new FileBuilder();
13
+ file.addLine(`export const config = {`, 0);
14
+ file.addLine(`about_text:`, 2);
15
+ file.addLine(`"${about}",`, 1);
16
+ file.addLine(`more_info_text:`, 2);
17
+ file.addLine(
18
+ `"See https://github.com/mackenziebowes/mkcmd for more details.",`,
19
+ 1,
20
+ );
21
+ file.addLine(`};`, 0);
22
+ return file.build();
23
+ };
24
+
25
+ export const package_init = (name: string) => {
26
+ const file = new FileBuilder();
27
+ file.addLine(`{`, 0);
28
+ file.addLine(`"name": "${name}",`, 1);
29
+ file.addLine(`"module": "index.ts",`, 1);
30
+ file.addLine(`"type": "module",`, 1);
31
+ file.addLine(`"private": true,`, 1);
32
+ file.addLine(`"bin": {`, 1);
33
+ file.addLine(`"${name.split(" ").join("")}": "./src/index.ts"`, 2);
34
+ file.addLine(`},`, 1);
35
+ file.addLine(`"devDependencies": {`, 1);
36
+ file.addLine(`"@types/bun": "latest"`, 2);
37
+ file.addLine(`},`, 1);
38
+ file.addLine(`"peerDependencies": {`, 1);
39
+ file.addLine(`"typescript": "^5"`, 2);
40
+ file.addLine(`},`, 1);
41
+ file.addLine(`"dependencies": {`, 1);
42
+ file.addLine(`"@clack/prompts": "^0.11.0",`, 2);
43
+ file.addLine(`"@types/figlet": "^1.7.0",`, 2);
44
+ file.addLine(`"figlet": "^1.9.4"`, 2);
45
+ file.addLine(`}`, 1);
46
+ file.addLine(`}`, 0);
47
+ return file.build();
48
+ };
49
+
50
+ export const readme_init = (name: string) => {
51
+ const file = new FileBuilder();
52
+ file.addLine(`# ${name}`);
53
+ file.addEmptyLine();
54
+ file.addLine(`To install dependencies:`);
55
+ file.addEmptyLine();
56
+ file.addLine("```bash");
57
+ file.addLine("bun install");
58
+ file.addLine("```");
59
+ file.addEmptyLine();
60
+ file.addLine("To run:");
61
+ file.addLine("```bash");
62
+ file.addLine("bun run index.ts");
63
+ file.addLine("```");
64
+ file.addLine(
65
+ "This project was created using `bun init` in bun v1.2.13. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.",
66
+ );
67
+ return file.build();
68
+ };
69
+
70
+ export const tsconfig_init = () => {
71
+ const file = new FileBuilder();
72
+ file.addLine(`{`, 0);
73
+ file.addLine(`"compilerOptions": {`, 1);
74
+ file.addEmptyLine();
75
+ file.addLine(`// Environment setup & latest features`, 1);
76
+ file.addLine(`"lib": ["ESNext"],`, 1);
77
+ file.addLine(`"target": "ESNext",`, 1);
78
+ file.addLine(`"module": "ESNext",`, 1);
79
+ file.addLine(`"moduleDetection": "force",`, 1);
80
+ file.addLine(`"jsx": "react-jsx",`, 1);
81
+ file.addLine(`"allowJs": true,`, 1);
82
+ file.addEmptyLine();
83
+ file.addLine(`// Bundler mode`, 1);
84
+ file.addLine(`"moduleResolution": "bundler",`, 1);
85
+ file.addLine(`"allowImportingTsExtensions": true,`, 1);
86
+ file.addLine(`"verbatimModuleSyntax": true,`, 1);
87
+ file.addLine(`"noEmit": true,`, 1);
88
+ file.addEmptyLine();
89
+ file.addLine(`// Best practices`, 1);
90
+ file.addLine(`"strict": true,`, 1);
91
+ file.addLine(`"skipLibCheck": true,`, 1);
92
+ file.addLine(`"noFallthroughCasesInSwitch": true,`, 1);
93
+ file.addLine(`"noUncheckedIndexedAccess": true,`, 1);
94
+ file.addEmptyLine();
95
+ file.addLine(`// Some stricter flags (disabled by default)`, 1);
96
+ file.addLine(`"noUnusedLocals": false,`, 1);
97
+ file.addLine(`"noUnusedParameters": false,`, 1);
98
+ file.addLine(`"noPropertyAccessFromIndexSignature": false`, 1);
99
+ file.addLine(`}`, 1);
100
+ file.addLine(`}`, 0);
101
+ return file.build();
102
+ };
@@ -0,0 +1,21 @@
1
+ import { promptProjectDetails, ProjectPrompts } from "./prompt-project";
2
+ import { scaffoldProject } from "./scaffold-project";
3
+ import { scaffoldCore } from "./scaffold-core";
4
+ import log from "../core/log";
5
+ import { config } from "../config";
6
+
7
+ export async function orchestrateScaffold() {
8
+ const prompts = await promptProjectDetails();
9
+
10
+ await scaffoldProject(prompts);
11
+ log.single.info("Project", "Project files created");
12
+
13
+ await scaffoldCore(prompts.targetDir);
14
+ log.single.info("Core", "Core files copied");
15
+
16
+ log.multi.info([
17
+ { t: "Success", m: "Project scaffolded successfully!" },
18
+ { t: "Location", m: prompts.targetDir },
19
+ { t: "Next", m: `cd ${prompts.targetDir} && bun install` },
20
+ ]);
21
+ }
@@ -0,0 +1,31 @@
1
+ import { text, intro, outro } from "@clack/prompts";
2
+
3
+ export interface ProjectPrompts {
4
+ projectName: string;
5
+ targetDir: string;
6
+ description: string;
7
+ }
8
+
9
+ export async function promptProjectDetails(): Promise<ProjectPrompts> {
10
+ intro("mkcmd - Scaffold a new CLI project");
11
+
12
+ const projectName = (await text({
13
+ message: "What is your project name?",
14
+ placeholder: "my-cli",
15
+ })) as string;
16
+
17
+ const targetDir = (await text({
18
+ message: "Where should the project be created?",
19
+ placeholder: "./my-cli",
20
+ initialValue: `./${projectName}`,
21
+ })) as string;
22
+
23
+ const description = (await text({
24
+ message: "What does this project do?",
25
+ placeholder: "A CLI tool that does amazing things",
26
+ })) as string;
27
+
28
+ outro("Project details collected!");
29
+
30
+ return { projectName, targetDir, description };
31
+ }
@@ -0,0 +1,18 @@
1
+ import { join } from "node:path";
2
+ import { readdir } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { writeFileTuple } from "../core/helpers/file-utils";
5
+
6
+ const currentDir = join(fileURLToPath(import.meta.url), "../..");
7
+ const sourceCoreDir = join(currentDir, "src", "core");
8
+
9
+ export async function scaffoldCore(targetDir: string) {
10
+ const files = await readdir(sourceCoreDir);
11
+
12
+ for (const file of files) {
13
+ if (file.endsWith(".ts")) {
14
+ const content = await Bun.file(join(sourceCoreDir, file)).text();
15
+ await writeFileTuple([targetDir, `src/core/${file}`, content]);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,19 @@
1
+ import { type ProjectPrompts } from "./prompt-project";
2
+ import { writeFileTuple } from "../core/helpers/file-utils";
3
+ import {
4
+ commands_init,
5
+ config_init,
6
+ package_init,
7
+ readme_init,
8
+ tsconfig_init,
9
+ } from "../data/init";
10
+
11
+ export async function scaffoldProject(prompts: ProjectPrompts) {
12
+ const { projectName, targetDir, description } = prompts;
13
+
14
+ await writeFileTuple([targetDir, "src/commands/index.ts", commands_init()]);
15
+ await writeFileTuple([targetDir, "src/config.ts", config_init(description)]);
16
+ await writeFileTuple([targetDir, "package.json", package_init(projectName)]);
17
+ await writeFileTuple([targetDir, "README.md", readme_init(projectName)]);
18
+ await writeFileTuple([targetDir, "tsconfig.json", tsconfig_init()]);
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bun
2
+ import { runCLI } from "./core/cli";
3
+ import { registerCommands } from "./commands";
4
+
5
+ async function main() {
6
+ registerCommands();
7
+ runCLI();
8
+ }
9
+
10
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+
23
+ // Some stricter flags (disabled by default)
24
+ "noUnusedLocals": false,
25
+ "noUnusedParameters": false,
26
+ "noPropertyAccessFromIndexSignature": false
27
+ }
28
+ }