@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,138 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { appState, createError } from "#meta";
4
+ import type { LanguageStrings } from "#types";
5
+
6
+ function normalizeLocaleString(
7
+ localeString: string | undefined | null,
8
+ ): string | null {
9
+ if (!localeString || typeof localeString !== "string") return null;
10
+ let cleaned = localeString.split(".")[0];
11
+ cleaned = cleaned.replace(/_/g, "-");
12
+ return cleaned;
13
+ }
14
+
15
+ const localeMap: Record<string, string> = {
16
+ "en-US": "English (United States)",
17
+ }; // Add more here
18
+ const supportedLocales = Object.keys(localeMap);
19
+ const supportedLocaleSet = new Set(supportedLocales);
20
+
21
+ const SPACE = " ";
22
+ export function generateLocaleList(): string {
23
+ return Object.entries(localeMap)
24
+ .map(([code, name]) => `${SPACE}${code} — ${name}`)
25
+ .join("\n");
26
+ }
27
+
28
+ export function isValidLocale(locale: string): boolean {
29
+ return supportedLocaleSet.has(locale);
30
+ }
31
+
32
+ const languageToLocaleMap = new Map<string, string>();
33
+ function findBestSupportedLocale(
34
+ localeString: string | undefined | null,
35
+ ): string | null {
36
+ const normalized = normalizeLocaleString(localeString);
37
+ if (!normalized) {
38
+ return null;
39
+ }
40
+
41
+ try {
42
+ const parsedInput = new Intl.Locale(normalized);
43
+
44
+ if (parsedInput.region) {
45
+ const exactTag = `${parsedInput.language}-${parsedInput.region}`;
46
+ if (supportedLocaleSet.has(exactTag)) {
47
+ return exactTag;
48
+ }
49
+ }
50
+ const language = normalized.split("-")[0];
51
+ if (languageToLocaleMap.size === 0) {
52
+ for (const locale of supportedLocales) {
53
+ const langPart = locale.split("-")[0];
54
+ if (!languageToLocaleMap.has(langPart)) {
55
+ languageToLocaleMap.set(langPart, locale);
56
+ }
57
+ }
58
+ }
59
+ return languageToLocaleMap.get(language) || null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function getUserLocale(): string | null {
66
+ const localePath = path.join(appState.STATE_DIR, "locale.json");
67
+ try {
68
+ const data = fs.readFileSync(localePath, "utf-8");
69
+ const { locale } = JSON.parse(data) as { locale: string };
70
+
71
+ if (isValidLocale(locale)) {
72
+ return locale;
73
+ }
74
+ } catch {}
75
+ return null;
76
+ }
77
+
78
+ export function getLocale(): string {
79
+ const userSetLocale = getUserLocale();
80
+ if (userSetLocale) {
81
+ return userSetLocale;
82
+ }
83
+
84
+ const potentialLocaleSources: (string | undefined)[] = [
85
+ process.env.LC_ALL,
86
+ process.env.LC_MESSAGES,
87
+ process.env.LANG,
88
+ process.env.LANGUAGE,
89
+ Intl.DateTimeFormat().resolvedOptions().locale,
90
+ ];
91
+
92
+ const localeCandidates = potentialLocaleSources
93
+ .filter((s): s is string => !!s)
94
+ .flatMap((source) => source.split(/[:\s]+/));
95
+
96
+ const foundLocale = localeCandidates
97
+ .map(findBestSupportedLocale)
98
+ .find((l): l is string => !!l);
99
+
100
+ return foundLocale ?? "en-US";
101
+ }
102
+
103
+ async function loadLocaleFile(
104
+ locale: string,
105
+ ): Promise<Partial<LanguageStrings> | null> {
106
+ try {
107
+ const module = (await import(`#i18nj/${locale}`, {
108
+ with: { type: "json" },
109
+ })) as { default: Partial<LanguageStrings> };
110
+ return module.default;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ async function loadStrings(locale = "en-US"): Promise<LanguageStrings> {
117
+ const englishStrings = (await loadLocaleFile(
118
+ "en-US",
119
+ )) as LanguageStrings | null;
120
+ if (!englishStrings) {
121
+ throw createError(
122
+ "English language file (en-US.json) could not be loaded.",
123
+ { code: "LANGUAGE_LOAD_FAILED" },
124
+ );
125
+ }
126
+
127
+ if (locale === "en-US") {
128
+ return englishStrings;
129
+ }
130
+
131
+ const localeStrings = await loadLocaleFile(locale);
132
+ return localeStrings
133
+ ? ({ ...englishStrings, ...localeStrings } as LanguageStrings)
134
+ : englishStrings;
135
+ }
136
+
137
+ const currentLocale = getLocale();
138
+ export const strings = await loadStrings(currentLocale);
@@ -0,0 +1,399 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import type {
6
+ NumConstraints,
7
+ StrConstraints,
8
+ HelpSection,
9
+ GenericHelpSection,
10
+ CommandConstructor,
11
+ } from "#types";
12
+
13
+ function findProjectRoot(startDir: string): string {
14
+ let currentDir = startDir;
15
+ while (true) {
16
+ const packageJsonPath = path.join(currentDir, "package.json");
17
+ if (fs.existsSync(packageJsonPath)) return currentDir;
18
+
19
+ const parentDir = path.dirname(currentDir);
20
+ if (parentDir === currentDir) {
21
+ throw new Error("Could not find project root.");
22
+ }
23
+ currentDir = parentDir;
24
+ }
25
+ }
26
+
27
+ const FILENAME = fileURLToPath(import.meta.url);
28
+ const DIRNAME = path.dirname(FILENAME);
29
+ export const PROJECT_ROOT = findProjectRoot(DIRNAME);
30
+
31
+ const packageJsonPath = path.join(PROJECT_ROOT, "package.json");
32
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString());
33
+
34
+ export const FILES_TO_KEEP = ["defaultvals.js", "locale.json"];
35
+ export const P_NAME = packageJson.name;
36
+ export const P_VERSION = packageJson.version;
37
+ export const P_AUTHOR = packageJson.author;
38
+ export const P_CONTACT_URL = packageJson.url;
39
+
40
+ const home = homedir();
41
+ export function getStateDirPath() {
42
+ switch (process.platform) {
43
+ case "win32": {
44
+ const appDataPath = process.env.APPDATA;
45
+ if (appDataPath) {
46
+ return path.join(appDataPath, P_NAME);
47
+ }
48
+ return path.join(home, "AppData", "Roaming", P_NAME);
49
+ }
50
+ case "linux": {
51
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
52
+ if (xdgConfigHome) {
53
+ return path.join(xdgConfigHome, P_NAME);
54
+ }
55
+ return path.join(home, ".config", P_NAME);
56
+ }
57
+ case "darwin": {
58
+ return path.join(home, "Library", "Application Support", P_NAME);
59
+ }
60
+ default:
61
+ console.warn(
62
+ `Unsupported OS: ${process.platform}. The application may not behave appropriately regarding state storage.`,
63
+ );
64
+ return path.join(home, `.${P_NAME}`);
65
+ }
66
+ }
67
+
68
+ export const stateDirPath = getStateDirPath();
69
+
70
+ export const appState = {
71
+ P_NAME: P_NAME,
72
+ P_VERSION: P_VERSION,
73
+ HOME_DIR: homedir(),
74
+ STATE_DIR: stateDirPath,
75
+ DEBUG_MODE: false,
76
+ };
77
+ export const setDebug = (): boolean => (appState.DEBUG_MODE = true);
78
+
79
+ interface CreateErrorOptions {
80
+ cause?: unknown;
81
+ code?: string;
82
+ immediateExitCode?: boolean;
83
+ }
84
+
85
+ export interface NodeError extends Error {
86
+ code?: string;
87
+ }
88
+
89
+ export function isNodeError(error: unknown): error is NodeError {
90
+ return error instanceof Error;
91
+ }
92
+
93
+ export function isTypeError(error: unknown): error is TypeError {
94
+ return error instanceof TypeError;
95
+ }
96
+
97
+ export function isEnoentError(
98
+ error: unknown,
99
+ ): error is NodeError & { code: "ENOENT" } {
100
+ return isNodeError(error) && error.code === "ENOENT";
101
+ }
102
+
103
+ export function isEexistError(
104
+ error: unknown,
105
+ ): error is NodeError & { code: "EEXIST" } {
106
+ return isNodeError(error) && error.code === "EEXIST";
107
+ }
108
+
109
+ export function exitOne(): void {
110
+ if (process.env.NODE_ENV !== "test") {
111
+ process.exitCode = 1;
112
+ }
113
+ }
114
+
115
+ export function createError(
116
+ message: string,
117
+ options: CreateErrorOptions = {},
118
+ ): Error {
119
+ const { cause, code, immediateExitCode = true } = options;
120
+ const newError = new Error(message, { cause });
121
+
122
+ if (code) {
123
+ (newError as NodeError).code = code;
124
+ } else if (isNodeError(cause) && cause.code) {
125
+ (newError as NodeError).code = cause.code;
126
+ }
127
+
128
+ if (process.env.NODE_ENV !== "test" && immediateExitCode) {
129
+ process.exitCode = 1;
130
+ }
131
+
132
+ return newError;
133
+ }
134
+
135
+ export const V = {
136
+ num:
137
+ (
138
+ constraints: NumConstraints,
139
+ errorMsg: string,
140
+ errorCode: string,
141
+ errorPlaceholder = "{{ .Value }}",
142
+ secondaryCheck?: { fn: (v: number) => boolean },
143
+ secondaryErrorMsg?: string,
144
+ secondaryErrorCode?: string,
145
+ secondaryErrorPlaceholder?: string,
146
+ ) =>
147
+ (val: unknown) => {
148
+ if (
149
+ typeof val !== "number" ||
150
+ (!constraints.allowNaN && Number.isNaN(val))
151
+ ) {
152
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
153
+ code: errorCode,
154
+ });
155
+ }
156
+ if (constraints.integer && !Number.isInteger(val)) {
157
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
158
+ code: errorCode,
159
+ });
160
+ }
161
+ if (constraints.isFloat && Number.isInteger(val)) {
162
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
163
+ code: errorCode,
164
+ });
165
+ }
166
+ if (constraints.min !== undefined && val < constraints.min) {
167
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
168
+ code: errorCode,
169
+ });
170
+ }
171
+ if (constraints.max !== undefined && val > constraints.max) {
172
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
173
+ code: errorCode,
174
+ });
175
+ }
176
+ if (
177
+ constraints.minExclusive !== undefined &&
178
+ val <= constraints.minExclusive
179
+ ) {
180
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
181
+ code: errorCode,
182
+ });
183
+ }
184
+ if (
185
+ constraints.maxExclusive !== undefined &&
186
+ val >= constraints.maxExclusive
187
+ ) {
188
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
189
+ code: errorCode,
190
+ });
191
+ }
192
+ if (
193
+ secondaryCheck &&
194
+ secondaryErrorMsg &&
195
+ secondaryErrorCode &&
196
+ secondaryErrorPlaceholder &&
197
+ !secondaryCheck.fn(val)
198
+ ) {
199
+ throw createError(
200
+ secondaryErrorMsg.replace(secondaryErrorPlaceholder, String(val)),
201
+ { code: secondaryErrorCode },
202
+ );
203
+ }
204
+ },
205
+
206
+ str:
207
+ (
208
+ constraints: StrConstraints,
209
+ errorMsg: string,
210
+ errorCode: string,
211
+ errorPlaceholder = "{{ .Value }}",
212
+ secondaryCheck?: { fn: (v: string) => boolean },
213
+ secondaryErrorMsg?: string,
214
+ secondaryErrorCode?: string,
215
+ secondaryErrorPlaceholder?: string,
216
+ ) =>
217
+ (val: unknown) => {
218
+ if (typeof val !== "string") {
219
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
220
+ code: errorCode,
221
+ });
222
+ }
223
+ if (constraints.notEmpty && val.trim() === "") {
224
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
225
+ code: errorCode,
226
+ });
227
+ }
228
+ if (
229
+ secondaryCheck &&
230
+ secondaryErrorMsg &&
231
+ secondaryErrorCode &&
232
+ secondaryErrorPlaceholder &&
233
+ !secondaryCheck.fn(val)
234
+ ) {
235
+ throw createError(
236
+ secondaryErrorMsg.replace(secondaryErrorPlaceholder, String(val)),
237
+ { code: secondaryErrorCode },
238
+ );
239
+ }
240
+ },
241
+ bool:
242
+ (
243
+ constraints: { strictTrueFalse?: boolean } = {},
244
+ errorMsg: string,
245
+ errorCode: string,
246
+ errorPlaceholder = "{{ .Value }}",
247
+ ) =>
248
+ (val: unknown) => {
249
+ const isBoolean = typeof val === "boolean";
250
+ const isStrict = constraints.strictTrueFalse;
251
+
252
+ if (isStrict && !isBoolean) {
253
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
254
+ code: errorCode,
255
+ });
256
+ }
257
+
258
+ if (!isStrict && typeof val !== "boolean" && val !== 0 && val !== 1) {
259
+ throw createError(errorMsg.replace(errorPlaceholder, String(val)), {
260
+ code: errorCode,
261
+ });
262
+ }
263
+ },
264
+ getValueFromArray:
265
+ <T>(
266
+ errorMsg: string,
267
+ errorCode: string,
268
+ errorPlaceholder = "{{ .optionValue }}",
269
+ ) =>
270
+ (optionValue: unknown): T => {
271
+ if (!Array.isArray(optionValue) || optionValue.length < 2) {
272
+ throw createError(
273
+ errorMsg.replace(errorPlaceholder, String(optionValue)),
274
+ { code: errorCode },
275
+ );
276
+ }
277
+ return optionValue[1] as T;
278
+ },
279
+ getDelaySeconds: (seconds: unknown): number => {
280
+ if (typeof seconds !== "number" && typeof seconds !== "string") {
281
+ throw new TypeError("Delay must be a number or string");
282
+ }
283
+ return Math.round(parseFloat(String(seconds)) * 1000);
284
+ },
285
+ };
286
+
287
+ function simpleTemplate(str: string, data: Record<string, string>): string {
288
+ if (!data) return str;
289
+ return str.replace(/\{\{\s*\.(.*?)\s*\}\}/g, (_, key) => data[key] ?? "");
290
+ }
291
+
292
+ export function generateHelpText(
293
+ helpSection: HelpSection,
294
+ optionsConfig: CommandConstructor["options"],
295
+ replacements: Record<string, string> = {},
296
+ ): string {
297
+ const lines: string[] = [];
298
+
299
+ lines.push(helpSection.usage);
300
+ lines.push(`\n${helpSection.description}`);
301
+
302
+ if (helpSection.flags && optionsConfig) {
303
+ lines.push("\nOptions:");
304
+
305
+ const flagLines = Object.entries(optionsConfig).map(
306
+ ([longName, config]) => {
307
+ const parts: string[] = [];
308
+ if (config.short) {
309
+ parts.push(`-${config.short},`);
310
+ }
311
+ parts.push(`--${longName}`);
312
+ if (config.type === "string") {
313
+ parts.push("<value>");
314
+ }
315
+
316
+ const flagPart = ` ${parts.join(" ")}`;
317
+ const description = helpSection.flags?.[longName] ?? "";
318
+
319
+ return { flagPart, description };
320
+ },
321
+ );
322
+
323
+ const longestFlagPart = Math.max(
324
+ ...flagLines.map((f) => f.flagPart.length),
325
+ );
326
+ const padding = 2;
327
+
328
+ flagLines.forEach(({ flagPart, description }) => {
329
+ const requiredPadding = longestFlagPart - flagPart.length + padding;
330
+ lines.push(`${flagPart}${" ".repeat(requiredPadding)}${description}`);
331
+ });
332
+ }
333
+
334
+ if (helpSection.footer) {
335
+ lines.push(`\n${helpSection.footer}`);
336
+ }
337
+
338
+ const rawText = lines.join("\n");
339
+ return simpleTemplate(rawText, replacements);
340
+ }
341
+
342
+ export function generateGenericHelpText(
343
+ helpSection: GenericHelpSection,
344
+ ): string {
345
+ const lines: string[] = [];
346
+
347
+ const commandParts = Object.keys(helpSection.commandDescriptions).map(
348
+ (cmd) => ` ${cmd}`,
349
+ );
350
+ const flagParts = Object.keys(helpSection.flags).map((flag) => ` --${flag}`);
351
+ const allLeftParts = [...commandParts, ...flagParts];
352
+
353
+ const longestPart = Math.max(...allLeftParts.map((p) => p.length));
354
+ const padding = 4;
355
+
356
+ lines.push(helpSection.header);
357
+ lines.push(`\n${helpSection.usage}`);
358
+
359
+ lines.push(`\n${helpSection.commandHeader}`);
360
+ Object.entries(helpSection.commandDescriptions).forEach(([cmd, desc]) => {
361
+ const cmdPart = ` ${cmd}`;
362
+ const requiredPadding = longestPart - cmdPart.length + padding;
363
+ lines.push(`${cmdPart}${" ".repeat(requiredPadding)}${desc}`);
364
+ });
365
+
366
+ lines.push(`\n${helpSection.footer}`);
367
+
368
+ lines.push(`\n${helpSection.globalOptionsHeader}`);
369
+ Object.entries(helpSection.flags).forEach(([flag, desc]) => {
370
+ const flagPart = ` --${flag}`;
371
+ const requiredPadding = longestPart - flagPart.length + padding;
372
+ lines.push(`${flagPart}${" ".repeat(requiredPadding)}${desc}`);
373
+ });
374
+
375
+ return lines.join("\n");
376
+ }
377
+
378
+ const isInteractive = process.stdout.isTTY;
379
+ const noop = (text: string): string => text;
380
+ export const red = isInteractive
381
+ ? (text: string) => `\x1b[31m${text}\x1b[0m`
382
+ : noop;
383
+ export const yellow = isInteractive
384
+ ? (text: string) => `\x1b[33m${text}\x1b[0m`
385
+ : noop;
386
+ export const yellowBright = isInteractive
387
+ ? (text: string) => `\x1b[93m${text}\x1b[0m`
388
+ : noop;
389
+ export const blue = isInteractive
390
+ ? (text: string) => `\x1b[34m${text}\x1b[0m`
391
+ : noop;
392
+
393
+ // Maps command aliases to their module filenames in src/commands/
394
+ export const commandConfig: Record<string, string> = {
395
+ help: "helpcommand",
396
+ cfg: "configcommand",
397
+ co: "cocommand",
398
+ hello: "hellocommand",
399
+ };
@@ -0,0 +1,59 @@
1
+ import type { parseArgs } from "node:util";
2
+ // Import the JSON type definition for the default language (en-US)
3
+ import type enUS from "#i18nj/en-US";
4
+
5
+ // Base interface for all command classes
6
+ export interface Command {
7
+ execute(argv: string[]): Promise<number | void>;
8
+ }
9
+
10
+ // Type for dynamically imported command modules
11
+ export type CommandModule = {
12
+ default: new () => Command;
13
+ };
14
+
15
+ // Defines the bash completion type for positional arguments (for the `co` command)
16
+ export type PositionalCompletion = "file" | "directory" | "none";
17
+
18
+ // Describes the static properties required on a command class constructor
19
+ export interface CommandConstructor {
20
+ new (): Command;
21
+ // Configuration for node:util.parseArgs
22
+ options: NonNullable<Parameters<typeof parseArgs>[0]>["options"];
23
+ allowPositionals?: boolean;
24
+ positionalCompletion?: PositionalCompletion;
25
+ }
26
+
27
+ // Constraints for numeric and string arguments
28
+ export type NumConstraints = {
29
+ min?: number;
30
+ max?: number;
31
+ minExclusive?: number;
32
+ maxExclusive?: number;
33
+ integer?: boolean;
34
+ isFloat?: boolean;
35
+ allowNaN?: boolean;
36
+ };
37
+ export type StrConstraints = { notEmpty?: boolean };
38
+
39
+ // Structure for help text sections used by generateHelpText
40
+ export interface HelpSection {
41
+ usage: string;
42
+ description: string;
43
+ flags?: Record<string, string>;
44
+ footer?: string;
45
+ }
46
+
47
+ // Structure for the main `help` command's text
48
+ export interface GenericHelpSection {
49
+ header: string;
50
+ usage: string;
51
+ commandHeader: string;
52
+ commandDescriptions: Record<string, string>;
53
+ footer: string;
54
+ globalOptionsHeader: string;
55
+ flags: Record<string, string>;
56
+ }
57
+
58
+ // Type definition for the i18n JSON file structure
59
+ export type LanguageStrings = typeof enUS;
@@ -0,0 +1,2 @@
1
+ // An example of a customizable string value.
2
+ export const GREETING = "Hello";
@@ -0,0 +1,19 @@
1
+ // when in doubt, just throw the misc stuff here
2
+ import assert from "node:assert/strict";
3
+ import { suite, test } from "node:test";
4
+ import * as utils from "#testutils";
5
+
6
+ suite("basic sanity checks", () => {
7
+ test("Command classes", async (t) => {
8
+ await t.test("HelpCommand shows generic help", async () => {
9
+ process.env.LC_ALL = "en_US.UTF-8";
10
+ process.env.LANG = "en_US.UTF-8";
11
+ const { default: HelpCommand } = await import("#commands/helpcommand");
12
+ const output = await utils.withCapturedConsole(async () => {
13
+ const command = new HelpCommand();
14
+ await command.execute(["help"]);
15
+ });
16
+ assert.match(output, /A tool for/);
17
+ });
18
+ });
19
+ });
@@ -0,0 +1,13 @@
1
+ export async function withCapturedConsole(fn) {
2
+ const originalLog = console.log;
3
+ let output = "";
4
+ console.log = (...args) => {
5
+ output += args.join(" ") + "\n";
6
+ };
7
+ try {
8
+ await fn();
9
+ } finally {
10
+ console.log = originalLog;
11
+ }
12
+ return output;
13
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "baseUrl": "./src",
7
+ "paths": {
8
+ "#app": ["./bitecli.ts"],
9
+ "#defaultvals": ["./defaultvals.ts"],
10
+ "#templatevals": ["./templatevals.ts"],
11
+ "#meta": ["./libs/meta.ts"],
12
+ "#i18n": ["./libs/i18n.ts"],
13
+ "#commands/*": ["./commands/*.ts"],
14
+ "#i18nj/*": ["../data/i18n/*.json"],
15
+ "#types": ["./libs/types/types.ts"]
16
+ },
17
+ "resolveJsonModule": true,
18
+ "esModuleInterop": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "strict": true,
21
+ "skipLibCheck": true,
22
+ "rootDir": "./src",
23
+ "outDir": "./dist",
24
+ "declaration": false,
25
+ "sourceMap": true
26
+ },
27
+ "include": ["src/**/*"],
28
+ "exclude": ["node_modules", "tests"]
29
+ }