@cmdwuzntfnd/bitecli 0.1.20

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.
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execSync } from "node:child_process";
6
+ import { createInterface } from "node:readline/promises";
7
+ const red = (text) => `\x1b[31m${text}\x1b[0m`;
8
+ const blue = (text) => `\x1b[34m${text}\x1b[0m`;
9
+ const dim = (text) => `\x1b[2m${text}\x1b[0m`;
10
+ async function askQuestions(questions) {
11
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
12
+ const answers = {};
13
+ try {
14
+ for (const question of questions) {
15
+ let answer;
16
+ let valid = false;
17
+ while (!valid) {
18
+ const defaultHint = question.initial
19
+ ? dim(` (${question.initial})`)
20
+ : "";
21
+ const promptText = `${question.message}${defaultHint}: `;
22
+ const rawAnswer = await rl.question(promptText);
23
+ answer = rawAnswer.trim() || question.initial;
24
+ if (answer === undefined) {
25
+ console.log(red(" > An answer is required."));
26
+ continue;
27
+ }
28
+ if (question.validate) {
29
+ const validationResult = question.validate(answer);
30
+ if (validationResult === true) {
31
+ valid = true;
32
+ }
33
+ else {
34
+ console.log(red(` > ${validationResult}`));
35
+ }
36
+ }
37
+ else {
38
+ valid = true;
39
+ }
40
+ }
41
+ answers[question.name] = answer;
42
+ }
43
+ }
44
+ finally {
45
+ rl.close();
46
+ }
47
+ return answers;
48
+ }
49
+ const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
50
+ function replaceInDirectory(dir, find, replace) {
51
+ const items = fs.readdirSync(dir, { withFileTypes: true });
52
+ for (const item of items) {
53
+ const itemPath = path.join(dir, item.name);
54
+ if (item.isDirectory()) {
55
+ replaceInDirectory(itemPath, find, replace);
56
+ }
57
+ else if (item.isFile()) {
58
+ try {
59
+ const content = fs.readFileSync(itemPath, "utf-8");
60
+ if (content.includes(find)) {
61
+ const newContent = content.replaceAll(find, replace);
62
+ fs.writeFileSync(itemPath, newContent, "utf-8");
63
+ }
64
+ }
65
+ catch (error) {
66
+ console.warn(dim(`Could not process binary or unreadable file: ${itemPath}`));
67
+ }
68
+ }
69
+ }
70
+ }
71
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
72
+ const templateDir = path.resolve(__dirname, "..", "template");
73
+ async function main() {
74
+ console.log(blue("Welcome! Let's create your new CLI app."));
75
+ const initialProjectName = process.argv[2] || "my-new-cli";
76
+ const projectResponse = await askQuestions([
77
+ {
78
+ name: "projectName",
79
+ message: "Project name",
80
+ initial: initialProjectName.replace(/[^\w-]/g, ""),
81
+ validate: (name) => /^[a-z0-9-_]+$/.test(name)
82
+ ? true
83
+ : "Project name requires lowercase letters, numbers, hyphens.",
84
+ },
85
+ {
86
+ name: "description",
87
+ message: "Short description",
88
+ initial: "A new CLI app.",
89
+ },
90
+ { name: "author", message: "Author" },
91
+ ]);
92
+ if (!projectResponse.projectName) {
93
+ console.log("\nProject creation cancelled.");
94
+ return;
95
+ }
96
+ console.log(blue("\nGreat! Now let's configure your first command."));
97
+ const commandResponse = await askQuestions([
98
+ {
99
+ name: "commandName",
100
+ message: "Command name (e.g., greet, create)",
101
+ initial: "greet",
102
+ validate: (name) => /^[a-z]+$/.test(name)
103
+ ? true
104
+ : "Command name requires lowercase letters.",
105
+ },
106
+ {
107
+ name: "commandDesc",
108
+ message: "Short description for this command",
109
+ initial: "A simple example command.",
110
+ },
111
+ {
112
+ name: "optionName",
113
+ message: "Primary option (e.g. name, target)",
114
+ initial: "name",
115
+ validate: (name) => /^[a-z]+$/.test(name)
116
+ ? true
117
+ : "Option name requires lowercase letters.",
118
+ },
119
+ {
120
+ name: "optionDesc",
121
+ message: "Description for the option",
122
+ initial: "The value to use for the command.",
123
+ },
124
+ ]);
125
+ const { projectName, description, author } = projectResponse;
126
+ const { commandName, commandDesc, optionName, optionDesc } = commandResponse;
127
+ const targetDir = path.resolve(process.cwd(), projectName);
128
+ try {
129
+ fs.statSync(targetDir);
130
+ console.error(`\nError: Directory "${projectName}" already exists.`);
131
+ process.exit(1);
132
+ }
133
+ catch (err) {
134
+ const error = err;
135
+ if (error?.code !== "ENOENT") {
136
+ throw error;
137
+ }
138
+ }
139
+ const SRC = path.join(targetDir, "src");
140
+ const DATA = path.join(targetDir, "data");
141
+ const I18N = path.join(DATA, "i18n");
142
+ const ENUS = path.join(I18N, "en-US.json");
143
+ const COMMANDS = path.join(SRC, "commands");
144
+ const CO_PATH = path.join(COMMANDS, "cocommand.ts");
145
+ const NEW_COMMAND = path.join(COMMANDS, `${commandName}command.ts`);
146
+ const LIBS = path.join(SRC, "libs");
147
+ const META_PATH = path.join(LIBS, "meta.ts");
148
+ const PKG = path.join(targetDir, "package.json");
149
+ const NPMIGNORE = path.join(targetDir, ".npmignore");
150
+ const npmignoreContent = `*.ts\n*.map\ntsconfig.json`;
151
+ console.log(`\nCreating project in ${targetDir}...`);
152
+ fs.cpSync(templateDir, targetDir, { recursive: true });
153
+ console.log("Customizing project files...");
154
+ replaceInDirectory(targetDir, "bitecli", projectName);
155
+ fs.renameSync(path.join(SRC, "bitecli.ts"), path.join(SRC, `${projectName}.ts`));
156
+ const pkgJson = JSON.parse(fs.readFileSync(PKG, "utf-8"));
157
+ pkgJson.name = projectName;
158
+ pkgJson.description = description;
159
+ pkgJson.author = author;
160
+ if (pkgJson.bin.bitecli) {
161
+ pkgJson.bin[projectName] = pkgJson.bin.bitecli;
162
+ delete pkgJson.bin.bitecli;
163
+ }
164
+ fs.writeFileSync(PKG, JSON.stringify(pkgJson, null, 2) + "\n");
165
+ console.log(`Configuring '${commandName}' command...`);
166
+ fs.renameSync(path.join(COMMANDS, "hellocommand.ts"), path.join(COMMANDS, `${commandName}command.ts`));
167
+ let metaContent = fs.readFileSync(META_PATH, "utf-8");
168
+ metaContent = metaContent.replace('hello: "hellocommand"', `${commandName}: "${commandName}command"`);
169
+ fs.writeFileSync(META_PATH, metaContent);
170
+ let coContent = fs.readFileSync(CO_PATH, "utf-8");
171
+ coContent = coContent.replace("hello: { Greeting: GREETING }", `${commandName}: { Greeting: GREETING }`);
172
+ fs.writeFileSync(CO_PATH, coContent);
173
+ let newCommandContent = fs.readFileSync(NEW_COMMAND, "utf-8");
174
+ const capOptionName = capitalize(optionName);
175
+ const newClassName = `${capitalize(commandName)}Command`;
176
+ newCommandContent = newCommandContent
177
+ .replaceAll("HelloCommand", newClassName)
178
+ .replaceAll('name: { type: "string", short: "n" }', `${optionName}: { type: "string", short: "${optionName[0]}" }`)
179
+ .replaceAll("argValues.name", `argValues.${optionName}`)
180
+ .replaceAll("s.help.commands.hello", `s.help.commands.${commandName}`)
181
+ .replaceAll(".Name", `.${capOptionName}`);
182
+ fs.writeFileSync(NEW_COMMAND, newCommandContent);
183
+ const i18nJson = JSON.parse(fs.readFileSync(ENUS, "utf-8"));
184
+ i18nJson.help.generic.header = `${projectName}: ${description}`;
185
+ delete i18nJson.help.generic.commandDescriptions.hello;
186
+ i18nJson.help.generic.commandDescriptions[commandName] = commandDesc;
187
+ const helloCommandHelp = i18nJson.help.commands.hello;
188
+ if (helloCommandHelp) {
189
+ i18nJson.help.commands[commandName] = helloCommandHelp;
190
+ delete i18nJson.help.commands.hello;
191
+ const newCommandHelp = i18nJson.help.commands[commandName];
192
+ newCommandHelp.usage = newCommandHelp.usage.replace("hello", commandName);
193
+ newCommandHelp.description = commandDesc;
194
+ newCommandHelp.flags[optionName] = optionDesc;
195
+ delete newCommandHelp.flags.name;
196
+ }
197
+ fs.writeFileSync(ENUS, JSON.stringify(i18nJson, null, 2) + "\n");
198
+ console.log("Creating .npmignore file...");
199
+ fs.writeFileSync(NPMIGNORE, npmignoreContent);
200
+ console.log("Installing dependencies...");
201
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
202
+ console.log(`\n✅ Success! Your new CLI app "${projectName}" is ready.`);
203
+ console.log("\nNext steps:");
204
+ console.log(` cd ${projectName}`);
205
+ console.log(" npm run build");
206
+ console.log(` npm link (to make the command available globally for testing)`);
207
+ console.log(` ${projectName} ${commandName} --help`);
208
+ }
209
+ await main().catch((err) => {
210
+ const error = err;
211
+ if (error?.code === "ABORT_ERR") {
212
+ console.log("\n\nProject creation cancelled.");
213
+ process.exit(0);
214
+ }
215
+ console.error(red("\nAn unexpected error occurred:"), err);
216
+ process.exit(1);
217
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cmdwuzntfnd/bitecli",
3
+ "author": "cmdntfnd",
4
+ "version": "0.1.20",
5
+ "description": "Creates a new CLI app from the bitecli template.",
6
+ "files": [
7
+ "dist/",
8
+ "template/"
9
+ ],
10
+ "bin": {
11
+ "bitecli": "dist/create-app.js"
12
+ },
13
+ "scripts": {
14
+ "clean": "del-cli \"dist\"",
15
+ "prebuild": "npm run clean",
16
+ "prepack": "npm i && npm run build && del-cli \"dist/**/*.map\"",
17
+ "build": "tsc",
18
+ "start": "node ./dist/create-app.js",
19
+ "format": "npx prettier . --write"
20
+ },
21
+ "type": "module",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/cmdntfnd/bitecli.git"
25
+ },
26
+ "keywords": [
27
+ "cli",
28
+ "framework",
29
+ "scaffolding",
30
+ "bitecli"
31
+ ],
32
+ "license": "MIT",
33
+ "engines": {
34
+ "node": ">=24.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "24.3.1",
38
+ "cross-env": "10.0.0",
39
+ "del-cli": "6.0.0",
40
+ "prettier": "3.6.2",
41
+ "typescript": "5.9.2"
42
+ }
43
+ }
package/readme.md ADDED
@@ -0,0 +1,8 @@
1
+ Initial extraction of a scaffolding from batchbite for a very basic but dependency free quick copy and paste for making throw away scripted cli tools.
2
+ Not a framework, nor a real library. Copy, paste and quickly get something okay looking for personal use.
3
+ Requires Node.JS (v24) and TypeScript. Only really needs v24 because I like RegExp.escape(), deal with it.
4
+ Run:
5
+
6
+ ```bash
7
+ npx @cmdwuzntfnd/bitecli@latest
8
+ ```
@@ -0,0 +1,7 @@
1
+ [*]
2
+ charset = utf-8
3
+ insert_final_newline = true
4
+ end_of_line = lf
5
+ indent_style = space
6
+ indent_size = 2
7
+ max_line_length = 80
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ data/** -diff
@@ -0,0 +1,28 @@
1
+ name: Node.js CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+
7
+ jobs:
8
+ build-and-test:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - name: Checkout repository
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Use Node.js 24.x
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: "24"
19
+ cache: "npm"
20
+
21
+ - name: Install dependencies
22
+ run: npm ci
23
+
24
+ - name: Build project
25
+ run: npm run build
26
+
27
+ - name: Run tests
28
+ run: npm test
File without changes
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ data/
@@ -0,0 +1,59 @@
1
+ {
2
+ "errors": {
3
+ "unknownErrorOccurred": "An unknown error occurred.",
4
+ "commandNotImplemented": "Error: This command is not implemented.",
5
+ "cfgCouldNotBeLoaded": "Error: The configuration file could not be loaded from '{{ .UserConfigPath }}'.",
6
+ "sourceRequired": "Error: A source file or directory is required.",
7
+ "editorNotFound": "Warning: $EDITOR environment variable not set. Cannot open configuration file automatically.",
8
+ "editorLaunchFailed": "Error: Failed to launch editor: {{ .ErrorMessage }}",
9
+ "failedToWriteLocale": "Error: Failed to write locale settings: {{ .ErrorMessage }}",
10
+ "coError": "Warning: Could not generate completions for command '{{ .Command }}'."
11
+ },
12
+ "messages": {
13
+ "userConfigNotFound": "User configuration not found at '{{ .UserConfigPath }}'.",
14
+ "cfgCreatedSuccessfully": "A new configuration file has been created successfully.",
15
+ "deletionConfirm": "Are you sure you want to delete these files? This action cannot be undone. (y/N): ",
16
+ "yN": "y",
17
+ "deletionAborted": "Deletion aborted.",
18
+ "cfgDeletedSuccessfully": "Configuration file(s) deleted successfully.",
19
+ "localeSuccessfullyChanged": "Application language changed to {{ .Locale }}."
20
+ },
21
+ "help": {
22
+ "generic": {
23
+ "header": "bitecli: A tool for fun things.",
24
+ "usage": "Usage: bitecli <command> [options]",
25
+ "commandHeader": "Commands:",
26
+ "commandDescriptions": {
27
+ "hello": "A simple example command that prints a greeting.",
28
+ "cfg": "Manage application configuration.",
29
+ "co": "Generate bash completion script."
30
+ },
31
+ "footer": "For more information on any command, use `bitecli <command> --help`.",
32
+ "globalOptionsHeader": "Global Options:",
33
+ "flags": {
34
+ "version": "Show application version."
35
+ }
36
+ },
37
+ "commands": {
38
+ "cfg": {
39
+ "usage": "Usage: bitecli cfg [options]",
40
+ "description": "Manages the application configuration.",
41
+ "flags": {
42
+ "help": "Show this help message.",
43
+ "edit": "Open the user configuration file in the default editor.",
44
+ "remove": "Delete the user configuration files (prompts for confirmation).",
45
+ "lang": "Set the application language. Supported locales:"
46
+ },
47
+ "footer": "{{ .LocaleList }}"
48
+ },
49
+ "hello": {
50
+ "usage": "Usage: bitecli hello [options]",
51
+ "description": "Prints a customizable greeting. Templated string substitution: {{ .Greeting }}",
52
+ "flags": {
53
+ "help": "Show this help message.",
54
+ "name": "The name to greet."
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }