@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.
- package/dist/create-app.js +217 -0
- package/package.json +43 -0
- package/readme.md +8 -0
- package/template/.editorconfig +7 -0
- package/template/.gitattributes +2 -0
- package/template/.github/workflows/ci.yml +28 -0
- package/template/.prettierignore +0 -0
- package/template/.prettierrc +1 -0
- package/template/.rgignore +1 -0
- package/template/data/i18n/en-US.json +59 -0
- package/template/package-lock.json +588 -0
- package/template/package.json +45 -0
- package/template/src/bitecli.ts +66 -0
- package/template/src/commands/cocommand.ts +294 -0
- package/template/src/commands/configcommand.ts +151 -0
- package/template/src/commands/hellocommand.ts +43 -0
- package/template/src/commands/helpcommand.ts +21 -0
- package/template/src/defaultvals.ts +60 -0
- package/template/src/globals.d.ts +11 -0
- package/template/src/libs/i18n.ts +138 -0
- package/template/src/libs/meta.ts +399 -0
- package/template/src/libs/types/types.ts +59 -0
- package/template/src/templatevals.ts +2 -0
- package/template/tests/commands.test.js +19 -0
- package/template/tests/testutils.js +13 -0
- package/template/tsconfig.json +29 -0
|
@@ -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,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
|
+
}
|