@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,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,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
|
+
}
|