@pablozaiden/terminatui 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.
Files changed (95) hide show
  1. package/.devcontainer/devcontainer.json +19 -0
  2. package/.devcontainer/install-prerequisites.sh +49 -0
  3. package/.github/workflows/copilot-setup-steps.yml +32 -0
  4. package/.github/workflows/pull-request.yml +27 -0
  5. package/.github/workflows/release-npm-package.yml +78 -0
  6. package/LICENSE +21 -0
  7. package/README.md +524 -0
  8. package/examples/tui-app/commands/greet.ts +75 -0
  9. package/examples/tui-app/commands/index.ts +3 -0
  10. package/examples/tui-app/commands/math.ts +114 -0
  11. package/examples/tui-app/commands/status.ts +75 -0
  12. package/examples/tui-app/index.ts +34 -0
  13. package/guides/01-hello-world.md +96 -0
  14. package/guides/02-adding-options.md +103 -0
  15. package/guides/03-multiple-commands.md +163 -0
  16. package/guides/04-subcommands.md +206 -0
  17. package/guides/05-interactive-tui.md +194 -0
  18. package/guides/06-config-validation.md +264 -0
  19. package/guides/07-async-cancellation.md +388 -0
  20. package/guides/08-complete-application.md +673 -0
  21. package/guides/README.md +74 -0
  22. package/package.json +32 -0
  23. package/src/__tests__/application.test.ts +425 -0
  24. package/src/__tests__/buildCliCommand.test.ts +125 -0
  25. package/src/__tests__/builtins.test.ts +133 -0
  26. package/src/__tests__/colors.test.ts +127 -0
  27. package/src/__tests__/command.test.ts +157 -0
  28. package/src/__tests__/commandClass.test.ts +130 -0
  29. package/src/__tests__/context.test.ts +97 -0
  30. package/src/__tests__/help.test.ts +412 -0
  31. package/src/__tests__/parser.test.ts +268 -0
  32. package/src/__tests__/registry.test.ts +195 -0
  33. package/src/__tests__/registryNew.test.ts +160 -0
  34. package/src/__tests__/schemaToFields.test.ts +176 -0
  35. package/src/__tests__/table.test.ts +146 -0
  36. package/src/__tests__/tui.test.ts +26 -0
  37. package/src/builtins/help.ts +85 -0
  38. package/src/builtins/index.ts +4 -0
  39. package/src/builtins/settings.ts +106 -0
  40. package/src/builtins/version.ts +72 -0
  41. package/src/cli/help.ts +174 -0
  42. package/src/cli/index.ts +3 -0
  43. package/src/cli/output/colors.ts +74 -0
  44. package/src/cli/output/index.ts +2 -0
  45. package/src/cli/output/table.ts +141 -0
  46. package/src/cli/parser.ts +241 -0
  47. package/src/commands/help.ts +50 -0
  48. package/src/commands/index.ts +1 -0
  49. package/src/components/index.ts +147 -0
  50. package/src/core/application.ts +461 -0
  51. package/src/core/command.ts +269 -0
  52. package/src/core/context.ts +112 -0
  53. package/src/core/help.ts +214 -0
  54. package/src/core/index.ts +15 -0
  55. package/src/core/logger.ts +164 -0
  56. package/src/core/registry.ts +140 -0
  57. package/src/hooks/index.ts +131 -0
  58. package/src/index.ts +137 -0
  59. package/src/registry/commandRegistry.ts +77 -0
  60. package/src/registry/index.ts +1 -0
  61. package/src/tui/TuiApp.tsx +582 -0
  62. package/src/tui/TuiApplication.tsx +230 -0
  63. package/src/tui/app.ts +29 -0
  64. package/src/tui/components/ActionButton.tsx +36 -0
  65. package/src/tui/components/CliModal.tsx +81 -0
  66. package/src/tui/components/CommandSelector.tsx +159 -0
  67. package/src/tui/components/ConfigForm.tsx +148 -0
  68. package/src/tui/components/EditorModal.tsx +177 -0
  69. package/src/tui/components/FieldRow.tsx +30 -0
  70. package/src/tui/components/Header.tsx +31 -0
  71. package/src/tui/components/JsonHighlight.tsx +128 -0
  72. package/src/tui/components/LogsPanel.tsx +86 -0
  73. package/src/tui/components/ResultsPanel.tsx +93 -0
  74. package/src/tui/components/StatusBar.tsx +59 -0
  75. package/src/tui/components/index.ts +13 -0
  76. package/src/tui/components/types.ts +30 -0
  77. package/src/tui/context/KeyboardContext.tsx +118 -0
  78. package/src/tui/context/index.ts +7 -0
  79. package/src/tui/hooks/index.ts +35 -0
  80. package/src/tui/hooks/useClipboard.ts +66 -0
  81. package/src/tui/hooks/useCommandExecutor.ts +131 -0
  82. package/src/tui/hooks/useConfigState.ts +171 -0
  83. package/src/tui/hooks/useKeyboardHandler.ts +91 -0
  84. package/src/tui/hooks/useLogStream.ts +96 -0
  85. package/src/tui/hooks/useSpinner.ts +46 -0
  86. package/src/tui/index.ts +65 -0
  87. package/src/tui/theme.ts +21 -0
  88. package/src/tui/utils/buildCliCommand.ts +90 -0
  89. package/src/tui/utils/index.ts +13 -0
  90. package/src/tui/utils/parameterPersistence.ts +96 -0
  91. package/src/tui/utils/schemaToFields.ts +144 -0
  92. package/src/types/command.ts +103 -0
  93. package/src/types/execution.ts +11 -0
  94. package/src/types/index.ts +1 -0
  95. package/tsconfig.json +25 -0
@@ -0,0 +1,133 @@
1
+ import { test, expect, describe, mock, beforeEach, afterEach } from "bun:test";
2
+ import { createVersionCommand, formatVersion } from "../builtins/version.ts";
3
+ import { createHelpCommand } from "../commands/help.ts";
4
+ import { defineCommand } from "../types/command.ts";
5
+
6
+ describe("Built-in Commands", () => {
7
+ let originalLog: typeof console.log;
8
+ let logOutput: string[];
9
+
10
+ beforeEach(() => {
11
+ originalLog = console.log;
12
+ logOutput = [];
13
+ console.log = (...args: unknown[]) => {
14
+ logOutput.push(args.map(String).join(" "));
15
+ };
16
+ });
17
+
18
+ afterEach(() => {
19
+ console.log = originalLog;
20
+ });
21
+
22
+ describe("formatVersion", () => {
23
+ test("formats version with commit hash", () => {
24
+ const result = formatVersion("1.0.0", "abc1234567890");
25
+ expect(result).toBe("1.0.0 - abc1234");
26
+ });
27
+
28
+ test("formats version with short commit hash", () => {
29
+ const result = formatVersion("1.0.0", "abc1234");
30
+ expect(result).toBe("1.0.0 - abc1234");
31
+ });
32
+
33
+ test("shows (dev) when no commit hash", () => {
34
+ const result = formatVersion("1.0.0");
35
+ expect(result).toBe("1.0.0 - (dev)");
36
+ });
37
+
38
+ test("shows (dev) when commit hash is empty", () => {
39
+ const result = formatVersion("1.0.0", "");
40
+ expect(result).toBe("1.0.0 - (dev)");
41
+ });
42
+ });
43
+
44
+ describe("VersionCommand", () => {
45
+ test("creates command with name 'version'", () => {
46
+ const cmd = createVersionCommand("myapp", "1.0.0");
47
+ expect(cmd.name).toBe("version");
48
+ });
49
+
50
+ test("has description", () => {
51
+ const cmd = createVersionCommand("myapp", "1.0.0");
52
+ expect(cmd.description).toBeDefined();
53
+ expect(cmd.description.length).toBeGreaterThan(0);
54
+ });
55
+
56
+ test("has aliases including --version", () => {
57
+ const cmd = createVersionCommand("myapp", "1.0.0");
58
+ expect(cmd.aliases).toContain("--version");
59
+ expect(cmd.aliases).toContain("-v");
60
+ });
61
+
62
+ test("getFormattedVersion returns version with dev", () => {
63
+ const cmd = createVersionCommand("myapp", "1.2.3");
64
+ expect(cmd.getFormattedVersion()).toBe("1.2.3 - (dev)");
65
+ });
66
+
67
+ test("getFormattedVersion returns version with commit hash", () => {
68
+ const cmd = createVersionCommand("myapp", "1.2.3", "abc1234567890");
69
+ expect(cmd.getFormattedVersion()).toBe("1.2.3 - abc1234");
70
+ });
71
+ });
72
+
73
+ describe("createHelpCommand", () => {
74
+ const mockCommands = [
75
+ defineCommand({
76
+ name: "run",
77
+ description: "Run something",
78
+ execute: () => {},
79
+ }),
80
+ defineCommand({
81
+ name: "build",
82
+ description: "Build something",
83
+ execute: () => {},
84
+ }),
85
+ ];
86
+
87
+ test("creates command with name 'help'", () => {
88
+ const cmd = createHelpCommand({ getCommands: () => mockCommands });
89
+ expect(cmd.name).toBe("help");
90
+ });
91
+
92
+ test("has description", () => {
93
+ const cmd = createHelpCommand({ getCommands: () => mockCommands });
94
+ expect(cmd.description).toBeDefined();
95
+ });
96
+
97
+ test("has aliases including --help", () => {
98
+ const cmd = createHelpCommand({ getCommands: () => mockCommands });
99
+ expect(cmd.aliases).toContain("--help");
100
+ });
101
+
102
+ test("is hidden by default", () => {
103
+ const cmd = createHelpCommand({ getCommands: () => mockCommands });
104
+ expect(cmd.hidden).toBe(true);
105
+ });
106
+
107
+ test("has command option", () => {
108
+ const cmd = createHelpCommand({ getCommands: () => mockCommands });
109
+ expect(cmd.options?.["command"]).toBeDefined();
110
+ });
111
+
112
+ test("execute calls getCommands", () => {
113
+ const getCommands = mock(() => mockCommands);
114
+ const cmd = createHelpCommand({ getCommands });
115
+ // @ts-expect-error - testing with partial options
116
+ cmd.execute({ options: {}, args: [], commandPath: ["help"] });
117
+ expect(getCommands).toHaveBeenCalled();
118
+ });
119
+
120
+ test("shows help for specific command when provided", () => {
121
+ const cmd = createHelpCommand({
122
+ getCommands: () => mockCommands,
123
+ appName: "myapp",
124
+ });
125
+ cmd.execute({
126
+ options: { command: "run" },
127
+ args: [],
128
+ commandPath: ["help"],
129
+ });
130
+ expect(logOutput.join("")).toContain("run");
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,127 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { colors, supportsColors } from "../cli/output/colors.ts";
3
+
4
+ describe("colors", () => {
5
+ describe("basic colors", () => {
6
+ test("red wraps text", () => {
7
+ const result = colors.red("test");
8
+ expect(result).toContain("test");
9
+ });
10
+
11
+ test("green wraps text", () => {
12
+ const result = colors.green("test");
13
+ expect(result).toContain("test");
14
+ });
15
+
16
+ test("blue wraps text", () => {
17
+ const result = colors.blue("test");
18
+ expect(result).toContain("test");
19
+ });
20
+
21
+ test("yellow wraps text", () => {
22
+ const result = colors.yellow("test");
23
+ expect(result).toContain("test");
24
+ });
25
+
26
+ test("cyan wraps text", () => {
27
+ const result = colors.cyan("test");
28
+ expect(result).toContain("test");
29
+ });
30
+
31
+ test("gray wraps text", () => {
32
+ const result = colors.gray("test");
33
+ expect(result).toContain("test");
34
+ });
35
+ });
36
+
37
+ describe("styles", () => {
38
+ test("bold wraps text", () => {
39
+ const result = colors.bold("test");
40
+ expect(result).toContain("test");
41
+ });
42
+
43
+ test("dim wraps text", () => {
44
+ const result = colors.dim("test");
45
+ expect(result).toContain("test");
46
+ });
47
+
48
+ test("italic wraps text", () => {
49
+ const result = colors.italic("test");
50
+ expect(result).toContain("test");
51
+ });
52
+
53
+ test("underline wraps text", () => {
54
+ const result = colors.underline("test");
55
+ expect(result).toContain("test");
56
+ });
57
+
58
+ test("strikethrough wraps text", () => {
59
+ const result = colors.strikethrough("test");
60
+ expect(result).toContain("test");
61
+ });
62
+ });
63
+
64
+ describe("semantic colors", () => {
65
+ test("success includes checkmark and message", () => {
66
+ const result = colors.success("done");
67
+ expect(result).toContain("done");
68
+ expect(result).toContain("✓");
69
+ });
70
+
71
+ test("error includes message", () => {
72
+ const result = colors.error("failed");
73
+ expect(result).toContain("failed");
74
+ });
75
+
76
+ test("warning includes message", () => {
77
+ const result = colors.warning("caution");
78
+ expect(result).toContain("caution");
79
+ });
80
+
81
+ test("info includes message", () => {
82
+ const result = colors.info("note");
83
+ expect(result).toContain("note");
84
+ });
85
+ });
86
+
87
+ describe("chaining", () => {
88
+ test("can combine bold and red", () => {
89
+ const result = colors.bold(colors.red("test"));
90
+ expect(result).toContain("test");
91
+ });
92
+
93
+ test("can combine dim and italic", () => {
94
+ const result = colors.dim(colors.italic("test"));
95
+ expect(result).toContain("test");
96
+ });
97
+ });
98
+
99
+ describe("edge cases", () => {
100
+ test("handles empty string", () => {
101
+ const result = colors.red("");
102
+ expect(typeof result).toBe("string");
103
+ });
104
+
105
+ test("handles string with newlines", () => {
106
+ const result = colors.blue("line1\nline2");
107
+ expect(result).toContain("line1");
108
+ expect(result).toContain("line2");
109
+ });
110
+
111
+ test("handles string with special characters", () => {
112
+ const result = colors.green("test © ® ™");
113
+ expect(result).toContain("©");
114
+ });
115
+ });
116
+ });
117
+
118
+ describe("supportsColors", () => {
119
+ test("is a function", () => {
120
+ expect(typeof supportsColors).toBe("function");
121
+ });
122
+
123
+ test("returns a boolean", () => {
124
+ const result = supportsColors();
125
+ expect(typeof result).toBe("boolean");
126
+ });
127
+ });
@@ -0,0 +1,157 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { defineCommand } from "../types/command.ts";
3
+
4
+ describe("defineCommand", () => {
5
+ test("creates a command with name and description", () => {
6
+ const cmd = defineCommand({
7
+ name: "test",
8
+ description: "A test command",
9
+ execute: () => {},
10
+ });
11
+
12
+ expect(cmd.name).toBe("test");
13
+ expect(cmd.description).toBe("A test command");
14
+ });
15
+
16
+ test("creates a command with options", () => {
17
+ const cmd = defineCommand({
18
+ name: "test",
19
+ description: "A test command",
20
+ options: {
21
+ verbose: {
22
+ type: "boolean",
23
+ description: "Enable verbose output",
24
+ alias: "v",
25
+ },
26
+ },
27
+ execute: () => {},
28
+ });
29
+
30
+ expect(cmd.options?.["verbose"]).toBeDefined();
31
+ expect(cmd.options?.["verbose"]?.type).toBe("boolean");
32
+ });
33
+
34
+ test("creates a command with aliases", () => {
35
+ const cmd = defineCommand({
36
+ name: "test",
37
+ description: "A test command",
38
+ aliases: ["t", "tst"],
39
+ execute: () => {},
40
+ });
41
+
42
+ expect(cmd.aliases).toEqual(["t", "tst"]);
43
+ });
44
+
45
+ test("creates a command with subcommands", () => {
46
+ const sub = defineCommand({
47
+ name: "sub",
48
+ description: "A subcommand",
49
+ execute: () => {},
50
+ });
51
+
52
+ const cmd = defineCommand({
53
+ name: "parent",
54
+ description: "A parent command",
55
+ subcommands: { sub },
56
+ execute: () => {},
57
+ });
58
+
59
+ expect(cmd.subcommands?.["sub"]).toBe(sub);
60
+ });
61
+
62
+ test("executes sync command", () => {
63
+ let executed = false;
64
+ const cmd = defineCommand({
65
+ name: "test",
66
+ description: "A test command",
67
+ execute: () => {
68
+ executed = true;
69
+ },
70
+ });
71
+
72
+ cmd.execute({ options: {}, args: [], commandPath: ["test"] });
73
+ expect(executed).toBe(true);
74
+ });
75
+
76
+ test("executes async command", async () => {
77
+ let executed = false;
78
+ const cmd = defineCommand({
79
+ name: "test",
80
+ description: "A test command",
81
+ execute: async () => {
82
+ await new Promise((r) => setTimeout(r, 10));
83
+ executed = true;
84
+ },
85
+ });
86
+
87
+ await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
88
+ expect(executed).toBe(true);
89
+ });
90
+
91
+ test("passes options to execute", () => {
92
+ let receivedOptions: unknown;
93
+ const cmd = defineCommand({
94
+ name: "test",
95
+ description: "A test command",
96
+ options: {
97
+ name: {
98
+ type: "string",
99
+ description: "Name option",
100
+ },
101
+ },
102
+ execute: (ctx) => {
103
+ receivedOptions = ctx.options;
104
+ },
105
+ });
106
+
107
+ cmd.execute({ options: { name: "world" }, args: [], commandPath: ["test"] });
108
+ expect(receivedOptions).toEqual({ name: "world" });
109
+ });
110
+
111
+ test("supports beforeExecute hook", async () => {
112
+ const order: string[] = [];
113
+ const cmd = defineCommand({
114
+ name: "test",
115
+ description: "A test command",
116
+ beforeExecute: () => {
117
+ order.push("before");
118
+ },
119
+ execute: () => {
120
+ order.push("execute");
121
+ },
122
+ });
123
+
124
+ await cmd.beforeExecute?.({ options: {}, args: [], commandPath: ["test"] });
125
+ await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
126
+ expect(order).toEqual(["before", "execute"]);
127
+ });
128
+
129
+ test("supports afterExecute hook", async () => {
130
+ const order: string[] = [];
131
+ const cmd = defineCommand({
132
+ name: "test",
133
+ description: "A test command",
134
+ execute: () => {
135
+ order.push("execute");
136
+ },
137
+ afterExecute: () => {
138
+ order.push("after");
139
+ },
140
+ });
141
+
142
+ await cmd.execute({ options: {}, args: [], commandPath: ["test"] });
143
+ await cmd.afterExecute?.({ options: {}, args: [], commandPath: ["test"] });
144
+ expect(order).toEqual(["execute", "after"]);
145
+ });
146
+
147
+ test("supports hidden commands", () => {
148
+ const cmd = defineCommand({
149
+ name: "hidden",
150
+ description: "A hidden command",
151
+ hidden: true,
152
+ execute: () => {},
153
+ });
154
+
155
+ expect(cmd.hidden).toBe(true);
156
+ });
157
+ });
@@ -0,0 +1,130 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { Command, type CommandResult } from "../core/command.ts";
3
+ import type { AppContext } from "../core/context.ts";
4
+ import type { OptionSchema, OptionValues } from "../types/command.ts";
5
+
6
+ // Test command with options
7
+ class TestCommand extends Command<{ name: { type: "string"; description: string } }> {
8
+ readonly name = "test";
9
+ readonly description = "A test command";
10
+ readonly options = {
11
+ name: { type: "string" as const, description: "Name option" },
12
+ };
13
+
14
+ executedWith: OptionValues<typeof this.options> | null = null;
15
+
16
+ override async execute(
17
+ _ctx: AppContext,
18
+ opts: OptionValues<typeof this.options>
19
+ ): Promise<CommandResult> {
20
+ this.executedWith = opts;
21
+ return { success: true, message: "Executed" };
22
+ }
23
+ }
24
+
25
+ // Simple command without options
26
+ class SimpleCommand extends Command<OptionSchema> {
27
+ readonly name = "simple";
28
+ readonly description = "A simple command";
29
+ readonly options = {};
30
+
31
+ executed = false;
32
+
33
+ override async execute(_ctx: AppContext): Promise<CommandResult> {
34
+ this.executed = true;
35
+ return { success: true, message: "Done" };
36
+ }
37
+ }
38
+
39
+ describe("Command", () => {
40
+ describe("core properties", () => {
41
+ test("has name", () => {
42
+ const cmd = new TestCommand();
43
+ expect(cmd.name).toBe("test");
44
+ });
45
+
46
+ test("has description", () => {
47
+ const cmd = new TestCommand();
48
+ expect(cmd.description).toBe("A test command");
49
+ });
50
+
51
+ test("has options", () => {
52
+ const cmd = new TestCommand();
53
+ expect(cmd.options).toEqual({
54
+ name: { type: "string", description: "Name option" },
55
+ });
56
+ });
57
+ });
58
+
59
+ describe("optional metadata", () => {
60
+ test("subCommands defaults to undefined", () => {
61
+ const cmd = new TestCommand();
62
+ expect(cmd.subCommands).toBeUndefined();
63
+ });
64
+
65
+ test("examples defaults to undefined", () => {
66
+ const cmd = new TestCommand();
67
+ expect(cmd.examples).toBeUndefined();
68
+ });
69
+
70
+ test("longDescription defaults to undefined", () => {
71
+ const cmd = new TestCommand();
72
+ expect(cmd.longDescription).toBeUndefined();
73
+ });
74
+ });
75
+
76
+ describe("supportsCli", () => {
77
+ test("returns true for command with execute", () => {
78
+ const cmd = new TestCommand();
79
+ expect(cmd.supportsCli()).toBe(true);
80
+ });
81
+ });
82
+
83
+ describe("supportsTui", () => {
84
+ test("returns true for command with execute", () => {
85
+ const cmd = new TestCommand();
86
+ expect(cmd.supportsTui()).toBe(true);
87
+ });
88
+ });
89
+
90
+ describe("both modes", () => {
91
+ test("command supports both CLI and TUI", () => {
92
+ const cmd = new TestCommand();
93
+ expect(cmd.supportsCli()).toBe(true);
94
+ expect(cmd.supportsTui()).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe("validate", () => {
99
+ test("passes for command with execute", () => {
100
+ const cmd = new TestCommand();
101
+ expect(() => cmd.validate()).not.toThrow();
102
+ });
103
+ });
104
+
105
+ describe("subcommands", () => {
106
+ test("hasSubCommands returns false when no subcommands", () => {
107
+ const cmd = new TestCommand();
108
+ expect(cmd.hasSubCommands()).toBe(false);
109
+ });
110
+
111
+ test("hasSubCommands returns true when subcommands exist", () => {
112
+ const cmd = new TestCommand();
113
+ cmd.subCommands = [new SimpleCommand()];
114
+ expect(cmd.hasSubCommands()).toBe(true);
115
+ });
116
+
117
+ test("getSubCommand finds subcommand by name", () => {
118
+ const cmd = new TestCommand();
119
+ const subCmd = new SimpleCommand();
120
+ cmd.subCommands = [subCmd];
121
+ expect(cmd.getSubCommand("simple")).toBe(subCmd);
122
+ });
123
+
124
+ test("getSubCommand returns undefined for unknown name", () => {
125
+ const cmd = new TestCommand();
126
+ cmd.subCommands = [new SimpleCommand()];
127
+ expect(cmd.getSubCommand("unknown")).toBeUndefined();
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, test, expect, afterEach } from "bun:test";
2
+ import { AppContext, type AppConfig } from "../core/context.ts";
3
+
4
+ describe("AppContext", () => {
5
+ afterEach(() => {
6
+ AppContext.clearCurrent();
7
+ });
8
+
9
+ describe("constructor", () => {
10
+ test("creates context with config", () => {
11
+ const config: AppConfig = { name: "test", version: "1.0.0" };
12
+ const ctx = new AppContext(config);
13
+ expect(ctx.config.name).toBe("test");
14
+ expect(ctx.config.version).toBe("1.0.0");
15
+ });
16
+
17
+ test("creates context with logger", () => {
18
+ const config: AppConfig = { name: "test", version: "1.0.0" };
19
+ const ctx = new AppContext(config);
20
+ expect(ctx.logger).toBeDefined();
21
+ });
22
+ });
23
+
24
+ describe("static current", () => {
25
+ test("throws when no current context", () => {
26
+ expect(() => AppContext.current).toThrow(
27
+ "AppContext.current accessed before initialization"
28
+ );
29
+ });
30
+
31
+ test("returns current context after setCurrent", () => {
32
+ const config: AppConfig = { name: "test", version: "1.0.0" };
33
+ const ctx = new AppContext(config);
34
+ AppContext.setCurrent(ctx);
35
+ expect(AppContext.current).toBe(ctx);
36
+ });
37
+
38
+ test("hasCurrent returns false when no context", () => {
39
+ expect(AppContext.hasCurrent()).toBe(false);
40
+ });
41
+
42
+ test("hasCurrent returns true after setCurrent", () => {
43
+ const config: AppConfig = { name: "test", version: "1.0.0" };
44
+ const ctx = new AppContext(config);
45
+ AppContext.setCurrent(ctx);
46
+ expect(AppContext.hasCurrent()).toBe(true);
47
+ });
48
+
49
+ test("clearCurrent clears the context", () => {
50
+ const config: AppConfig = { name: "test", version: "1.0.0" };
51
+ const ctx = new AppContext(config);
52
+ AppContext.setCurrent(ctx);
53
+ AppContext.clearCurrent();
54
+ expect(AppContext.hasCurrent()).toBe(false);
55
+ });
56
+ });
57
+
58
+ describe("services", () => {
59
+ test("setService and getService work", () => {
60
+ const config: AppConfig = { name: "test", version: "1.0.0" };
61
+ const ctx = new AppContext(config);
62
+ const service = { value: 42 };
63
+ ctx.setService("myService", service);
64
+ expect(ctx.getService<typeof service>("myService")).toBe(service);
65
+ });
66
+
67
+ test("getService returns undefined for unknown service", () => {
68
+ const config: AppConfig = { name: "test", version: "1.0.0" };
69
+ const ctx = new AppContext(config);
70
+ expect(ctx.getService("unknown")).toBeUndefined();
71
+ });
72
+
73
+ test("requireService returns service", () => {
74
+ const config: AppConfig = { name: "test", version: "1.0.0" };
75
+ const ctx = new AppContext(config);
76
+ const service = { value: 42 };
77
+ ctx.setService("myService", service);
78
+ expect(ctx.requireService<typeof service>("myService")).toBe(service);
79
+ });
80
+
81
+ test("requireService throws for unknown service", () => {
82
+ const config: AppConfig = { name: "test", version: "1.0.0" };
83
+ const ctx = new AppContext(config);
84
+ expect(() => ctx.requireService("unknown")).toThrow(
85
+ "Service 'unknown' not found"
86
+ );
87
+ });
88
+
89
+ test("hasService returns correct value", () => {
90
+ const config: AppConfig = { name: "test", version: "1.0.0" };
91
+ const ctx = new AppContext(config);
92
+ expect(ctx.hasService("myService")).toBe(false);
93
+ ctx.setService("myService", {});
94
+ expect(ctx.hasService("myService")).toBe(true);
95
+ });
96
+ });
97
+ });