@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,66 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { strings as s } from "#i18n";
4
+ import {
5
+ commandConfig,
6
+ setDebug,
7
+ appState,
8
+ red,
9
+ isNodeError,
10
+ exitOne,
11
+ } from "#meta";
12
+ import "#defaultvals"; // Import for side effects (config generation)
13
+ import type { CommandModule } from "#types";
14
+
15
+ commandConfig._default = commandConfig.help;
16
+ let parsedGlobalArgs: ReturnType<typeof parseArgs>;
17
+
18
+ async function applyCommand(): Promise<void> {
19
+ const commandAlias =
20
+ (parsedGlobalArgs.positionals[0] as string) ?? "_default";
21
+ const commandModuleName = commandConfig[commandAlias];
22
+ if (!commandModuleName) {
23
+ exitOne();
24
+ console.error(red(s.errors.commandNotImplemented));
25
+ return;
26
+ }
27
+ const modulePath = `#commands/${commandModuleName}`;
28
+ const { default: CommandClass } = (await import(modulePath)) as CommandModule;
29
+ const commandInstance = new CommandClass();
30
+ await commandInstance.execute(process.argv.slice(2));
31
+ }
32
+
33
+ async function main(): Promise<number> {
34
+ try {
35
+ // Parse global flags like --version before handing off to commands
36
+ parsedGlobalArgs = parseArgs({
37
+ strict: false, // Allow command-specific options
38
+ args: process.argv.slice(2),
39
+ });
40
+ if (parsedGlobalArgs.values.version) {
41
+ console.log(red(`${appState.P_NAME}: ${appState.P_VERSION}`));
42
+ return 0;
43
+ }
44
+ if (parsedGlobalArgs.values.debug) {
45
+ setDebug();
46
+ }
47
+ await applyCommand();
48
+ return 0;
49
+ } catch (err: unknown) {
50
+ exitOne();
51
+ if (isNodeError(err)) {
52
+ console.error(red(err.message));
53
+ if (isNodeError(err.cause)) {
54
+ console.error(red(`>Cause: ${err.cause.message ?? String(err.cause)}`));
55
+ }
56
+ if (parsedGlobalArgs?.values.debug && err.stack) {
57
+ console.error(err.stack);
58
+ }
59
+ } else {
60
+ console.error(red(String(err)));
61
+ }
62
+ return 1;
63
+ }
64
+ }
65
+
66
+ await main();
@@ -0,0 +1,294 @@
1
+ import type { Command, CommandConstructor, PositionalCompletion } from "#types";
2
+ import { commandConfig, createError, P_NAME, isNodeError } from "#meta";
3
+ import { strings as s } from "#i18n";
4
+ import { GREETING } from "#defaultvals";
5
+
6
+ // this is where you add template substitutions for help texts
7
+ // e.g. if a help text contains "{{ .Greeting }}", it will be replaced with the value of GREETING
8
+ // from #defaultvals.ts
9
+ // Add more entries as needed for other commands and placeholders
10
+ // The structure is: { commandName: { Placeholder: value, ... }, ... }
11
+ const replacements: Record<string, Record<string, string>> = {
12
+ hello: { Greeting: GREETING },
13
+ };
14
+
15
+ function applyReplacements(
16
+ template: string,
17
+ commandReplacements: Record<string, string> | undefined,
18
+ ): string {
19
+ if (!template || !commandReplacements) {
20
+ return template;
21
+ }
22
+ let result = template;
23
+ for (const [key, value] of Object.entries(commandReplacements)) {
24
+ const placeholder = `{{ .${key} }}`;
25
+ result = result.replace(new RegExp(RegExp.escape(placeholder), "g"), value);
26
+ }
27
+ return result;
28
+ }
29
+
30
+ export default class CoCommand implements Command {
31
+ static allowPositionals = false;
32
+ static positionalCompletion = "none" as const;
33
+ static options = {};
34
+ async execute(argv: string[]): Promise<number> {
35
+ const script = await this.generateBashCompletionScript();
36
+ console.log(script);
37
+ return 0;
38
+ }
39
+
40
+ private async generateBashCompletionScript(): Promise<string> {
41
+ const programName = P_NAME || "batchbite";
42
+
43
+ const subcommands = Object.keys(commandConfig)
44
+ .filter((c) => c !== "_default")
45
+ .sort();
46
+ const allSubcommands = [...subcommands].sort();
47
+ const globalOpts = ["--version", "--help"].sort().join(" ");
48
+
49
+ let caseBlock = "";
50
+
51
+ const sortedCommandConfigEntries = Object.entries(commandConfig).sort(
52
+ ([a], [b]) => a.localeCompare(b),
53
+ );
54
+
55
+ for (const [alias, moduleName] of sortedCommandConfigEntries) {
56
+ if (alias === "_default") continue;
57
+
58
+ const modulePath = `#commands/${moduleName}`;
59
+ try {
60
+ const { default: CommandClass } = (await import(modulePath)) as {
61
+ default: CommandConstructor;
62
+ };
63
+
64
+ const commandsHelp = s.help.commands;
65
+ const cmdHelpSection =
66
+ commandsHelp[alias as keyof typeof commandsHelp] ??
67
+ commandsHelp[moduleName as keyof typeof commandsHelp];
68
+
69
+ let helpFlags: Record<string, string> | undefined;
70
+ if (
71
+ cmdHelpSection &&
72
+ "flags" in cmdHelpSection &&
73
+ cmdHelpSection.flags
74
+ ) {
75
+ helpFlags = cmdHelpSection.flags;
76
+ }
77
+
78
+ caseBlock += this.generateCaseForCommand(
79
+ alias,
80
+ CommandClass,
81
+ helpFlags,
82
+ );
83
+ } catch (err) {
84
+ if (isNodeError(err)) {
85
+ caseBlock += ` # failed to load command ${alias} (${moduleName}): ${String(
86
+ err.message ?? err,
87
+ )}\n`;
88
+ createError(s.errors.coError.replace("{{ .Command }}", alias), {
89
+ cause: err,
90
+ });
91
+ } else {
92
+ createError(s.errors.unknownErrorOccurred, { cause: err });
93
+ }
94
+ }
95
+ }
96
+
97
+ const script = `#!/usr/bin/env bash
98
+ # Bash completion for ${programName}
99
+ # Generated on: ${new Date().toISOString()}
100
+
101
+ _${programName}_completions() {
102
+ local cur prev words cword
103
+ _get_comp_words_by_ref -n : cur prev words cword
104
+
105
+ local subcommands="${allSubcommands.join(" ")}"
106
+ local global_opts="${globalOpts}"
107
+
108
+ # Helpers: file/dir completion that preserves tilde
109
+ _bb_filedir() {
110
+ local expanded_cur="\${cur/#\~/$HOME}"
111
+ COMPREPLY=( $(compgen -f -- "\${expanded_cur}") )
112
+ if [[ "\${cur}" == "~"* && "\${#COMPREPLY[@]}" -gt 0 ]]; then
113
+ for i in "\${!COMPREPLY[@]}"; do
114
+ COMPREPLY[i]="~/\${COMPREPLY[i]#$HOME/}"
115
+ done
116
+ fi
117
+ }
118
+
119
+ _bb_dirdir() {
120
+ local expanded_cur="\${cur/#\~/$HOME}"
121
+ COMPREPLY=( $(compgen -d -- "\${expanded_cur}") )
122
+ if [[ "\${cur}" == "~"* && "\${#COMPREPLY[@]}" -gt 0 ]]; then
123
+ for i in "\${!COMPREPLY[@]}"; do
124
+ COMPREPLY[i]="~/\${COMPREPLY[i]#$HOME/}"
125
+ done
126
+ fi
127
+ }
128
+
129
+ # Top-level: if completing the program itself (first arg)
130
+ if [[ $cword -eq 1 ]]; then
131
+ COMPREPLY=( $(compgen -W "\${subcommands} \${global_opts}" -- "\${cur}") )
132
+ return 0
133
+ fi
134
+
135
+ case "\${words[1]}" in
136
+ ${caseBlock}
137
+ help)
138
+ COMPREPLY=( $(compgen -W "\${subcommands}" -- "\${cur}") )
139
+ ;;
140
+ *)
141
+ COMPREPLY=()
142
+ ;;
143
+ esac
144
+
145
+ return 0
146
+ }
147
+
148
+ complete -F _${programName}_completions ${programName}
149
+ `;
150
+ return script;
151
+ }
152
+
153
+ private generateCaseForCommand(
154
+ alias: string,
155
+ CommandClass: CommandConstructor,
156
+ helpFlags?: Record<string, string>,
157
+ ): string {
158
+ const options = CommandClass.options ?? {};
159
+ const allowPositionals = CommandClass.allowPositionals ?? false;
160
+ const positionalCompletion = CommandClass.positionalCompletion ?? "none";
161
+
162
+ const longOpts = Object.keys(options).map((o) => `--${o}`);
163
+ const shortOpts = Object.values(options)
164
+ .map((cfg) => (cfg.short ? `-${cfg.short}` : ""))
165
+ .filter(Boolean);
166
+
167
+ const descEntries: string[] = [];
168
+ const optsWords: string[] = [];
169
+
170
+ for (const [longName, cfg] of Object.entries(options)) {
171
+ const longOpt = `--${longName}`;
172
+ optsWords.push(longOpt);
173
+
174
+ let desc = (helpFlags && helpFlags[longName]) || "";
175
+ const commandReplacements = replacements[alias];
176
+ desc = applyReplacements(desc, commandReplacements);
177
+
178
+ const safeKey = escapeForSingleQuotedBash(longOpt);
179
+ const safeVal = escapeForDoubleQuotedBash(desc);
180
+
181
+ descEntries.push(`_BB_DESC['${safeKey}']="${safeVal}"`);
182
+
183
+ if (cfg.short) {
184
+ const shortOpt = `-${cfg.short}`;
185
+ optsWords.push(shortOpt);
186
+ descEntries.push(
187
+ `_BB_DESC['${escapeForSingleQuotedBash(shortOpt)}']="${safeVal}"`,
188
+ );
189
+ }
190
+ }
191
+
192
+ const allOpts = Array.from(new Set([...longOpts, ...shortOpts]))
193
+ .sort()
194
+ .join(" ");
195
+
196
+ const valueOpts = Object.entries(options)
197
+ .filter(([, cfg]) => cfg.type === "string")
198
+ .flatMap(([long, cfg]) => {
199
+ const arr = [`--${long}`];
200
+ if (cfg.short) arr.push(`-${cfg.short}`);
201
+ return arr;
202
+ })
203
+ .map((v) => v.replace(/(["'\\])/g, "\\$1"));
204
+
205
+ const valueOptsJoined = valueOpts.join(" ");
206
+
207
+ const positionalHandler = this.getPositionalHandler(
208
+ allowPositionals,
209
+ positionalCompletion,
210
+ );
211
+
212
+ const block = `
213
+ ${alias})
214
+ # Option words for ${alias}
215
+ local _opts="${allOpts}"
216
+ # associative array _BB_DESC maps option -> description
217
+ declare -A _BB_DESC=()
218
+ ${descEntries.length > 0 ? descEntries.join("\n ") : "# no descriptions available"}
219
+
220
+ # If previous argument is one that requires a value, provide file/dir completions
221
+ ${
222
+ valueOpts.length > 0
223
+ ? `
224
+ if [[ " ${valueOptsJoined} " == *" \${prev} "* ]]; then
225
+ ${positionalHandler === "_bb_filedir" ? "_bb_filedir" : positionalHandler === "_bb_dirdir" ? "_bb_dirdir" : "# no specific value completion"}
226
+ return 0
227
+ fi
228
+ `
229
+ : ""
230
+ }
231
+
232
+ # If current token looks like an option, complete options
233
+ if [[ "\${cur}" == -* ]]; then
234
+ COMPREPLY=( $(compgen -W "\${_opts}" -- "\${cur}") )
235
+
236
+ # If bash is asking for a listing (double-Tab), COMP_TYPE==63. Print descriptions then clear COMPREPLY
237
+ if [[ -n "\${COMP_TYPE-}" && "\${COMP_TYPE}" -eq 63 ]]; then
238
+ printf "\\n"
239
+ local k d
240
+ for k in "\${COMPREPLY[@]}"; do
241
+ d="\${_BB_DESC[\${k}]:-}"
242
+ if [[ -n "\${d}" ]]; then
243
+ printf "%-28s %s\n" "\${k}" "\${d}"
244
+ else
245
+ printf "%s\n" "\${k}"
246
+ fi
247
+ done
248
+ COMPREPLY=()
249
+ return 0
250
+ fi
251
+
252
+ # Normal completion: return matching words so Tab completes
253
+ return 0
254
+ else
255
+ # not an option token -> positional completion (file/dir/none)
256
+ ${positionalHandler}
257
+ fi
258
+ ;;
259
+ `;
260
+
261
+ return block;
262
+ }
263
+
264
+ private getPositionalHandler(
265
+ allow: boolean,
266
+ type: PositionalCompletion,
267
+ ): string {
268
+ if (!allow) return "COMPREPLY=()";
269
+ switch (type) {
270
+ case "file":
271
+ return "_bb_filedir";
272
+ case "directory":
273
+ return "_bb_dirdir";
274
+ case "none":
275
+ default:
276
+ return "COMPREPLY=()";
277
+ }
278
+ }
279
+ }
280
+
281
+ function escapeForSingleQuotedBash(str: string | undefined): string {
282
+ if (!str) return "";
283
+ return str.replace(/'/g, `'"'"'`);
284
+ }
285
+
286
+ function escapeForDoubleQuotedBash(str: string | undefined): string {
287
+ if (!str) return "";
288
+ return str
289
+ .replace(/\\/g, `\\\\`)
290
+ .replace(/"/g, `\\"`)
291
+ .replace(/\$/g, `\\$`)
292
+ .replace(/`/g, "\\`")
293
+ .replace(/\r?\n/g, "\\n");
294
+ }
@@ -0,0 +1,151 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { generateLocaleList, isValidLocale, strings as s } from "#i18n";
6
+ import {
7
+ appState,
8
+ generateHelpText,
9
+ red,
10
+ yellowBright,
11
+ exitOne,
12
+ isNodeError,
13
+ } from "#meta";
14
+ import { spawn } from "node:child_process";
15
+ import type { Command } from "#types";
16
+
17
+ export function openWith(filePath: string) {
18
+ const editor = process.env.EDITOR;
19
+
20
+ if (!editor) {
21
+ console.warn(yellowBright(`${s.errors.editorNotFound}`));
22
+ return;
23
+ }
24
+
25
+ const child = spawn(editor, [filePath], {
26
+ stdio: "inherit",
27
+ });
28
+
29
+ child.on("error", (err) => {
30
+ console.error(
31
+ red(
32
+ `${s.errors.editorLaunchFailed.replace("{{ .ErrorMessage }}", err.message)}`,
33
+ ),
34
+ );
35
+ });
36
+ }
37
+
38
+ export default class CfgCommand implements Command {
39
+ static allowPositionals = false;
40
+ static positionalCompletion = "none" as const;
41
+ static options = {
42
+ help: { type: "boolean", short: "h" },
43
+ edit: { type: "boolean", short: "e" },
44
+ remove: { type: "boolean", short: "r" },
45
+ lang: { type: "string", short: "l" },
46
+ } as const;
47
+ async execute(argv: string[]): Promise<number> {
48
+ const { values: argValues, positionals } = parseArgs({
49
+ args: argv.slice(1),
50
+ allowPositionals: false,
51
+ strict: true,
52
+ options: (this.constructor as typeof CfgCommand).options,
53
+ });
54
+
55
+ const hasArguments =
56
+ Object.keys(argValues).some((key) => {
57
+ const value = argValues[key as keyof typeof argValues];
58
+ if (typeof value === "boolean") {
59
+ return value === true;
60
+ }
61
+ if (typeof value === "string") {
62
+ return value !== "";
63
+ }
64
+ return value !== undefined && value !== null;
65
+ }) || positionals.length > 1;
66
+
67
+ if (!hasArguments || argValues.help) {
68
+ const replacements = {
69
+ LocaleList: generateLocaleList(),
70
+ };
71
+ const helpText = generateHelpText(
72
+ s.help.commands.cfg,
73
+ (this.constructor as typeof CfgCommand).options,
74
+ replacements,
75
+ );
76
+ console.log(helpText);
77
+ return 0;
78
+ }
79
+
80
+ const cfgPath = path.join(appState.STATE_DIR, "defaultvals.js");
81
+ const localePath = path.join(appState.STATE_DIR, "locale.json");
82
+
83
+ if (argValues.edit) {
84
+ console.log(`${cfgPath}`);
85
+ openWith(cfgPath);
86
+ return 0;
87
+ }
88
+ if (argValues.remove) {
89
+ const rl = createInterface({
90
+ input: process.stdin,
91
+ output: process.stdout,
92
+ });
93
+ console.log(red(`${cfgPath}`));
94
+
95
+ const localeExists = fs.existsSync(localePath);
96
+ if (localeExists) {
97
+ console.log(red(`${localePath}`));
98
+ }
99
+
100
+ const answer = await rl.question(s.messages.deletionConfirm);
101
+ rl.close();
102
+ if (answer.trim().toLowerCase() === s.messages.yN) {
103
+ await fs.promises.unlink(cfgPath);
104
+ if (localeExists) {
105
+ await fs.promises.unlink(localePath);
106
+ }
107
+ console.log(yellowBright(s.messages.cfgDeletedSuccessfully));
108
+ return 0;
109
+ }
110
+ console.log(s.messages.deletionAborted);
111
+ return 0;
112
+ }
113
+ const lang = argValues.lang;
114
+ if (lang) {
115
+ if (isValidLocale(lang)) {
116
+ const localeData = { locale: argValues.lang };
117
+ try {
118
+ await fs.promises.writeFile(
119
+ localePath,
120
+ JSON.stringify(localeData, null, 2),
121
+ "utf8",
122
+ );
123
+ console.log(
124
+ s.messages.localeSuccessfullyChanged.replace(
125
+ "{{ .Locale }}",
126
+ `${argValues.lang}`,
127
+ ),
128
+ );
129
+ return 0;
130
+ } catch (err: unknown) {
131
+ if (isNodeError(err)) {
132
+ exitOne();
133
+ console.error(
134
+ red(
135
+ s.errors.failedToWriteLocale.replace(
136
+ "{{ .ErrorMessage }}",
137
+ `${err?.message}`,
138
+ ),
139
+ ),
140
+ );
141
+ return 1;
142
+ }
143
+ }
144
+ } else {
145
+ console.error(red(`Invalid locale: ${lang}`));
146
+ return 1;
147
+ }
148
+ }
149
+ return 0;
150
+ }
151
+ }
@@ -0,0 +1,43 @@
1
+ import { parseArgs } from "node:util";
2
+ import { strings as s } from "#i18n";
3
+ import { generateHelpText, createError } from "#meta";
4
+ import { GREETING } from "#defaultvals";
5
+ import type { Command } from "#types";
6
+
7
+ export default class HelloCommand implements Command {
8
+ // Static properties are used for metadata by help and completion generators
9
+ static allowPositionals = false;
10
+ static positionalCompletion = "none" as const;
11
+ static options = {
12
+ help: { type: "boolean", short: "h" },
13
+ name: { type: "string", short: "n" },
14
+ } as const;
15
+
16
+ async execute(argv: string[]): Promise<number> {
17
+ const { values: argValues } = parseArgs({
18
+ args: argv.slice(1), // slice(1) removes the cmd, useful for disabling positionals
19
+ allowPositionals: false,
20
+ strict: true,
21
+ options: (this.constructor as typeof HelloCommand).options,
22
+ });
23
+ const helloHelp = () => {
24
+ const helpText = generateHelpText(
25
+ s.help.commands.hello,
26
+ (this.constructor as typeof HelloCommand).options,
27
+ { Greeting: GREETING },
28
+ );
29
+ console.log(helpText);
30
+ };
31
+ if (argValues.help) {
32
+ helloHelp();
33
+ return 0;
34
+ }
35
+
36
+ /* if (!argValues.x) {
37
+ helloHelp();
38
+ throw createError(s.errors.xRequired, { code: "X_REQUIRED" });
39
+ }
40
+ */
41
+ return 0;
42
+ }
43
+ }
@@ -0,0 +1,21 @@
1
+ import { parseArgs } from "node:util";
2
+ import { strings as s } from "#i18n";
3
+ import { generateGenericHelpText } from "#meta";
4
+ import type { Command } from "#types";
5
+
6
+ export default class HelpCommand implements Command {
7
+ static allowPositionals = false;
8
+ static positionalCompletion = "none" as const;
9
+ static options = {};
10
+ async execute(argv: string[]): Promise<number> {
11
+ parseArgs({
12
+ args: argv.slice(1),
13
+ allowPositionals: false,
14
+ strict: true,
15
+ options: (this.constructor as typeof HelpCommand).options,
16
+ });
17
+ const helpText = generateGenericHelpText(s.help.generic);
18
+ console.log(helpText);
19
+ return 0;
20
+ }
21
+ }
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { strings as s } from "#i18n";
5
+ import { appState, createError } from "#meta";
6
+ import type * as Template from "#templatevals";
7
+
8
+ const USER_CONFIG_FILENAME = "defaultvals.js";
9
+ const USER_CONFIG_DIR = appState.STATE_DIR;
10
+ const USER_CONFIG_PATH = path.join(USER_CONFIG_DIR, USER_CONFIG_FILENAME);
11
+
12
+ let finalConfig: typeof Template;
13
+
14
+ try {
15
+ // If the user's config file doesn't exist, create it from the template.
16
+ if (!fs.existsSync(USER_CONFIG_PATH)) {
17
+ console.log(
18
+ s.messages.userConfigNotFound.replace(
19
+ "{{ .UserConfigPath }}",
20
+ USER_CONFIG_PATH,
21
+ ),
22
+ );
23
+ fs.mkdirSync(USER_CONFIG_DIR, { recursive: true });
24
+
25
+ const templateURL = await import.meta.resolve("#templatevals");
26
+ const templatePath = fileURLToPath(templateURL);
27
+ const templateContent = fs.readFileSync(templatePath, "utf8");
28
+
29
+ const userFileHeader = `/**
30
+ * This is your user-specific configuration file.
31
+ * It was automatically generated from the application's template.
32
+ * You can safely edit the values in this file to customize your experience.
33
+ * Your changes will be preserved across application updates.
34
+ * If you delete this file, a new one will be generated on the next launch.
35
+ */\n\n`;
36
+
37
+ fs.writeFileSync(
38
+ USER_CONFIG_PATH,
39
+ userFileHeader + templateContent,
40
+ "utf8",
41
+ );
42
+ console.log(s.messages.cfgCreatedSuccessfully);
43
+ }
44
+
45
+ // Import the user's configuration (or the newly created one).
46
+ finalConfig = (await import(
47
+ pathToFileURL(USER_CONFIG_PATH).href
48
+ )) as typeof Template;
49
+ } catch (err: unknown) {
50
+ throw createError(
51
+ s.errors.cfgCouldNotBeLoaded.replace(
52
+ "{{ .UserConfigPath }}",
53
+ USER_CONFIG_PATH,
54
+ ),
55
+ { cause: err, code: "CONFIG_LOAD_FAILED" },
56
+ );
57
+ }
58
+
59
+ // Export values from the user's configuration file.
60
+ export const { GREETING } = finalConfig;
@@ -0,0 +1,11 @@
1
+ // global.d.ts
2
+ export {};
3
+
4
+ declare global {
5
+ interface RegExpConstructor {
6
+ /**
7
+ * Escapes special characters in a string so it can be used literally in a RegExp.
8
+ */
9
+ escape(str: string): string;
10
+ }
11
+ }