@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,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;
|