UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,425 lines (1,196 loc) 52.2 kB
import { Alepha, z } from "alepha"; import { describe, expect, it } from "vitest"; import { $command } from "../primitives/$command.ts"; import { CliProvider } from "../providers/CliProvider.ts"; /** * Test subclass that exposes protected methods for unit testing. */ class TestCliProvider extends CliProvider { public testParseFlags = this.parseFlags.bind(this); public testParseCommandArgs = this.parseCommandArgs.bind(this); public testParseArgumentValue = this.parseArgumentValue.bind(this); public testParseModeFlag = this.parseModeFlag.bind(this); public testResolveCommand = this.resolveCommand.bind(this); public testRemoveConsumedArgs = this.removeConsumedArgs.bind(this); public testGetFlagConsumedIndices = this.getFlagConsumedIndices.bind(this); public testGenerateArgsUsage = this.generateArgsUsage.bind(this); public testGenerateColoredArgsUsage = this.generateColoredArgsUsage.bind(this); public testGetTypeName = this.getTypeName.bind(this); public testGetTopLevelCommands = this.getTopLevelCommands.bind(this); public testFindParentCommand = this.findParentCommand.bind(this); public testGetCommandPath = this.getCommandPath.bind(this); public testFindCommand = this.findCommand.bind(this); public testFindPreHooks = this.findPreHooks.bind(this); public testFindPostHooks = this.findPostHooks.bind(this); public testGetEnumValues = this.getEnumValues.bind(this); public testFormatFlagDescription = this.formatFlagDescription.bind(this); public testParseCommandEnv = this.parseCommandEnv.bind(this); /** * Extract flag definitions from a command's flags schema (for testing printHelp logic). * * Delegates to the real `extractFlagDefs`, which reads aliases/description from * the schema's `.meta()` registry (zod) — under typebox these were direct * schema properties, but strict-zod stores them in metadata. */ public testExtractFlagDefs = this.extractFlagDefs.bind(this); /** * Format aliases array into flag string (e.g., "-t, --target"). * Sorts by length (shorter first). */ public testFormatFlagStr(aliases: string[]): string { return aliases .slice() .sort((a, b) => a.length - b.length) .map((a) => (a.length === 1 ? `-${a}` : `--${a}`)) .join(", "); } } describe("CliProvider", () => { const createTestCli = () => { const alepha = Alepha.create(); return alepha.inject(TestCliProvider); }; // ───────────────────────────────────────────────────────────────────────────── // parseFlags // ───────────────────────────────────────────────────────────────────────────── describe("parseFlags", () => { it("should parse boolean flags", () => { const cli = createTestCli(); const flagDefs = [ { key: "verbose", aliases: ["v", "verbose"], schema: z.boolean() }, ]; const result = cli.testParseFlags(["--verbose"], flagDefs); expect(result.verbose).toBe(true); }); it("should parse short boolean flags", () => { const cli = createTestCli(); const flagDefs = [ { key: "verbose", aliases: ["v", "verbose"], schema: z.boolean() }, ]; const result = cli.testParseFlags(["-v"], flagDefs); expect(result.verbose).toBe(true); }); it("should parse string flags with = syntax", () => { const cli = createTestCli(); const flagDefs = [ { key: "name", aliases: ["n", "name"], schema: z.string() }, ]; const result = cli.testParseFlags(["--name=hello"], flagDefs); expect(result.name).toBe("hello"); }); it("should parse string flags with space syntax", () => { const cli = createTestCli(); const flagDefs = [ { key: "name", aliases: ["n", "name"], schema: z.string() }, ]; const result = cli.testParseFlags(["--name", "hello"], flagDefs); expect(result.name).toBe("hello"); }); it("should parse JSON object flags", () => { const cli = createTestCli(); const flagDefs = [ { key: "config", aliases: ["config"], schema: z.object({}) }, ]; const result = cli.testParseFlags( ["--config", '{"key":"value"}'], flagDefs, ); expect(result.config).toEqual({ key: "value" }); }); it("should parse JSON array flags", () => { const cli = createTestCli(); const flagDefs = [ { key: "items", aliases: ["items"], schema: z.array(z.string()) }, ]; const result = cli.testParseFlags(["--items", '["a","b"]'], flagDefs); expect(result.items).toEqual(["a", "b"]); }); it("should throw on unknown flags", () => { const cli = createTestCli(); const flagDefs = [ { key: "known", aliases: ["known"], schema: z.boolean() }, ]; expect(() => cli.testParseFlags(["--unknown", "--known"], flagDefs), ).toThrow("Unknown flag: --unknown"); }); it("should throw on missing required value", () => { const cli = createTestCli(); const flagDefs = [{ key: "name", aliases: ["name"], schema: z.string() }]; expect(() => cli.testParseFlags(["--name"], flagDefs)).toThrow( "requires a value", ); }); it("should throw on invalid JSON", () => { const cli = createTestCli(); const flagDefs = [ { key: "config", aliases: ["config"], schema: z.object({}) }, ]; expect(() => cli.testParseFlags(["--config", "{invalid}"], flagDefs), ).toThrow("Invalid JSON"); }); it("should parse union(boolean, text) flag without value as true", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testParseFlags(["-i"], flagDefs); expect(result.image).toBe(true); }); it("should parse union(boolean, text) flag with = value as string", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testParseFlags(["-i=1.3.4"], flagDefs); expect(result.image).toBe("1.3.4"); }); it("should parse union(boolean, text) flag with space value as string", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testParseFlags(["-i", "1.3.4"], flagDefs); expect(result.image).toBe("1.3.4"); }); it("should parse union(boolean, text) flag without value when followed by another flag", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, { key: "verbose", aliases: ["v", "verbose"], schema: z.boolean() }, ]; const result = cli.testParseFlags(["-i", "-v"], flagDefs); expect(result.image).toBe(true); expect(result.verbose).toBe(true); }); it("should parse union(boolean, text) flag with long form without value", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testParseFlags(["--image"], flagDefs); expect(result.image).toBe(true); }); it("should parse union(boolean, text) flag at end of argv without value", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, { key: "verbose", aliases: ["v", "verbose"], schema: z.boolean() }, ]; const result = cli.testParseFlags(["-v", "-i"], flagDefs); expect(result.verbose).toBe(true); expect(result.image).toBe(true); }); it("should parse union(boolean, text) with empty = value as empty string", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; // Note: --image= results in empty string value const result = cli.testParseFlags(["--image="], flagDefs); // Empty string is falsy, so it goes through the "no value" path → true // This is expected behavior - use --image="" if you need empty string expect(result.image).toBe(true); }); it("should parse union(boolean, text) with value containing special chars", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testParseFlags( ["--image=my-org/my-app:v1.2.3-beta"], flagDefs, ); expect(result.image).toBe("my-org/my-app:v1.2.3-beta"); }); }); // ───────────────────────────────────────────────────────────────────────────── // parseArgumentValue // ───────────────────────────────────────────────────────────────────────────── describe("parseArgumentValue", () => { it("should parse string values", () => { const cli = createTestCli(); expect(cli.testParseArgumentValue("hello", z.string())).toBe("hello"); }); it("should parse number values", () => { const cli = createTestCli(); expect(cli.testParseArgumentValue("42", z.number())).toBe(42); expect(cli.testParseArgumentValue("3.14", z.number())).toBe(3.14); }); it("should throw on invalid number", () => { const cli = createTestCli(); expect(() => cli.testParseArgumentValue("abc", z.number())).toThrow( "Expected number", ); }); it("should parse integer values", () => { const cli = createTestCli(); expect(cli.testParseArgumentValue("42", z.integer())).toBe(42); }); it("should throw on non-integer for integer schema", () => { const cli = createTestCli(); expect(() => cli.testParseArgumentValue("3.14", z.integer())).toThrow( "Expected integer", ); }); it("should parse boolean true values", () => { const cli = createTestCli(); expect(cli.testParseArgumentValue("true", z.boolean())).toBe(true); expect(cli.testParseArgumentValue("1", z.boolean())).toBe(true); }); it("should parse boolean false values", () => { const cli = createTestCli(); expect(cli.testParseArgumentValue("false", z.boolean())).toBe(false); expect(cli.testParseArgumentValue("0", z.boolean())).toBe(false); }); it("should throw on invalid boolean", () => { const cli = createTestCli(); expect(() => cli.testParseArgumentValue("yes", z.boolean())).toThrow( "Expected boolean", ); }); }); // ───────────────────────────────────────────────────────────────────────────── // parseCommandArgs // ───────────────────────────────────────────────────────────────────────────── describe("parseCommandArgs", () => { it("should return undefined when no schema", () => { const cli = createTestCli(); expect( cli.testParseCommandArgs(["arg"], undefined, true), ).toBeUndefined(); }); it("should parse optional args when present", () => { const cli = createTestCli(); const schema = z.string().optional(); expect(cli.testParseCommandArgs(["hello"], schema, true)).toBe("hello"); }); it("should return undefined for optional args when missing", () => { const cli = createTestCli(); const schema = z.string().optional(); expect(cli.testParseCommandArgs([], schema, true)).toBeUndefined(); }); it("should parse required args", () => { const cli = createTestCli(); const schema = z.string(); expect(cli.testParseCommandArgs(["hello"], schema, true)).toBe("hello"); }); it("should throw on missing required args", () => { const cli = createTestCli(); const schema = z.string(); expect(() => cli.testParseCommandArgs([], schema, true)).toThrow( "Missing required argument", ); }); it("should parse tuple args", () => { const cli = createTestCli(); const schema = z.tuple([z.string(), z.number()]); const result = cli.testParseCommandArgs(["hello", "42"], schema, true); expect(result).toEqual(["hello", 42]); }); it("should handle optional tuple items", () => { const cli = createTestCli(); const schema = z.tuple([z.string(), z.number().optional()]); const result = cli.testParseCommandArgs(["hello"], schema, true); expect(result).toEqual(["hello", undefined]); }); it("should skip flags when parsing args", () => { const cli = createTestCli(); const schema = z.string(); const flags = z.object({ verbose: z.boolean().optional() }); const result = cli.testParseCommandArgs( ["--verbose", "hello"], schema, true, flags, ); expect(result).toBe("hello"); }); it("should skip flag values when parsing args", () => { const cli = createTestCli(); const schema = z.string(); const flags = z.object({ name: z.string().optional() }); const result = cli.testParseCommandArgs( ["--name", "ignored", "actual"], schema, true, flags, ); expect(result).toBe("actual"); }); }); // ───────────────────────────────────────────────────────────────────────────── // parseModeFlag // ───────────────────────────────────────────────────────────────────────────── describe("parseModeFlag", () => { it("should parse --mode=value", () => { const cli = createTestCli(); expect(cli.testParseModeFlag(["--mode=production"])).toBe("production"); }); it("should parse -m=value", () => { const cli = createTestCli(); expect(cli.testParseModeFlag(["-m=staging"])).toBe("staging"); }); it("should parse --mode value", () => { const cli = createTestCli(); expect(cli.testParseModeFlag(["--mode", "production"])).toBe( "production", ); }); it("should parse -m value", () => { const cli = createTestCli(); expect(cli.testParseModeFlag(["-m", "staging"])).toBe("staging"); }); it("should return undefined when no mode flag", () => { const cli = createTestCli(); expect(cli.testParseModeFlag(["--other", "value"])).toBeUndefined(); }); it("should throw when --mode has no value", () => { const cli = createTestCli(); expect(() => cli.testParseModeFlag(["--mode"])).toThrow( "requires a value", ); }); it("should throw when --mode followed by another flag", () => { const cli = createTestCli(); expect(() => cli.testParseModeFlag(["--mode", "--other"])).toThrow( "requires a value", ); }); }); // ───────────────────────────────────────────────────────────────────────────── // resolveCommand // ───────────────────────────────────────────────────────────────────────────── describe("resolveCommand", () => { it("should return undefined for empty args", () => { const cli = createTestCli(); const result = cli.testResolveCommand([]); expect(result.command).toBeUndefined(); expect(result.consumedArgs).toEqual([]); }); it("should find command by name", () => { class TestCommands { build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const result = cli.testResolveCommand(["build"]); expect(result.command?.name).toBe("build"); expect(result.consumedArgs).toEqual(["build"]); }); it("should find command by colon notation", () => { class TestCommands { deployVercel = $command({ name: "deploy:vercel", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const result = cli.testResolveCommand(["deploy:vercel"]); expect(result.command?.name).toBe("deploy:vercel"); expect(result.consumedArgs).toEqual(["deploy:vercel"]); }); it("should return undefined for unknown command", () => { const cli = createTestCli(); const result = cli.testResolveCommand(["unknown"]); expect(result.command).toBeUndefined(); expect(result.consumedArgs).toEqual([]); }); }); // ───────────────────────────────────────────────────────────────────────────── // removeConsumedArgs // ───────────────────────────────────────────────────────────────────────────── describe("removeConsumedArgs", () => { it("should remove consumed command args", () => { const cli = createTestCli(); const result = cli.testRemoveConsumedArgs( ["build", "--verbose", "extra"], ["build"], ); expect(result).toEqual(["--verbose", "extra"]); }); it("should preserve flags", () => { const cli = createTestCli(); const result = cli.testRemoveConsumedArgs( ["--flag", "build", "arg"], ["build"], ); expect(result).toEqual(["--flag", "arg"]); }); it("should remove multiple consumed args", () => { const cli = createTestCli(); const result = cli.testRemoveConsumedArgs( ["deploy", "vercel", "--prod"], ["deploy", "vercel"], ); expect(result).toEqual(["--prod"]); }); }); // ───────────────────────────────────────────────────────────────────────────── // getFlagConsumedIndices // ───────────────────────────────────────────────────────────────────────────── describe("getFlagConsumedIndices", () => { it("should mark flag indices as consumed", () => { const cli = createTestCli(); const flagDefs = [ { key: "verbose", aliases: ["v", "verbose"], schema: z.boolean() }, ]; const result = cli.testGetFlagConsumedIndices(["--verbose"], flagDefs); expect(result.has(0)).toBe(true); }); it("should mark flag value indices as consumed", () => { const cli = createTestCli(); const flagDefs = [{ key: "name", aliases: ["name"], schema: z.string() }]; const result = cli.testGetFlagConsumedIndices( ["--name", "value", "arg"], flagDefs, ); expect(result.has(0)).toBe(true); // --name expect(result.has(1)).toBe(true); // value expect(result.has(2)).toBe(false); // arg }); it("should not consume next arg for --flag=value syntax", () => { const cli = createTestCli(); const flagDefs = [{ key: "name", aliases: ["name"], schema: z.string() }]; const result = cli.testGetFlagConsumedIndices( ["--name=value", "arg"], flagDefs, ); expect(result.has(0)).toBe(true); // --name=value expect(result.has(1)).toBe(false); // arg }); it("should consume value for union(boolean, text) when value is provided", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testGetFlagConsumedIndices( ["-i", "1.3.4", "arg"], flagDefs, ); expect(result.has(0)).toBe(true); // -i expect(result.has(1)).toBe(true); // 1.3.4 (consumed as value) expect(result.has(2)).toBe(false); // arg }); it("should not consume next arg for union(boolean, text) when next is a flag", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, { key: "verbose", aliases: ["v", "verbose"], schema: z.boolean() }, ]; const result = cli.testGetFlagConsumedIndices(["-i", "-v"], flagDefs); expect(result.has(0)).toBe(true); // -i expect(result.has(1)).toBe(true); // -v (not consumed by -i, but by itself) }); it("should not consume next arg for union(boolean, text) at end of argv", () => { const cli = createTestCli(); const flagDefs = [ { key: "image", aliases: ["i", "image"], schema: z.union([z.boolean(), z.text()]), }, ]; const result = cli.testGetFlagConsumedIndices(["-i"], flagDefs); expect(result.has(0)).toBe(true); // -i expect(result.size).toBe(1); }); }); // ───────────────────────────────────────────────────────────────────────────── // generateArgsUsage // ───────────────────────────────────────────────────────────────────────────── describe("generateArgsUsage", () => { it("should return empty string for no schema", () => { const cli = createTestCli(); expect(cli.testGenerateArgsUsage(undefined)).toBe(""); }); it("should generate optional arg usage", () => { const cli = createTestCli(); const schema = z.string().meta({ title: "path" }).optional(); expect(cli.testGenerateArgsUsage(schema)).toBe(" [path]"); }); it("should generate required arg usage", () => { const cli = createTestCli(); const schema = z.string().meta({ title: "path" }); expect(cli.testGenerateArgsUsage(schema)).toBe(" <path>"); }); it("should generate tuple arg usage", () => { const cli = createTestCli(); const schema = z.tuple([z.string(), z.number().optional()]); const result = cli.testGenerateArgsUsage(schema); expect(result).toContain("<arg1>"); expect(result).toContain("[arg2"); }); it("should include type for numbers", () => { const cli = createTestCli(); const schema = z.number().meta({ title: "count" }); expect(cli.testGenerateArgsUsage(schema)).toBe(" <count: number>"); }); it("should include type for integers", () => { const cli = createTestCli(); const schema = z.integer().meta({ title: "port" }); expect(cli.testGenerateArgsUsage(schema)).toBe(" <port: integer>"); }); it("should include type for booleans", () => { const cli = createTestCli(); const schema = z.boolean().meta({ title: "force" }); expect(cli.testGenerateArgsUsage(schema)).toBe(" <force: boolean>"); }); }); // ───────────────────────────────────────────────────────────────────────────── // getTypeName // ───────────────────────────────────────────────────────────────────────────── describe("getTypeName", () => { it("should return empty for string", () => { const cli = createTestCli(); expect(cli.testGetTypeName(z.string())).toBe(""); }); it("should return ': number' for number", () => { const cli = createTestCli(); expect(cli.testGetTypeName(z.number())).toBe(": number"); }); it("should return ': integer' for integer", () => { const cli = createTestCli(); expect(cli.testGetTypeName(z.integer())).toBe(": integer"); }); it("should return ': boolean' for boolean", () => { const cli = createTestCli(); expect(cli.testGetTypeName(z.boolean())).toBe(": boolean"); }); }); // ───────────────────────────────────────────────────────────────────────────── // generateColoredArgsUsage // ───────────────────────────────────────────────────────────────────────────── describe("generateColoredArgsUsage", () => { it("should return empty string for no schema", () => { const cli = createTestCli(); expect(cli.testGenerateColoredArgsUsage(undefined)).toBe(""); }); it("should generate colored optional arg usage", () => { const cli = createTestCli(); const schema = z.string().meta({ title: "path" }).optional(); const result = cli.testGenerateColoredArgsUsage(schema); expect(result).toContain("[path]"); }); it("should generate colored required arg usage", () => { const cli = createTestCli(); const schema = z.string().meta({ title: "path" }); const result = cli.testGenerateColoredArgsUsage(schema); expect(result).toContain("<path>"); }); it("should generate colored tuple arg usage", () => { const cli = createTestCli(); const schema = z.tuple([z.string(), z.number().optional()]); const result = cli.testGenerateColoredArgsUsage(schema); expect(result).toContain("<arg1>"); expect(result).toContain("[arg2"); }); }); // ───────────────────────────────────────────────────────────────────────────── // findCommand // ───────────────────────────────────────────────────────────────────────────── describe("findCommand", () => { it("should return undefined for unknown command", () => { const cli = createTestCli(); expect(cli.testFindCommand("unknown")).toBeUndefined(); }); it("should find command by name", () => { class TestCommands { test = $command({ name: "test", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); expect(cli.testFindCommand("test")?.name).toBe("test"); }); it("should find command by alias", () => { class TestCommands { test = $command({ name: "test", aliases: ["t"], flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); expect(cli.testFindCommand("t")?.name).toBe("test"); }); }); // ───────────────────────────────────────────────────────────────────────────── // findPreHooks / findPostHooks // ───────────────────────────────────────────────────────────────────────────── describe("findPreHooks", () => { it("should return empty array when no hooks", () => { const cli = createTestCli(); expect(cli.testFindPreHooks("build")).toEqual([]); }); it("should find pre-hooks for command", () => { class TestCommands { preBuild = $command({ name: "prebuild", flags: z.object({}), handler: async () => {}, }); build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); expect(cli.testFindPreHooks("build").length).toBe(1); }); }); describe("findPostHooks", () => { it("should return empty array when no hooks", () => { const cli = createTestCli(); expect(cli.testFindPostHooks("build")).toEqual([]); }); it("should find post-hooks for command", () => { class TestCommands { build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); postBuild = $command({ name: "postbuild", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); expect(cli.testFindPostHooks("build").length).toBe(1); }); }); // ───────────────────────────────────────────────────────────────────────────── // getTopLevelCommands // ───────────────────────────────────────────────────────────────────────────── describe("getTopLevelCommands", () => { it("should return all commands when no children", () => { class TestCommands { build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); test = $command({ name: "test", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const topLevel = cli.testGetTopLevelCommands(); expect(topLevel.length).toBe(2); }); it("should exclude child commands", () => { class TestCommands { deployVercel = $command({ name: "vercel", flags: z.object({}), handler: async () => {}, }); deploy = $command({ name: "deploy", flags: z.object({}), children: [this.deployVercel], handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const topLevel = cli.testGetTopLevelCommands(); expect(topLevel.length).toBe(1); expect(topLevel[0].name).toBe("deploy"); }); }); // ───────────────────────────────────────────────────────────────────────────── // findParentCommand // ───────────────────────────────────────────────────────────────────────────── describe("findParentCommand", () => { it("should return undefined for top-level command", () => { class TestCommands { build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("build")!; expect(cli.testFindParentCommand(cmd)).toBeUndefined(); }); it("should find parent of child command", () => { class TestCommands { deployVercel = $command({ name: "vercel", flags: z.object({}), handler: async () => {}, }); deploy = $command({ name: "deploy", flags: z.object({}), children: [this.deployVercel], handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const child = cli.testFindCommand("vercel")!; expect(cli.testFindParentCommand(child)?.name).toBe("deploy"); }); }); // ───────────────────────────────────────────────────────────────────────────── // getCommandPath // ───────────────────────────────────────────────────────────────────────────── describe("getCommandPath", () => { it("should return simple name for top-level command", () => { class TestCommands { build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("build")!; expect(cli.testGetCommandPath(cmd)).toBe("build"); }); it("should return full path for nested command", () => { class TestCommands { deployVercel = $command({ name: "vercel", flags: z.object({}), handler: async () => {}, }); deploy = $command({ name: "deploy", flags: z.object({}), children: [this.deployVercel], handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const child = cli.testFindCommand("vercel")!; expect(cli.testGetCommandPath(child)).toBe("deploy vercel"); }); }); // ───────────────────────────────────────────────────────────────────────────── // printHelp // ───────────────────────────────────────────────────────────────────────────── describe("printHelp", () => { it("should print general help when no command provided", () => { class TestCommands { build = $command({ name: "build", description: "Build the project", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); // Just verify it doesn't throw expect(() => cli.printHelp()).not.toThrow(); }); it("should print command-specific help", () => { class TestCommands { build = $command({ name: "build", description: "Build the project", flags: z.object({ watch: z.boolean().describe("Watch for changes").optional(), }), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("build")!; expect(() => cli.printHelp(cmd)).not.toThrow(); }); it("should print help for command with children", () => { class TestCommands { deployVercel = $command({ name: "vercel", description: "Deploy to Vercel", flags: z.object({}), handler: async () => {}, }); deploy = $command({ name: "deploy", description: "Deploy commands", flags: z.object({}), children: [this.deployVercel], handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("deploy")!; expect(() => cli.printHelp(cmd)).not.toThrow(); }); it("should print help for command with env vars", () => { class TestCommands { deploy = $command({ name: "deploy", description: "Deploy to production", flags: z.object({}), env: z.object({ API_KEY: z.string().describe("API key for deployment"), REGION: z.string().describe("Target region").optional(), }), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("deploy")!; expect(() => cli.printHelp(cmd)).not.toThrow(); }); it("should print help for command with mode option", () => { class TestCommands { build = $command({ name: "build", description: "Build for environment", flags: z.object({}), mode: "development", handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("build")!; expect(() => cli.printHelp(cmd)).not.toThrow(); }); it("should print help for command with args", () => { class TestCommands { greet = $command({ name: "greet", description: "Greet someone", flags: z.object({}), args: z.string().meta({ title: "name" }), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); const cmd = cli.testFindCommand("greet")!; expect(() => cli.printHelp(cmd)).not.toThrow(); }); it("should hide commands with hide option in general help", () => { class TestCommands { visible = $command({ name: "visible", description: "Visible command", flags: z.object({}), handler: async () => {}, }); hidden = $command({ name: "hidden", description: "Hidden command", hide: true, flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); // Just verify it doesn't throw - hidden commands filtered in getTopLevelCommands expect(() => cli.printHelp()).not.toThrow(); }); it("should include flag aliases in help output", () => { const cli = createTestCli(); const flagsSchema = z.object({ target: z .enum(["bare", "docker", "vercel"]) .meta({ aliases: ["t"] }) .describe("Deployment target") .optional(), runtime: z .enum(["node", "bun"]) .meta({ alias: "r" }) .describe("JavaScript runtime") .optional(), verbose: z.boolean().describe("Verbose output").optional(), }); const flagDefs = cli.testExtractFlagDefs(flagsSchema); // target should have key + aliases array const targetFlag = flagDefs.find((f) => f.key === "target"); expect(targetFlag?.aliases).toContain("target"); expect(targetFlag?.aliases).toContain("t"); expect(cli.testFormatFlagStr(targetFlag!.aliases)).toBe("-t, --target"); // runtime should have key + alias (singular) const runtimeFlag = flagDefs.find((f) => f.key === "runtime"); expect(runtimeFlag?.aliases).toContain("runtime"); expect(runtimeFlag?.aliases).toContain("r"); expect(cli.testFormatFlagStr(runtimeFlag!.aliases)).toBe("-r, --runtime"); // verbose should only have key (no aliases defined) const verboseFlag = flagDefs.find((f) => f.key === "verbose"); expect(verboseFlag?.aliases).toEqual(["verbose"]); expect(cli.testFormatFlagStr(verboseFlag!.aliases)).toBe("--verbose"); }); }); // ───────────────────────────────────────────────────────────────────────────── // getEnumValues // ───────────────────────────────────────────────────────────────────────────── describe("getEnumValues", () => { it("should extract values from z.enum schema", () => { const cli = createTestCli(); const schema = z.enum(["yarn", "npm", "pnpm", "bun"]); const values = cli.testGetEnumValues(schema); expect(values).toEqual(["yarn", "npm", "pnpm", "bun"]); }); it("should return undefined for non-enum schemas", () => { const cli = createTestCli(); expect(cli.testGetEnumValues(z.string())).toBeUndefined(); expect(cli.testGetEnumValues(z.boolean())).toBeUndefined(); expect(cli.testGetEnumValues(z.number())).toBeUndefined(); expect(cli.testGetEnumValues(z.object({}))).toBeUndefined(); }); it("should return undefined for empty or undefined schema", () => { const cli = createTestCli(); expect(cli.testGetEnumValues(undefined as any)).toBeUndefined(); }); }); // ───────────────────────────────────────────────────────────────────────────── // formatFlagDescription // ───────────────────────────────────────────────────────────────────────────── describe("formatFlagDescription", () => { it("should append enum values to description", () => { const cli = createTestCli(); const schema = z.enum(["yarn", "npm", "pnpm", "bun"]); const result = cli.testFormatFlagDescription( "Package manager to use", schema, ); expect(result).toContain("Package manager to use"); expect(result).toContain("yarn"); expect(result).toContain("npm"); expect(result).toContain("pnpm"); expect(result).toContain("bun"); }); it("should return only enum values when description is empty", () => { const cli = createTestCli(); const schema = z.enum(["a", "b", "c"]); const result = cli.testFormatFlagDescription(undefined, schema); expect(result).toContain("a"); expect(result).toContain("b"); expect(result).toContain("c"); }); it("should return original description for non-enum schemas", () => { const cli = createTestCli(); expect(cli.testFormatFlagDescription("A string flag", z.string())).toBe( "A string flag", ); expect(cli.testFormatFlagDescription("A boolean flag", z.boolean())).toBe( "A boolean flag", ); }); it("should return empty string when no description and no enum", () => { const cli = createTestCli(); expect(cli.testFormatFlagDescription(undefined, z.string())).toBe(""); }); }); // ───────────────────────────────────────────────────────────────────────────── // commands getter // ───────────────────────────────────────────────────────────────────────────── describe("commands", () => { it("should return all registered commands", () => { class TestCommands { build = $command({ name: "build", flags: z.object({}), handler: async () => {}, }); test = $command({ name: "test", flags: z.object({}), handler: async () => {}, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(TestCliProvider); expect(cli.commands.length).toBe(2); expect(cli.commands.map((c) => c.name).sort()).toEqual(["build", "test"]); }); it("should return empty array when no commands", () => { const cli = createTestCli(); expect(cli.commands).toEqual([]); }); }); // ───────────────────────────────────────────────────────────────────────────── // parseCommandEnv // ───────────────────────────────────────────────────────────────────────────── describe("parseCommandEnv", () => { it("should use default value for required field with default when env var is missing", () => { const cli = createTestCli(); const schema = z.object({ MY_VAR: z.text({ default: "fallback" }), }); delete process.env.MY_VAR; const result = cli.testParseCommandEnv(schema, "test"); expect(result.MY_VAR).toBe("fallback"); }); it("should prefer env var over default when both exist", () => { const cli = createTestCli(); const schema = z.object({ MY_VAR: z.text({ default: "fallback" }), }); process.env.MY_VAR = "from-env"; try { const result = cli.testParseCommandEnv(schema, "test"); expect(result.MY_VAR).toBe("from-env"); } finally { delete process.env.MY_VAR; } }); it("should throw for required field without default when env var is missing", () => { const cli = createTestCli(); const schema = z.object({ MY_VAR: z.text(), }); delete process.env.MY_VAR; expect(() => cli.testParseCommandEnv(schema, "test")).toThrow( "Missing required environment variable", ); }); }); // ───────────────────────────────────────────────────────────────────────────── // run (test helper) // ───────────────────────────────────────────────────────────────────────────── describe("run", () => { it("should execute command with string args", async () => { let capturedFlags: Record<string, unknown> = {}; class TestCommands { build = $command({ name: "build", flags: z.object({ watch: z.boolean().optional(), }), handler: async ({ flags }) => { capturedFlags = flags; }, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(CliProvider); const cmd = alepha.inject(TestCommands); await cli.run(cmd.build, "--watch"); expect(capturedFlags.watch).toBe(true); }); it("should execute command with array args", async () => { let capturedFlags: Record<string, unknown> = {}; class TestCommands { greet = $command({ name: "greet", flags: z.object({ name: z.string().optional(), }), handler: async ({ flags }) => { capturedFlags = flags; }, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(CliProvider); const cmd = alepha.inject(TestCommands); await cli.run(cmd.greet, ["--name", "World"]); expect(capturedFlags.name).toBe("World"); }); it("should execute command with options object", async () => { let capturedRoot = ""; class TestCommands { init = $command({ name: "init", flags: z.object({}), handler: async ({ root }) => { capturedRoot = root; }, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(CliProvider); const cmd = alepha.inject(TestCommands); await cli.run(cmd.init, { root: "/custom/path" }); expect(capturedRoot).toBe("/custom/path"); }); it("should parse command args", async () => { let capturedArgs: unknown; class TestCommands { greet = $command({ name: "greet", flags: z.object({}), args: z.string(), handler: async ({ args }) => { capturedArgs = args; }, }); } const alepha = Alepha.create().with(TestCommands); const cli = alepha.inject(CliProvider); const cmd = alepha.inject(TestCommands); await cli.run(cmd.greet, "World"); expect(capturedArgs).toBe("World"); }); }); });