preguito 0.1.0

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/index.cjs ADDED
@@ -0,0 +1,276 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_fs = require("node:fs");
4
+ let node_path = require("node:path");
5
+ let node_os = require("node:os");
6
+
7
+ //#region src/utils/errors.ts
8
+ var PrequitoError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "PrequitoError";
12
+ }
13
+ };
14
+ var TemplateMissingVariableError = class extends PrequitoError {
15
+ missingVariables;
16
+ constructor(missing) {
17
+ super(`Missing template variable(s): ${missing.join(", ")}. Provide shortcodes or check your config.`);
18
+ this.name = "TemplateMissingVariableError";
19
+ this.missingVariables = missing;
20
+ }
21
+ };
22
+ var TemplateMissingMessageError = class extends PrequitoError {
23
+ constructor(placeholderName) {
24
+ super(`Template requires a message (the <${placeholderName}> placeholder). Provide it as the last argument(s).`);
25
+ this.name = "TemplateMissingMessageError";
26
+ }
27
+ };
28
+
29
+ //#endregion
30
+ //#region src/template/engine.ts
31
+ const VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
32
+ const MESSAGE_PLACEHOLDER_PATTERN = /<(\w+)>/;
33
+ function parseTemplate(template) {
34
+ const variables = [];
35
+ let match;
36
+ const pattern = new RegExp(VARIABLE_PATTERN.source, "g");
37
+ while ((match = pattern.exec(template)) !== null) if (!variables.includes(match[1])) variables.push(match[1]);
38
+ const messageMatch = MESSAGE_PLACEHOLDER_PATTERN.exec(template);
39
+ return {
40
+ variables,
41
+ messagePlaceholder: messageMatch ? messageMatch[1] : null
42
+ };
43
+ }
44
+ function renderTemplate(template, context, message) {
45
+ const parsed = parseTemplate(template);
46
+ const missing = parsed.variables.filter((v) => !(v in context));
47
+ if (missing.length > 0) throw new TemplateMissingVariableError(missing);
48
+ let result = template.replace(/\{\{(\w+)\}\}/g, (_, varName) => {
49
+ return context[varName];
50
+ });
51
+ if (parsed.messagePlaceholder) {
52
+ if (!message) throw new TemplateMissingMessageError(parsed.messagePlaceholder);
53
+ result = result.replace(/<\w+>/, message);
54
+ }
55
+ return result;
56
+ }
57
+ function mergeContext(defaults, overrides) {
58
+ return {
59
+ ...defaults,
60
+ ...overrides
61
+ };
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/config/types.ts
66
+ const PREDEFINED_TYPES = [
67
+ {
68
+ key: "f",
69
+ label: "feat"
70
+ },
71
+ {
72
+ key: "x",
73
+ label: "fix"
74
+ },
75
+ {
76
+ key: "c",
77
+ label: "chore"
78
+ },
79
+ {
80
+ key: "t",
81
+ label: "test"
82
+ },
83
+ {
84
+ key: "l",
85
+ label: "lint"
86
+ },
87
+ {
88
+ key: "r",
89
+ label: "refactor"
90
+ },
91
+ {
92
+ key: "o",
93
+ label: "docs"
94
+ },
95
+ {
96
+ key: "y",
97
+ label: "style"
98
+ },
99
+ {
100
+ key: "e",
101
+ label: "perf"
102
+ },
103
+ {
104
+ key: "b",
105
+ label: "build"
106
+ }
107
+ ];
108
+ const PREDEFINED_ENVIRONMENTS = [
109
+ {
110
+ key: "p",
111
+ label: "prd"
112
+ },
113
+ {
114
+ key: "u",
115
+ label: "uat"
116
+ },
117
+ {
118
+ key: "h",
119
+ label: "homolog"
120
+ },
121
+ {
122
+ key: "d",
123
+ label: "dev"
124
+ },
125
+ {
126
+ key: "s",
127
+ label: "staging"
128
+ }
129
+ ];
130
+ function generateTemplate(features, hasPrefix) {
131
+ const { cardId, type, environment } = features;
132
+ return [
133
+ cardId ? hasPrefix ? "[{{prefix}}-{{card_id}}]" : "[{{card_id}}]" : "",
134
+ type && environment ? "{{type}}({{environment}}):" : type ? "{{type}}:" : environment ? "({{environment}}):" : "",
135
+ "<message>"
136
+ ].filter((p) => p !== "").join(" ");
137
+ }
138
+ const DEFAULT_CONFIG = {
139
+ template: "{{type}}: <message>",
140
+ features: {
141
+ cardId: false,
142
+ type: true,
143
+ environment: false
144
+ },
145
+ types: [
146
+ {
147
+ key: "f",
148
+ label: "feat"
149
+ },
150
+ {
151
+ key: "x",
152
+ label: "fix"
153
+ },
154
+ {
155
+ key: "c",
156
+ label: "chore"
157
+ },
158
+ {
159
+ key: "t",
160
+ label: "test"
161
+ },
162
+ {
163
+ key: "r",
164
+ label: "refactor"
165
+ },
166
+ {
167
+ key: "o",
168
+ label: "docs"
169
+ }
170
+ ],
171
+ environments: [],
172
+ defaults: {}
173
+ };
174
+ const CONFIG_PATHS = [
175
+ ".preguitorc",
176
+ ".preguitorc.json",
177
+ ".config/preguito/config.json"
178
+ ];
179
+
180
+ //#endregion
181
+ //#region src/config/loader.ts
182
+ async function findConfigPath() {
183
+ const cwd = process.cwd();
184
+ const home = (0, node_os.homedir)();
185
+ for (const relPath of [".preguitorc", ".preguitorc.json"]) {
186
+ const fullPath = (0, node_path.join)(cwd, relPath);
187
+ if ((0, node_fs.existsSync)(fullPath)) return fullPath;
188
+ }
189
+ for (const relPath of CONFIG_PATHS) {
190
+ const fullPath = (0, node_path.join)(home, relPath);
191
+ if ((0, node_fs.existsSync)(fullPath)) return fullPath;
192
+ }
193
+ return null;
194
+ }
195
+ async function loadConfig() {
196
+ const configPath = await findConfigPath();
197
+ if (!configPath) return null;
198
+ const raw = await (0, node_fs_promises.readFile)(configPath, "utf-8");
199
+ return validateConfig(JSON.parse(raw));
200
+ }
201
+ async function loadConfigOrDefault() {
202
+ return await loadConfig() ?? DEFAULT_CONFIG;
203
+ }
204
+ function validateConfig(obj) {
205
+ if (typeof obj !== "object" || obj === null) throw new Error("Config must be a JSON object");
206
+ const config = obj;
207
+ if (typeof config.template !== "string" || config.template.trim() === "") throw new Error("Config must have a non-empty \"template\" string field");
208
+ const defaults = {};
209
+ if (config.defaults !== void 0) {
210
+ if (typeof config.defaults !== "object" || config.defaults === null) throw new Error("\"defaults\" must be an object");
211
+ for (const [key, value] of Object.entries(config.defaults)) {
212
+ if (typeof value !== "string") throw new Error(`Default value for "${key}" must be a string`);
213
+ defaults[key] = value;
214
+ }
215
+ }
216
+ let features;
217
+ if (config.features && typeof config.features === "object") {
218
+ const f = config.features;
219
+ features = {
220
+ cardId: Boolean(f.cardId),
221
+ type: Boolean(f.type),
222
+ environment: Boolean(f.environment)
223
+ };
224
+ } else features = inferFeaturesFromTemplate(config.template);
225
+ let types = [];
226
+ if (Array.isArray(config.types)) types = validateShortcodeArray(config.types, "types");
227
+ else if (features.type) types = [...PREDEFINED_TYPES];
228
+ let environments = [];
229
+ if (Array.isArray(config.environments)) environments = validateShortcodeArray(config.environments, "environments");
230
+ else if (features.environment) environments = [...PREDEFINED_ENVIRONMENTS];
231
+ return {
232
+ template: config.template,
233
+ features,
234
+ types,
235
+ environments,
236
+ defaults
237
+ };
238
+ }
239
+ function inferFeaturesFromTemplate(template) {
240
+ const parsed = parseTemplate(template);
241
+ return {
242
+ cardId: parsed.variables.includes("card_id"),
243
+ type: parsed.variables.includes("type"),
244
+ environment: parsed.variables.includes("environment")
245
+ };
246
+ }
247
+ function validateShortcodeArray(arr, fieldName) {
248
+ const result = [];
249
+ for (let i = 0; i < arr.length; i++) {
250
+ const item = arr[i];
251
+ if (typeof item !== "object" || item === null || typeof item.key !== "string" || typeof item.label !== "string") throw new Error(`${fieldName}[${i}] must be an object with "key" and "label" string fields`);
252
+ const entry = item;
253
+ if (entry.key.length !== 1) throw new Error(`${fieldName}[${i}].key must be a single character, got "${entry.key}"`);
254
+ result.push({
255
+ key: entry.key,
256
+ label: entry.label
257
+ });
258
+ }
259
+ return result;
260
+ }
261
+ async function writeConfig(config) {
262
+ const configDir = (0, node_path.join)((0, node_os.homedir)(), ".config", "preguito");
263
+ const configPath = (0, node_path.join)(configDir, "config.json");
264
+ if (!(0, node_fs.existsSync)(configDir)) await (0, node_fs_promises.mkdir)(configDir, { recursive: true });
265
+ await (0, node_fs_promises.writeFile)(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
266
+ return configPath;
267
+ }
268
+
269
+ //#endregion
270
+ exports.generateTemplate = generateTemplate;
271
+ exports.loadConfig = loadConfig;
272
+ exports.loadConfigOrDefault = loadConfigOrDefault;
273
+ exports.mergeContext = mergeContext;
274
+ exports.parseTemplate = parseTemplate;
275
+ exports.renderTemplate = renderTemplate;
276
+ exports.writeConfig = writeConfig;
@@ -0,0 +1,37 @@
1
+ //#region src/template/engine.d.ts
2
+ interface TemplateContext {
3
+ [key: string]: string;
4
+ }
5
+ interface ParsedTemplate {
6
+ variables: string[];
7
+ messagePlaceholder: string | null;
8
+ }
9
+ declare function parseTemplate(template: string): ParsedTemplate;
10
+ declare function renderTemplate(template: string, context: TemplateContext, message?: string): string;
11
+ declare function mergeContext(defaults: TemplateContext, overrides: TemplateContext): TemplateContext;
12
+ //#endregion
13
+ //#region src/config/types.d.ts
14
+ interface ShortcodeEntry {
15
+ key: string;
16
+ label: string;
17
+ }
18
+ interface PrequitoFeatures {
19
+ cardId: boolean;
20
+ type: boolean;
21
+ environment: boolean;
22
+ }
23
+ interface PrequitoConfig {
24
+ template: string;
25
+ features: PrequitoFeatures;
26
+ types: ShortcodeEntry[];
27
+ environments: ShortcodeEntry[];
28
+ defaults: Record<string, string>;
29
+ }
30
+ declare function generateTemplate(features: PrequitoFeatures, hasPrefix: boolean): string;
31
+ //#endregion
32
+ //#region src/config/loader.d.ts
33
+ declare function loadConfig(): Promise<PrequitoConfig | null>;
34
+ declare function loadConfigOrDefault(): Promise<PrequitoConfig>;
35
+ declare function writeConfig(config: PrequitoConfig): Promise<string>;
36
+ //#endregion
37
+ export { type ParsedTemplate, type PrequitoConfig, type PrequitoFeatures, type ShortcodeEntry, type TemplateContext, generateTemplate, loadConfig, loadConfigOrDefault, mergeContext, parseTemplate, renderTemplate, writeConfig };
@@ -0,0 +1,37 @@
1
+ //#region src/template/engine.d.ts
2
+ interface TemplateContext {
3
+ [key: string]: string;
4
+ }
5
+ interface ParsedTemplate {
6
+ variables: string[];
7
+ messagePlaceholder: string | null;
8
+ }
9
+ declare function parseTemplate(template: string): ParsedTemplate;
10
+ declare function renderTemplate(template: string, context: TemplateContext, message?: string): string;
11
+ declare function mergeContext(defaults: TemplateContext, overrides: TemplateContext): TemplateContext;
12
+ //#endregion
13
+ //#region src/config/types.d.ts
14
+ interface ShortcodeEntry {
15
+ key: string;
16
+ label: string;
17
+ }
18
+ interface PrequitoFeatures {
19
+ cardId: boolean;
20
+ type: boolean;
21
+ environment: boolean;
22
+ }
23
+ interface PrequitoConfig {
24
+ template: string;
25
+ features: PrequitoFeatures;
26
+ types: ShortcodeEntry[];
27
+ environments: ShortcodeEntry[];
28
+ defaults: Record<string, string>;
29
+ }
30
+ declare function generateTemplate(features: PrequitoFeatures, hasPrefix: boolean): string;
31
+ //#endregion
32
+ //#region src/config/loader.d.ts
33
+ declare function loadConfig(): Promise<PrequitoConfig | null>;
34
+ declare function loadConfigOrDefault(): Promise<PrequitoConfig>;
35
+ declare function writeConfig(config: PrequitoConfig): Promise<string>;
36
+ //#endregion
37
+ export { type ParsedTemplate, type PrequitoConfig, type PrequitoFeatures, type ShortcodeEntry, type TemplateContext, generateTemplate, loadConfig, loadConfigOrDefault, mergeContext, parseTemplate, renderTemplate, writeConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,269 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ //#region src/utils/errors.ts
7
+ var PrequitoError = class extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "PrequitoError";
11
+ }
12
+ };
13
+ var TemplateMissingVariableError = class extends PrequitoError {
14
+ missingVariables;
15
+ constructor(missing) {
16
+ super(`Missing template variable(s): ${missing.join(", ")}. Provide shortcodes or check your config.`);
17
+ this.name = "TemplateMissingVariableError";
18
+ this.missingVariables = missing;
19
+ }
20
+ };
21
+ var TemplateMissingMessageError = class extends PrequitoError {
22
+ constructor(placeholderName) {
23
+ super(`Template requires a message (the <${placeholderName}> placeholder). Provide it as the last argument(s).`);
24
+ this.name = "TemplateMissingMessageError";
25
+ }
26
+ };
27
+
28
+ //#endregion
29
+ //#region src/template/engine.ts
30
+ const VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
31
+ const MESSAGE_PLACEHOLDER_PATTERN = /<(\w+)>/;
32
+ function parseTemplate(template) {
33
+ const variables = [];
34
+ let match;
35
+ const pattern = new RegExp(VARIABLE_PATTERN.source, "g");
36
+ while ((match = pattern.exec(template)) !== null) if (!variables.includes(match[1])) variables.push(match[1]);
37
+ const messageMatch = MESSAGE_PLACEHOLDER_PATTERN.exec(template);
38
+ return {
39
+ variables,
40
+ messagePlaceholder: messageMatch ? messageMatch[1] : null
41
+ };
42
+ }
43
+ function renderTemplate(template, context, message) {
44
+ const parsed = parseTemplate(template);
45
+ const missing = parsed.variables.filter((v) => !(v in context));
46
+ if (missing.length > 0) throw new TemplateMissingVariableError(missing);
47
+ let result = template.replace(/\{\{(\w+)\}\}/g, (_, varName) => {
48
+ return context[varName];
49
+ });
50
+ if (parsed.messagePlaceholder) {
51
+ if (!message) throw new TemplateMissingMessageError(parsed.messagePlaceholder);
52
+ result = result.replace(/<\w+>/, message);
53
+ }
54
+ return result;
55
+ }
56
+ function mergeContext(defaults, overrides) {
57
+ return {
58
+ ...defaults,
59
+ ...overrides
60
+ };
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/config/types.ts
65
+ const PREDEFINED_TYPES = [
66
+ {
67
+ key: "f",
68
+ label: "feat"
69
+ },
70
+ {
71
+ key: "x",
72
+ label: "fix"
73
+ },
74
+ {
75
+ key: "c",
76
+ label: "chore"
77
+ },
78
+ {
79
+ key: "t",
80
+ label: "test"
81
+ },
82
+ {
83
+ key: "l",
84
+ label: "lint"
85
+ },
86
+ {
87
+ key: "r",
88
+ label: "refactor"
89
+ },
90
+ {
91
+ key: "o",
92
+ label: "docs"
93
+ },
94
+ {
95
+ key: "y",
96
+ label: "style"
97
+ },
98
+ {
99
+ key: "e",
100
+ label: "perf"
101
+ },
102
+ {
103
+ key: "b",
104
+ label: "build"
105
+ }
106
+ ];
107
+ const PREDEFINED_ENVIRONMENTS = [
108
+ {
109
+ key: "p",
110
+ label: "prd"
111
+ },
112
+ {
113
+ key: "u",
114
+ label: "uat"
115
+ },
116
+ {
117
+ key: "h",
118
+ label: "homolog"
119
+ },
120
+ {
121
+ key: "d",
122
+ label: "dev"
123
+ },
124
+ {
125
+ key: "s",
126
+ label: "staging"
127
+ }
128
+ ];
129
+ function generateTemplate(features, hasPrefix) {
130
+ const { cardId, type, environment } = features;
131
+ return [
132
+ cardId ? hasPrefix ? "[{{prefix}}-{{card_id}}]" : "[{{card_id}}]" : "",
133
+ type && environment ? "{{type}}({{environment}}):" : type ? "{{type}}:" : environment ? "({{environment}}):" : "",
134
+ "<message>"
135
+ ].filter((p) => p !== "").join(" ");
136
+ }
137
+ const DEFAULT_CONFIG = {
138
+ template: "{{type}}: <message>",
139
+ features: {
140
+ cardId: false,
141
+ type: true,
142
+ environment: false
143
+ },
144
+ types: [
145
+ {
146
+ key: "f",
147
+ label: "feat"
148
+ },
149
+ {
150
+ key: "x",
151
+ label: "fix"
152
+ },
153
+ {
154
+ key: "c",
155
+ label: "chore"
156
+ },
157
+ {
158
+ key: "t",
159
+ label: "test"
160
+ },
161
+ {
162
+ key: "r",
163
+ label: "refactor"
164
+ },
165
+ {
166
+ key: "o",
167
+ label: "docs"
168
+ }
169
+ ],
170
+ environments: [],
171
+ defaults: {}
172
+ };
173
+ const CONFIG_PATHS = [
174
+ ".preguitorc",
175
+ ".preguitorc.json",
176
+ ".config/preguito/config.json"
177
+ ];
178
+
179
+ //#endregion
180
+ //#region src/config/loader.ts
181
+ async function findConfigPath() {
182
+ const cwd = process.cwd();
183
+ const home = homedir();
184
+ for (const relPath of [".preguitorc", ".preguitorc.json"]) {
185
+ const fullPath = join(cwd, relPath);
186
+ if (existsSync(fullPath)) return fullPath;
187
+ }
188
+ for (const relPath of CONFIG_PATHS) {
189
+ const fullPath = join(home, relPath);
190
+ if (existsSync(fullPath)) return fullPath;
191
+ }
192
+ return null;
193
+ }
194
+ async function loadConfig() {
195
+ const configPath = await findConfigPath();
196
+ if (!configPath) return null;
197
+ const raw = await readFile(configPath, "utf-8");
198
+ return validateConfig(JSON.parse(raw));
199
+ }
200
+ async function loadConfigOrDefault() {
201
+ return await loadConfig() ?? DEFAULT_CONFIG;
202
+ }
203
+ function validateConfig(obj) {
204
+ if (typeof obj !== "object" || obj === null) throw new Error("Config must be a JSON object");
205
+ const config = obj;
206
+ if (typeof config.template !== "string" || config.template.trim() === "") throw new Error("Config must have a non-empty \"template\" string field");
207
+ const defaults = {};
208
+ if (config.defaults !== void 0) {
209
+ if (typeof config.defaults !== "object" || config.defaults === null) throw new Error("\"defaults\" must be an object");
210
+ for (const [key, value] of Object.entries(config.defaults)) {
211
+ if (typeof value !== "string") throw new Error(`Default value for "${key}" must be a string`);
212
+ defaults[key] = value;
213
+ }
214
+ }
215
+ let features;
216
+ if (config.features && typeof config.features === "object") {
217
+ const f = config.features;
218
+ features = {
219
+ cardId: Boolean(f.cardId),
220
+ type: Boolean(f.type),
221
+ environment: Boolean(f.environment)
222
+ };
223
+ } else features = inferFeaturesFromTemplate(config.template);
224
+ let types = [];
225
+ if (Array.isArray(config.types)) types = validateShortcodeArray(config.types, "types");
226
+ else if (features.type) types = [...PREDEFINED_TYPES];
227
+ let environments = [];
228
+ if (Array.isArray(config.environments)) environments = validateShortcodeArray(config.environments, "environments");
229
+ else if (features.environment) environments = [...PREDEFINED_ENVIRONMENTS];
230
+ return {
231
+ template: config.template,
232
+ features,
233
+ types,
234
+ environments,
235
+ defaults
236
+ };
237
+ }
238
+ function inferFeaturesFromTemplate(template) {
239
+ const parsed = parseTemplate(template);
240
+ return {
241
+ cardId: parsed.variables.includes("card_id"),
242
+ type: parsed.variables.includes("type"),
243
+ environment: parsed.variables.includes("environment")
244
+ };
245
+ }
246
+ function validateShortcodeArray(arr, fieldName) {
247
+ const result = [];
248
+ for (let i = 0; i < arr.length; i++) {
249
+ const item = arr[i];
250
+ if (typeof item !== "object" || item === null || typeof item.key !== "string" || typeof item.label !== "string") throw new Error(`${fieldName}[${i}] must be an object with "key" and "label" string fields`);
251
+ const entry = item;
252
+ if (entry.key.length !== 1) throw new Error(`${fieldName}[${i}].key must be a single character, got "${entry.key}"`);
253
+ result.push({
254
+ key: entry.key,
255
+ label: entry.label
256
+ });
257
+ }
258
+ return result;
259
+ }
260
+ async function writeConfig(config) {
261
+ const configDir = join(homedir(), ".config", "preguito");
262
+ const configPath = join(configDir, "config.json");
263
+ if (!existsSync(configDir)) await mkdir(configDir, { recursive: true });
264
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
265
+ return configPath;
266
+ }
267
+
268
+ //#endregion
269
+ export { generateTemplate, loadConfig, loadConfigOrDefault, mergeContext, parseTemplate, renderTemplate, writeConfig };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "preguito",
3
+ "version": "0.1.0",
4
+ "description": "A lazy git CLI tool with commit templates and shortcuts",
5
+ "type": "module",
6
+ "bin": {
7
+ "guito": "./dist/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsdown",
14
+ "dev": "tsdown --watch",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "lint": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build",
19
+ "build:sea": "npm run build && node scripts/build-sea.js",
20
+ "build:deb": "npm run build:sea && bash scripts/build-deb.sh"
21
+ },
22
+ "keywords": [
23
+ "git",
24
+ "cli",
25
+ "commit",
26
+ "template",
27
+ "lazy"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "commander": "^13.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "tsdown": "^0.20.0",
35
+ "typescript": "^5.7.0",
36
+ "vitest": "^3.0.0",
37
+ "@types/node": "^22.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20.0.0"
41
+ }
42
+ }