ag-toolkit 0.5.0 → 0.6.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.
@@ -1,404 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { ArgParser, defineSchema } from "../lib/arg-parser.js";
3
- describe("ArgParser", () => {
4
- const schema = defineSchema({
5
- verbose: { type: "boolean", shortFlag: "v" },
6
- output: { type: "string", shortFlag: "o" },
7
- force: { type: "boolean" },
8
- configFile: { type: "string" },
9
- });
10
- describe("help and version flags", () => {
11
- test("should return help message when -h is passed", () => {
12
- const parser = new ArgParser({
13
- schema,
14
- helpMessage: "Test help message",
15
- version: "1.0.0",
16
- });
17
- const result = parser.parse(["-h"]);
18
- expect(result.help).toBe("Test help message");
19
- });
20
- test("should return help message when --help is passed", () => {
21
- const parser = new ArgParser({
22
- schema,
23
- helpMessage: "Test help message",
24
- version: "1.0.0",
25
- });
26
- const result = parser.parse(["--help"]);
27
- expect(result.help).toBe("Test help message");
28
- });
29
- test("should return version when -v is passed (not short flag conflict)", () => {
30
- const schemaWithoutVerbose = {
31
- output: { type: "string", shortFlag: "o" },
32
- };
33
- const parser = new ArgParser({
34
- schema: schemaWithoutVerbose,
35
- helpMessage: "Test help",
36
- version: "1.0.0",
37
- });
38
- const result = parser.parse(["-v"]);
39
- expect(result.version).toBe("1.0.0");
40
- });
41
- test("should return version when --version is passed", () => {
42
- const parser = new ArgParser({
43
- schema,
44
- helpMessage: "Test help",
45
- version: "1.0.0",
46
- });
47
- const result = parser.parse(["--version"]);
48
- expect(result.version).toBe("1.0.0");
49
- });
50
- });
51
- describe("boolean flags", () => {
52
- test("should parse long boolean flags", () => {
53
- const parser = new ArgParser({
54
- schema,
55
- helpMessage: "",
56
- version: "",
57
- });
58
- const result = parser.parse(["--verbose", "--force"]);
59
- expect(result.flags.verbose).toBe(true);
60
- expect(result.flags.force).toBe(true);
61
- });
62
- test("should parse short boolean flags", () => {
63
- const schemaWithDifferentShortFlag = {
64
- ...schema,
65
- verbose: { type: "boolean", shortFlag: "V" },
66
- };
67
- const parser = new ArgParser({
68
- schema: schemaWithDifferentShortFlag,
69
- helpMessage: "",
70
- version: "",
71
- });
72
- const result = parser.parse(["-V"]);
73
- expect(result.flags.verbose).toBe(true);
74
- });
75
- test("should handle camelCase to kebab-case conversion", () => {
76
- const parser = new ArgParser({
77
- schema: { myLongFlag: { type: "boolean" } },
78
- helpMessage: "",
79
- version: "",
80
- });
81
- const result = parser.parse(["--my-long-flag"]);
82
- expect(result.flags.myLongFlag).toBe(true);
83
- });
84
- test("should leave undefined flags as undefined", () => {
85
- const parser = new ArgParser({
86
- schema,
87
- helpMessage: "",
88
- version: "",
89
- });
90
- const result = parser.parse([]);
91
- expect(result.flags.verbose).toBeUndefined();
92
- expect(result.flags.force).toBeUndefined();
93
- });
94
- });
95
- describe("enum flags", () => {
96
- const schemaWithEnum = defineSchema({
97
- logLevel: {
98
- type: "enum",
99
- enumValues: ["debug", "info", "warn", "error"],
100
- shortFlag: "l",
101
- },
102
- environment: {
103
- type: "enum",
104
- enumValues: ["development", "staging", "production"],
105
- },
106
- });
107
- test("should parse long enum flags with space", () => {
108
- const parser = new ArgParser({
109
- schema: schemaWithEnum,
110
- helpMessage: "",
111
- version: "",
112
- });
113
- const result = parser.parse(["--log-level", "info"]);
114
- expect(result.flags.logLevel).toBe("info");
115
- });
116
- test("should parse long enum flags with equals", () => {
117
- const parser = new ArgParser({
118
- schema: schemaWithEnum,
119
- helpMessage: "",
120
- version: "",
121
- });
122
- const result = parser.parse(["--log-level=error"]);
123
- expect(result.flags.logLevel).toBe("error");
124
- });
125
- test("should parse short enum flags", () => {
126
- const parser = new ArgParser({
127
- schema: schemaWithEnum,
128
- helpMessage: "",
129
- version: "",
130
- });
131
- const result = parser.parse(["-l", "debug"]);
132
- expect(result.flags.logLevel).toBe("debug");
133
- });
134
- test("should parse short enum flags with equals", () => {
135
- const parser = new ArgParser({
136
- schema: schemaWithEnum,
137
- helpMessage: "",
138
- version: "",
139
- });
140
- const result = parser.parse(["-l=warn"]);
141
- expect(result.flags.logLevel).toBe("warn");
142
- });
143
- test("should handle camelCase to kebab-case for enum flags", () => {
144
- const parser = new ArgParser({
145
- schema: schemaWithEnum,
146
- helpMessage: "",
147
- version: "",
148
- });
149
- const result = parser.parse(["--log-level", "info"]);
150
- expect(result.flags.logLevel).toBe("info");
151
- });
152
- test("should throw error when enum flag has invalid value", () => {
153
- const parser = new ArgParser({
154
- schema: schemaWithEnum,
155
- helpMessage: "",
156
- version: "",
157
- });
158
- expect(() => parser.parse(["--log-level", "invalid"])).toThrow("Flag --logLevel must be one of: debug, info, warn, error");
159
- });
160
- test("should throw error when enum flag has no value", () => {
161
- const parser = new ArgParser({
162
- schema: schemaWithEnum,
163
- helpMessage: "",
164
- version: "",
165
- });
166
- expect(() => parser.parse(["--log-level"])).toThrow("Flag --logLevel requires a value");
167
- });
168
- test("should parse multiple enum flags", () => {
169
- const parser = new ArgParser({
170
- schema: schemaWithEnum,
171
- helpMessage: "",
172
- version: "",
173
- });
174
- const result = parser.parse([
175
- "--log-level",
176
- "debug",
177
- "--environment",
178
- "production",
179
- ]);
180
- expect(result.flags.logLevel).toBe("debug");
181
- expect(result.flags.environment).toBe("production");
182
- });
183
- test("should leave undefined enum flags as undefined", () => {
184
- const parser = new ArgParser({
185
- schema: schemaWithEnum,
186
- helpMessage: "",
187
- version: "",
188
- });
189
- const result = parser.parse([]);
190
- expect(result.flags.logLevel).toBeUndefined();
191
- expect(result.flags.environment).toBeUndefined();
192
- });
193
- test("should handle multiple occurrences of same enum flag (last wins)", () => {
194
- const parser = new ArgParser({
195
- schema: schemaWithEnum,
196
- helpMessage: "",
197
- version: "",
198
- });
199
- const result = parser.parse([
200
- "--log-level",
201
- "debug",
202
- "--log-level",
203
- "error",
204
- ]);
205
- expect(result.flags.logLevel).toBe("error");
206
- });
207
- });
208
- describe("string flags", () => {
209
- test("should parse long string flags with space", () => {
210
- const parser = new ArgParser({
211
- schema,
212
- helpMessage: "",
213
- version: "",
214
- });
215
- const result = parser.parse(["--output", "file.txt"]);
216
- expect(result.flags.output).toBe("file.txt");
217
- });
218
- test("should parse long string flags with equals", () => {
219
- const parser = new ArgParser({
220
- schema,
221
- helpMessage: "",
222
- version: "",
223
- });
224
- const result = parser.parse(["--output=file.txt"]);
225
- expect(result.flags.output).toBe("file.txt");
226
- });
227
- test("should parse short string flags", () => {
228
- const parser = new ArgParser({
229
- schema,
230
- helpMessage: "",
231
- version: "",
232
- });
233
- const result = parser.parse(["-o", "file.txt"]);
234
- expect(result.flags.output).toBe("file.txt");
235
- });
236
- test("should parse short string flags with equals", () => {
237
- const parser = new ArgParser({
238
- schema,
239
- helpMessage: "",
240
- version: "",
241
- });
242
- const result = parser.parse(["-o=file.txt"]);
243
- expect(result.flags.output).toBe("file.txt");
244
- });
245
- test("should handle camelCase to kebab-case for string flags", () => {
246
- const parser = new ArgParser({
247
- schema,
248
- helpMessage: "",
249
- version: "",
250
- });
251
- const result = parser.parse(["--config-file", "config.json"]);
252
- expect(result.flags.configFile).toBe("config.json");
253
- });
254
- test("should throw error when string flag has no value", () => {
255
- const parser = new ArgParser({
256
- schema,
257
- helpMessage: "",
258
- version: "",
259
- });
260
- expect(() => parser.parse(["--output"])).toThrow();
261
- });
262
- });
263
- describe("input arguments", () => {
264
- test("should collect non-flag arguments as input", () => {
265
- const parser = new ArgParser({
266
- schema,
267
- helpMessage: "",
268
- version: "",
269
- });
270
- const result = parser.parse(["arg1", "arg2", "arg3"]);
271
- expect(result.input).toEqual(["arg1", "arg2", "arg3"]);
272
- });
273
- test("should collect input after flags", () => {
274
- const parser = new ArgParser({
275
- schema,
276
- helpMessage: "",
277
- version: "",
278
- });
279
- const result = parser.parse(["--verbose", "arg1", "arg2"]);
280
- expect(result.flags.verbose).toBe(true);
281
- expect(result.input).toEqual(["arg1", "arg2"]);
282
- });
283
- test("should handle -- separator", () => {
284
- const parser = new ArgParser({
285
- schema,
286
- helpMessage: "",
287
- version: "",
288
- });
289
- const result = parser.parse(["--verbose", "--", "--output", "file.txt"]);
290
- expect(result.flags.verbose).toBe(true);
291
- expect(result.flags.output).toBeUndefined();
292
- expect(result.input).toEqual(["--output", "file.txt"]);
293
- });
294
- test("should collect everything after -- as input", () => {
295
- const schemaWithoutConflicts = defineSchema({
296
- force: { type: "boolean" },
297
- });
298
- const parser = new ArgParser({
299
- schema: schemaWithoutConflicts,
300
- helpMessage: "",
301
- version: "",
302
- });
303
- const result = parser.parse(["--", "--force"]);
304
- expect(result.input).toEqual(["--force"]);
305
- });
306
- });
307
- describe("mixed arguments", () => {
308
- test("should parse mixed flags and input", () => {
309
- const parser = new ArgParser({
310
- schema,
311
- helpMessage: "",
312
- version: "",
313
- });
314
- const result = parser.parse([
315
- "--verbose",
316
- "--output",
317
- "file.txt",
318
- "input1",
319
- "--force",
320
- "input2",
321
- ]);
322
- expect(result.flags.verbose).toBe(true);
323
- expect(result.flags.output).toBe("file.txt");
324
- expect(result.flags.force).toBe(true);
325
- expect(result.input).toEqual(["input1", "input2"]);
326
- });
327
- test("should handle multiple flags with equals syntax", () => {
328
- const parser = new ArgParser({
329
- schema,
330
- helpMessage: "",
331
- version: "",
332
- });
333
- const result = parser.parse([
334
- "--verbose",
335
- "--output=out.txt",
336
- "--config-file=config.json",
337
- ]);
338
- expect(result.flags.verbose).toBe(true);
339
- expect(result.flags.output).toBe("out.txt");
340
- expect(result.flags.configFile).toBe("config.json");
341
- });
342
- });
343
- describe("error handling", () => {
344
- test("should throw error for unknown long flags", () => {
345
- const parser = new ArgParser({
346
- schema,
347
- helpMessage: "",
348
- version: "",
349
- });
350
- expect(() => parser.parse(["--unknown"])).toThrow("Unknown option: --unknown");
351
- });
352
- test("should throw error for unknown short flags", () => {
353
- const parser = new ArgParser({
354
- schema,
355
- helpMessage: "",
356
- version: "",
357
- });
358
- expect(() => parser.parse(["-x"])).toThrow("Unknown option: -x");
359
- });
360
- test("should throw error when string flag requires value", () => {
361
- const parser = new ArgParser({
362
- schema,
363
- helpMessage: "",
364
- version: "",
365
- });
366
- expect(() => parser.parse(["--output"])).toThrow("Flag --output requires a value");
367
- });
368
- });
369
- describe("edge cases", () => {
370
- test("should handle empty args array", () => {
371
- const parser = new ArgParser({
372
- schema,
373
- helpMessage: "",
374
- version: "",
375
- });
376
- const result = parser.parse([]);
377
- expect(result.input).toEqual([]);
378
- expect(result.flags.verbose).toBeUndefined();
379
- });
380
- test("should handle flags with empty string values", () => {
381
- const parser = new ArgParser({
382
- schema,
383
- helpMessage: "",
384
- version: "",
385
- });
386
- const result = parser.parse(["--output="]);
387
- expect(result.flags.output).toBe("");
388
- });
389
- test("should handle multiple occurrences of same flag (last wins)", () => {
390
- const parser = new ArgParser({
391
- schema,
392
- helpMessage: "",
393
- version: "",
394
- });
395
- const result = parser.parse([
396
- "--output",
397
- "first.txt",
398
- "--output",
399
- "second.txt",
400
- ]);
401
- expect(result.flags.output).toBe("second.txt");
402
- });
403
- });
404
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,234 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { promises as fs } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { findLocalConfigPath, findUp, getGlobalConfigPath, loadConfig, loadLocalConfig, readConfigFile, resetGlobalConfig, writeConfig, writeGlobalConfig, writeLocalConfig, } from "../lib/config.js";
6
- describe("config", () => {
7
- let testDir;
8
- beforeEach(async () => {
9
- testDir = await fs.mkdtemp(join(tmpdir(), "config-test-"));
10
- });
11
- afterEach(async () => {
12
- await fs.rm(testDir, { recursive: true, force: true });
13
- });
14
- describe("findUp", () => {
15
- test("should find file in current directory", async () => {
16
- const testFile = "test-config.json";
17
- await fs.writeFile(join(testDir, testFile), "{}");
18
- const result = await findUp(testFile, testDir);
19
- expect(result).toBe(join(testDir, testFile));
20
- });
21
- test("should find file in parent directory", async () => {
22
- const testFile = "test-config.json";
23
- const subDir = join(testDir, "subdir");
24
- await fs.mkdir(subDir);
25
- await fs.writeFile(join(testDir, testFile), "{}");
26
- const result = await findUp(testFile, subDir);
27
- expect(result).toBe(join(testDir, testFile));
28
- });
29
- test("should return null when file is not found", async () => {
30
- const subDir = join(process.cwd(), "src", "tests");
31
- const result = await findUp("nonexistent-file-that-should-not-exist.json", subDir);
32
- expect(result).toBeNull();
33
- });
34
- test("should find file in nested parent directories", async () => {
35
- const testFile = "test-config.json";
36
- const deepDir = join(testDir, "a", "b", "c");
37
- await fs.mkdir(deepDir, { recursive: true });
38
- await fs.writeFile(join(testDir, testFile), "{}");
39
- const result = await findUp(testFile, deepDir);
40
- expect(result).toBe(join(testDir, testFile));
41
- });
42
- });
43
- describe("readConfigFile", () => {
44
- test("should read and parse valid JSON config file", async () => {
45
- const configPath = join(testDir, "config.json");
46
- const config = { key: "value", number: 42 };
47
- await fs.writeFile(configPath, JSON.stringify(config));
48
- const validate = (c) => c;
49
- const result = await readConfigFile(configPath, validate);
50
- expect(result).toEqual(config);
51
- });
52
- test("should return null for non-existent file", async () => {
53
- const validate = (c) => c;
54
- const result = await readConfigFile(join(testDir, "nonexistent.json"), validate);
55
- expect(result).toBeNull();
56
- });
57
- test("should throw error for invalid JSON", async () => {
58
- const configPath = join(testDir, "invalid.json");
59
- await fs.writeFile(configPath, "{ invalid json }");
60
- const validate = (c) => c;
61
- await expect(readConfigFile(configPath, validate)).rejects.toThrow();
62
- });
63
- });
64
- describe("writeConfig", () => {
65
- test("should write config to file", async () => {
66
- const configPath = join(testDir, "config.json");
67
- const config = { key: "value", nested: { prop: 123 } };
68
- await writeConfig(configPath, config);
69
- const content = await fs.readFile(configPath, "utf-8");
70
- expect(JSON.parse(content)).toEqual(config);
71
- });
72
- test("should create parent directories if they don't exist", async () => {
73
- const configPath = join(testDir, "nested", "deep", "config.json");
74
- const config = { key: "value" };
75
- await writeConfig(configPath, config);
76
- const content = await fs.readFile(configPath, "utf-8");
77
- expect(JSON.parse(content)).toEqual(config);
78
- });
79
- test("should format JSON with 2-space indentation", async () => {
80
- const configPath = join(testDir, "config.json");
81
- const config = { key: "value" };
82
- await writeConfig(configPath, config);
83
- const content = await fs.readFile(configPath, "utf-8");
84
- expect(content).toBe('{\n "key": "value"\n}');
85
- });
86
- test("should throw error when write fails", async () => {
87
- const invalidPath = "/invalid/path/that/cannot/be/created/config.json";
88
- const config = { key: "value" };
89
- expect(writeConfig(invalidPath, config)).rejects.toThrow("Failed to write config file");
90
- });
91
- });
92
- describe("loadConfig", () => {
93
- test("should return default config when no config files exist", async () => {
94
- const defaultConfig = { option1: "default", option2: 42 };
95
- const validate = (c) => c;
96
- const cwdDir = join(process.cwd(), "src", "tests");
97
- const result = await loadConfig({
98
- toolName: "test-tool-nonexistent",
99
- configFile: "config-nonexistent.json",
100
- localConfigFile: ".testrc-nonexistent",
101
- defaultConfig,
102
- validate,
103
- }, cwdDir);
104
- expect(result.config).toEqual(defaultConfig);
105
- expect(result.sources.option1).toBe("default");
106
- expect(result.sources.option2).toBe("default");
107
- });
108
- test("should merge local config over default", async () => {
109
- const defaultConfig = { option1: "default", option2: 42 };
110
- const localConfig = { option1: "local" };
111
- const validate = (c) => c;
112
- await fs.writeFile(join(testDir, ".testrc"), JSON.stringify(localConfig));
113
- const result = await loadConfig({
114
- toolName: "test-tool",
115
- configFile: "config.json",
116
- localConfigFile: ".testrc",
117
- defaultConfig,
118
- validate,
119
- }, testDir);
120
- expect(result.config.option1).toBe("local");
121
- expect(result.config.option2).toBe(42);
122
- expect(result.sources.option1).toBe("local");
123
- expect(result.sources.option2).toBe("default");
124
- });
125
- test("should merge global config over default", async () => {
126
- const defaultConfig = { option1: "default", option2: 42 };
127
- const globalConfig = { option1: "global" };
128
- const validate = (c) => c;
129
- const globalConfigPath = getGlobalConfigPath("test-tool-global", "config.json");
130
- try {
131
- await writeGlobalConfig(globalConfig, "test-tool-global", "config.json");
132
- const cwdDir = join(process.cwd(), "src", "tests");
133
- const result = await loadConfig({
134
- toolName: "test-tool-global",
135
- configFile: "config.json",
136
- localConfigFile: ".testrc-nonexistent",
137
- defaultConfig,
138
- validate,
139
- }, cwdDir);
140
- expect(result.config.option1).toBe("global");
141
- expect(result.config.option2).toBe(42);
142
- expect(result.sources.option1).toBe("global");
143
- expect(result.sources.option2).toBe("default");
144
- }
145
- finally {
146
- fs.rm(globalConfigPath, { force: true });
147
- const dir = join(globalConfigPath, "..");
148
- fs.rm(dir, { force: true, recursive: true });
149
- }
150
- });
151
- });
152
- describe("getGlobalConfigPath", () => {
153
- test("should return correct global config path", () => {
154
- const path = getGlobalConfigPath("my-tool", "config.json");
155
- expect(path).toContain(".config");
156
- expect(path).toContain("my-tool");
157
- expect(path).toContain("config.json");
158
- });
159
- });
160
- describe("findLocalConfigPath", () => {
161
- test("should find local config file", async () => {
162
- await fs.writeFile(join(testDir, ".testrc"), "{}");
163
- const result = await findLocalConfigPath(".testrc", testDir);
164
- expect(result).toBe(join(testDir, ".testrc"));
165
- });
166
- test("should return null when local config doesn't exist", async () => {
167
- const cwdDir = join(process.cwd(), "src", "tests");
168
- const result = await findLocalConfigPath(".testrc-nonexistent", cwdDir);
169
- expect(result).toBeNull();
170
- });
171
- });
172
- describe("writeLocalConfig", () => {
173
- test("should write local config", async () => {
174
- const configPath = join(testDir, ".testrc");
175
- const config = { key: "value" };
176
- await writeLocalConfig(configPath, config);
177
- const content = await fs.readFile(configPath, "utf-8");
178
- expect(JSON.parse(content)).toEqual(config);
179
- });
180
- });
181
- describe("loadLocalConfig", () => {
182
- test("should load existing local config", async () => {
183
- const config = { key: "value" };
184
- const configPath = join(testDir, ".testrc");
185
- await fs.writeFile(configPath, JSON.stringify(config));
186
- const validate = (c) => c;
187
- const result = await loadLocalConfig(".testrc", validate, testDir);
188
- expect(result.exists).toBe(true);
189
- expect(result.config).toEqual(config);
190
- expect(result.path).toBe(configPath);
191
- });
192
- test("should return null config when file doesn't exist", async () => {
193
- const validate = (c) => c;
194
- const cwdDir = join(process.cwd(), "src", "tests");
195
- const result = await loadLocalConfig(".testrc-nonexistent", validate, cwdDir);
196
- expect(result.exists).toBe(false);
197
- expect(result.config).toBeNull();
198
- expect(result.path).toBe(join(cwdDir, ".testrc-nonexistent"));
199
- });
200
- });
201
- describe("writeGlobalConfig", () => {
202
- test("should write global config", async () => {
203
- const config = { key: "value" };
204
- const globalPath = getGlobalConfigPath("test-tool-temp", "config.json");
205
- try {
206
- await writeGlobalConfig(config, "test-tool-temp", "config.json");
207
- const content = await fs.readFile(globalPath, "utf-8");
208
- expect(JSON.parse(content)).toEqual(config);
209
- }
210
- finally {
211
- fs.rm(globalPath, { force: true });
212
- const dir = join(globalPath, "..");
213
- fs.rm(dir, { force: true, recursive: true });
214
- }
215
- });
216
- });
217
- describe("resetGlobalConfig", () => {
218
- test("should reset global config to defaults", async () => {
219
- const defaultConfig = { key: "default" };
220
- const globalPath = getGlobalConfigPath("test-tool-reset", "config.json");
221
- try {
222
- await writeGlobalConfig({ key: "modified" }, "test-tool-reset", "config.json");
223
- await resetGlobalConfig(defaultConfig, "test-tool-reset", "config.json");
224
- const content = await fs.readFile(globalPath, "utf-8");
225
- expect(JSON.parse(content)).toEqual(defaultConfig);
226
- }
227
- finally {
228
- fs.rm(globalPath, { force: true });
229
- const dir = join(globalPath, "..");
230
- fs.rm(dir, { force: true, recursive: true });
231
- }
232
- });
233
- });
234
- });
@@ -1 +0,0 @@
1
- export {};