@nesalia/cli 1.0.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 (103) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/dist/commands/auth/index.d.ts +4 -0
  3. package/dist/commands/auth/index.d.ts.map +1 -0
  4. package/dist/commands/auth/index.js +4 -0
  5. package/dist/commands/auth/index.js.map +1 -0
  6. package/dist/commands/auth/login.d.ts +2 -0
  7. package/dist/commands/auth/login.d.ts.map +1 -0
  8. package/dist/commands/auth/login.js +19 -0
  9. package/dist/commands/auth/login.js.map +1 -0
  10. package/dist/commands/auth/logout.d.ts +2 -0
  11. package/dist/commands/auth/logout.d.ts.map +1 -0
  12. package/dist/commands/auth/logout.js +23 -0
  13. package/dist/commands/auth/logout.js.map +1 -0
  14. package/dist/commands/auth/status.d.ts +2 -0
  15. package/dist/commands/auth/status.d.ts.map +1 -0
  16. package/dist/commands/auth/status.js +33 -0
  17. package/dist/commands/auth/status.js.map +1 -0
  18. package/dist/commands/index.d.ts +2 -0
  19. package/dist/commands/index.d.ts.map +1 -0
  20. package/dist/commands/index.js +2 -0
  21. package/dist/commands/index.js.map +1 -0
  22. package/dist/commands/post/by-id.d.ts +6 -0
  23. package/dist/commands/post/by-id.d.ts.map +1 -0
  24. package/dist/commands/post/by-id.js +32 -0
  25. package/dist/commands/post/by-id.js.map +1 -0
  26. package/dist/commands/post/create.d.ts +7 -0
  27. package/dist/commands/post/create.d.ts.map +1 -0
  28. package/dist/commands/post/create.js +35 -0
  29. package/dist/commands/post/create.js.map +1 -0
  30. package/dist/commands/post/index.d.ts +4 -0
  31. package/dist/commands/post/index.d.ts.map +1 -0
  32. package/dist/commands/post/index.js +4 -0
  33. package/dist/commands/post/index.js.map +1 -0
  34. package/dist/commands/post/list.d.ts +2 -0
  35. package/dist/commands/post/list.d.ts.map +1 -0
  36. package/dist/commands/post/list.js +35 -0
  37. package/dist/commands/post/list.js.map +1 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +22 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/lib/api/client.d.ts +17 -0
  43. package/dist/lib/api/client.d.ts.map +1 -0
  44. package/dist/lib/api/client.js +18 -0
  45. package/dist/lib/api/client.js.map +1 -0
  46. package/dist/lib/auth/client.d.ts +2748 -0
  47. package/dist/lib/auth/client.d.ts.map +1 -0
  48. package/dist/lib/auth/client.js +11 -0
  49. package/dist/lib/auth/client.js.map +1 -0
  50. package/dist/lib/auth/device-flow/config.d.ts +5 -0
  51. package/dist/lib/auth/device-flow/config.d.ts.map +1 -0
  52. package/dist/lib/auth/device-flow/config.js +5 -0
  53. package/dist/lib/auth/device-flow/config.js.map +1 -0
  54. package/dist/lib/auth/device-flow/device-code.d.ts +10 -0
  55. package/dist/lib/auth/device-flow/device-code.d.ts.map +1 -0
  56. package/dist/lib/auth/device-flow/device-code.js +27 -0
  57. package/dist/lib/auth/device-flow/device-code.js.map +1 -0
  58. package/dist/lib/auth/device-flow/errors.d.ts +8 -0
  59. package/dist/lib/auth/device-flow/errors.d.ts.map +1 -0
  60. package/dist/lib/auth/device-flow/errors.js +26 -0
  61. package/dist/lib/auth/device-flow/errors.js.map +1 -0
  62. package/dist/lib/auth/device-flow/index.d.ts +3 -0
  63. package/dist/lib/auth/device-flow/index.d.ts.map +1 -0
  64. package/dist/lib/auth/device-flow/index.js +13 -0
  65. package/dist/lib/auth/device-flow/index.js.map +1 -0
  66. package/dist/lib/auth/device-flow/polling.d.ts +4 -0
  67. package/dist/lib/auth/device-flow/polling.d.ts.map +1 -0
  68. package/dist/lib/auth/device-flow/polling.js +75 -0
  69. package/dist/lib/auth/device-flow/polling.js.map +1 -0
  70. package/dist/lib/auth/device-flow/types.d.ts +10 -0
  71. package/dist/lib/auth/device-flow/types.d.ts.map +1 -0
  72. package/dist/lib/auth/device-flow/types.js +2 -0
  73. package/dist/lib/auth/device-flow/types.js.map +1 -0
  74. package/dist/lib/auth/index.d.ts +12 -0
  75. package/dist/lib/auth/index.d.ts.map +1 -0
  76. package/dist/lib/auth/index.js +24 -0
  77. package/dist/lib/auth/index.js.map +1 -0
  78. package/dist/lib/auth/storage.d.ts +20 -0
  79. package/dist/lib/auth/storage.d.ts.map +1 -0
  80. package/dist/lib/auth/storage.js +48 -0
  81. package/dist/lib/auth/storage.js.map +1 -0
  82. package/package.json +36 -0
  83. package/src/CLAUDE.md +83 -0
  84. package/src/commands/auth/index.ts +3 -0
  85. package/src/commands/auth/login.ts +20 -0
  86. package/src/commands/auth/logout.ts +25 -0
  87. package/src/commands/auth/status.ts +37 -0
  88. package/src/commands/index.ts +1 -0
  89. package/src/index.ts +32 -0
  90. package/src/lib/api/client.ts +20 -0
  91. package/src/lib/auth/client.ts +14 -0
  92. package/src/lib/auth/device-flow/config.ts +4 -0
  93. package/src/lib/auth/device-flow/device-code.ts +39 -0
  94. package/src/lib/auth/device-flow/errors.ts +29 -0
  95. package/src/lib/auth/device-flow/index.ts +19 -0
  96. package/src/lib/auth/device-flow/polling.ts +91 -0
  97. package/src/lib/auth/device-flow/types.ts +9 -0
  98. package/src/lib/auth/index.ts +41 -0
  99. package/src/lib/auth/storage.ts +69 -0
  100. package/tests/auth.test.ts +123 -0
  101. package/tests/setup.ts +43 -0
  102. package/tsconfig.json +19 -0
  103. package/vitest.config.ts +9 -0
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { login, status, logout } from "./commands/index.js";
5
+
6
+ const program = new Command();
7
+
8
+ program
9
+ .name("nesalia")
10
+ .version("1.0.0")
11
+ .description("@nesalia/cli — Manage your account authentication");
12
+
13
+ program
14
+ .command("auth", { isDefault: false })
15
+ .description("Authentication commands")
16
+ .addCommand(
17
+ new Command("login")
18
+ .description("Login via device authorization")
19
+ .action(login),
20
+ )
21
+ .addCommand(
22
+ new Command("status")
23
+ .description("Check authentication status")
24
+ .action(status),
25
+ )
26
+ .addCommand(
27
+ new Command("logout")
28
+ .description("Logout and clear credentials")
29
+ .action(logout),
30
+ );
31
+
32
+ program.parse(process.argv);
@@ -0,0 +1,20 @@
1
+ import { createTRPCClient, httpBatchLink } from "@trpc/client";
2
+ import type { AppRouter } from "@complete-web-template/api";
3
+ import { loadCredentials } from "../auth/index.js";
4
+
5
+ const BASE_URL = process.env.CLI_AUTH_API_URL ?? "http://localhost:3000";
6
+
7
+ export const trpcClient = createTRPCClient<AppRouter>({
8
+ links: [
9
+ httpBatchLink({
10
+ url: `${BASE_URL}/api/trpc`,
11
+ headers() {
12
+ const creds = loadCredentials();
13
+ if (creds?.accessToken) {
14
+ return { Authorization: `Bearer ${creds.accessToken}` };
15
+ }
16
+ return {};
17
+ },
18
+ }),
19
+ ],
20
+ });
@@ -0,0 +1,14 @@
1
+ import { createAuthClient } from "better-auth/client";
2
+ import { deviceAuthorizationClient, organizationClient } from "better-auth/client/plugins";
3
+
4
+ const BASE_URL = process.env.CLI_AUTH_API_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000";
5
+
6
+ export const authClient = createAuthClient({
7
+ baseURL: BASE_URL,
8
+ plugins: [
9
+ deviceAuthorizationClient(),
10
+ organizationClient(),
11
+ ],
12
+ });
13
+
14
+ export type AuthClient = typeof authClient;
@@ -0,0 +1,4 @@
1
+ export const CLIENT_ID = process.env.CLI_AUTH_CLIENT_ID ?? "cli";
2
+ export const POLL_TIMEOUT_MS = 30 * 60 * 1000; // 30 min max (matches server default expiresIn)
3
+ export const SCOPE = "openid profile email";
4
+ export const MAX_NETWORK_RETRIES = 3;
@@ -0,0 +1,39 @@
1
+ import { log } from "@clack/prompts";
2
+ import open from "open";
3
+ import type { AuthClient } from "../client.js";
4
+ import { CLIENT_ID, SCOPE } from "./config.js";
5
+ import { AuthFlowError } from "./errors.js";
6
+
7
+ export interface DeviceCodeResult {
8
+ deviceCode: string;
9
+ userCode: string;
10
+ verificationUri: string;
11
+ interval: number;
12
+ }
13
+
14
+ export const requestDeviceCode = async (client: AuthClient): Promise<DeviceCodeResult> => {
15
+ log.info("Requesting device authorization...");
16
+
17
+ const { data, error } = await client.device.code({
18
+ client_id: CLIENT_ID,
19
+ scope: SCOPE,
20
+ });
21
+
22
+ if (error || !data) {
23
+ const msg = error?.error_description ?? "Failed to get device code";
24
+ throw new AuthFlowError(msg);
25
+ }
26
+
27
+ return {
28
+ deviceCode: data.device_code,
29
+ userCode: data.user_code,
30
+ verificationUri: data.verification_uri_complete,
31
+ interval: data.interval ?? 5,
32
+ };
33
+ };
34
+
35
+ export const openBrowser = async (uri: string): Promise<void> => {
36
+ await open(uri).catch(() => {
37
+ // Non-fatal: browser may fail to open, user can still use the URL
38
+ });
39
+ };
@@ -0,0 +1,29 @@
1
+ // Transient network error codes from Node.js
2
+ export const TRANSIENT_ERROR_CODES = new Set([
3
+ "ETIMEDOUT",
4
+ "ECONNRESET",
5
+ "ECONNREFUSED",
6
+ "ENOTFOUND",
7
+ "ENETUNREACH",
8
+ ]);
9
+
10
+ export const isTransientError = (error: unknown): boolean => {
11
+ if (error && typeof error === "object") {
12
+ const code = (error as { code?: string }).code;
13
+ if (code && TRANSIENT_ERROR_CODES.has(code)) return true;
14
+ }
15
+ return false;
16
+ };
17
+
18
+ // Generic auth flow error (network or oauth)
19
+ export class AuthFlowError extends Error {
20
+ constructor(
21
+ message: string,
22
+ public readonly isNetwork = false,
23
+ ) {
24
+ super(message);
25
+ this.name = "AuthFlowError";
26
+ }
27
+
28
+ static network = (msg: string): AuthFlowError => new AuthFlowError(msg, true);
29
+ }
@@ -0,0 +1,19 @@
1
+ import { log } from "@clack/prompts";
2
+ import { authClient } from "../client.js";
3
+ import { requestDeviceCode, openBrowser } from "./device-code.js";
4
+ import { pollForToken } from "./polling.js";
5
+ import type { AuthFlowResult } from "./types.js";
6
+
7
+ export const startDeviceFlow = async (): Promise<AuthFlowResult> => {
8
+ const { deviceCode, userCode, verificationUri, interval } = await requestDeviceCode(authClient);
9
+
10
+ log.message(
11
+ `Open this URL in your browser:\n ${verificationUri}\n` +
12
+ `Or enter the code: ${userCode}`
13
+ );
14
+
15
+ await openBrowser(verificationUri);
16
+ log.info(`Waiting for authorization... (polling every ${interval}s)`);
17
+
18
+ return pollForToken(authClient, deviceCode, interval);
19
+ }
@@ -0,0 +1,91 @@
1
+ import { log } from "@clack/prompts";
2
+ import type { AuthClient } from "../client.js";
3
+ import { CLIENT_ID, POLL_TIMEOUT_MS, MAX_NETWORK_RETRIES } from "./config.js";
4
+ import { isTransientError } from "./errors.js";
5
+ import type { AuthFlowResult } from "./types.js";
6
+
7
+ const sleep = (ms: number): Promise<void> =>
8
+ new Promise((resolve) => setTimeout(resolve, ms));
9
+
10
+ const resolveUser = async (client: AuthClient, accessToken: string): Promise<AuthFlowResult["user"]> => {
11
+ const response = await client.getSession({
12
+ fetchOptions: {
13
+ headers: { Authorization: `Bearer ${accessToken}` },
14
+ },
15
+ });
16
+
17
+ const user = response?.data?.user;
18
+ if (!user) {
19
+ throw new Error("Could not retrieve user session.");
20
+ }
21
+ return { id: user.id, name: user.name, email: user.email, image: user.image ?? undefined };
22
+ };
23
+
24
+ export const pollForToken = async (
25
+ client: AuthClient,
26
+ deviceCode: string,
27
+ intervalSeconds: number,
28
+ startedAt = Date.now(),
29
+ networkRetries = 0,
30
+ ): Promise<AuthFlowResult> => {
31
+ if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
32
+ throw new Error("Authorization timed out. Please try again.");
33
+ }
34
+
35
+ let data: Awaited<ReturnType<AuthClient["device"]["token"]>>["data"];
36
+ let error: Awaited<ReturnType<AuthClient["device"]["token"]>>["error"];
37
+
38
+ try {
39
+ const result = await client.device.token({
40
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
41
+ device_code: deviceCode,
42
+ client_id: CLIENT_ID,
43
+ });
44
+ data = result.data;
45
+ error = result.error;
46
+ } catch (err) {
47
+ if (isTransientError(err)) {
48
+ const retries = networkRetries + 1;
49
+ if (retries > MAX_NETWORK_RETRIES) {
50
+ throw new Error(`Network error after ${MAX_NETWORK_RETRIES} retries. Check your connection.`);
51
+ }
52
+ log.warn(`Network error during polling — retry ${retries}/${MAX_NETWORK_RETRIES}.`);
53
+ await sleep(intervalSeconds * 1000);
54
+ return pollForToken(client, deviceCode, intervalSeconds, startedAt, retries);
55
+ }
56
+ throw err;
57
+ }
58
+
59
+ if (data?.access_token) {
60
+ log.success("Authorization successful!");
61
+ const user = await resolveUser(client, data.access_token);
62
+ if (!user.id) {
63
+ throw new Error("Could not retrieve user information. Please try again.");
64
+ }
65
+ log.success(`Connected as ${user.name || user.email || "user"}`);
66
+ return { accessToken: data.access_token, user };
67
+ }
68
+
69
+ if (error) {
70
+ switch (error.error) {
71
+ case "authorization_pending":
72
+ await sleep(intervalSeconds * 1000);
73
+ return pollForToken(client, deviceCode, intervalSeconds, startedAt);
74
+ case "slow_down":
75
+ const newInterval = intervalSeconds + 5;
76
+ log.warn(`Slowing down polling to ${newInterval}s`);
77
+ await sleep(newInterval * 1000);
78
+ return pollForToken(client, deviceCode, newInterval, startedAt);
79
+ case "access_denied":
80
+ throw new Error("Authorization was denied.");
81
+ case "expired_token":
82
+ throw new Error("The code expired. Please try again.");
83
+ default:
84
+ throw new Error(error.error_description ?? `Unexpected error: ${error.error}`);
85
+ }
86
+ }
87
+
88
+ // No data and no error — should not happen, but guard anyway
89
+ await sleep(intervalSeconds * 1000);
90
+ return pollForToken(client, deviceCode, intervalSeconds, startedAt);
91
+ };
@@ -0,0 +1,9 @@
1
+ export type AuthFlowResult = {
2
+ accessToken: string;
3
+ user: {
4
+ id: string;
5
+ name: string;
6
+ email: string;
7
+ image?: string;
8
+ };
9
+ };
@@ -0,0 +1,41 @@
1
+ // Re-export everything from submodules
2
+ export { authClient } from "./client.js";
3
+ export type { AuthClient } from "./client.js";
4
+
5
+ export { startDeviceFlow } from "./device-flow/index.js";
6
+ export type { AuthFlowResult } from "./device-flow/types.js";
7
+
8
+ export {
9
+ saveCredentials,
10
+ loadCredentials,
11
+ clearCredentials,
12
+ isExpired,
13
+ requireAuth,
14
+ type StoredCredentials,
15
+ } from "./storage.js";
16
+
17
+ import { log } from "@clack/prompts";
18
+ import { loadCredentials, clearCredentials, isExpired, type StoredCredentials } from "./storage.js";
19
+
20
+ /**
21
+ * HOF - wraps an async operation requiring authentication.
22
+ * Checks credentials before execution, exits with error message if not authed.
23
+ */
24
+ export async function withAuth<T>(
25
+ fn: (credentials: StoredCredentials) => Promise<T>,
26
+ ): Promise<T> {
27
+ const credentials = loadCredentials();
28
+
29
+ if (!credentials) {
30
+ log.error("Not logged in. Run 'auth login' first.");
31
+ process.exit(1);
32
+ }
33
+
34
+ if (isExpired(credentials)) {
35
+ log.error("Session expired. Run 'auth login' again.");
36
+ clearCredentials();
37
+ process.exit(1);
38
+ }
39
+
40
+ return fn(credentials);
41
+ }
@@ -0,0 +1,69 @@
1
+ import Conf from "conf";
2
+ import { log } from "@clack/prompts";
3
+
4
+ export type StoredCredentials = {
5
+ accessToken: string;
6
+ user: {
7
+ id: string;
8
+ email: string;
9
+ name: string;
10
+ image?: string;
11
+ };
12
+ expiresAt: number;
13
+ };
14
+
15
+ // Support test configuration via environment variable
16
+ const configPath = process.env.CLI_AUTH_CONFIG_PATH;
17
+
18
+ export const storage = new Conf<{ credentials: StoredCredentials | null }>({
19
+ projectName: "complete-web-template",
20
+ configName: "auth",
21
+ cwd: configPath, // Use custom path if set (for tests)
22
+ defaults: {
23
+ credentials: null,
24
+ },
25
+ });
26
+
27
+ export function saveCredentials(credentials: StoredCredentials): void {
28
+ storage.set("credentials", credentials);
29
+ }
30
+
31
+ export function loadCredentials(): StoredCredentials | null {
32
+ const raw = storage.get("credentials");
33
+
34
+ // Guard against corrupted storage (e.g., old version, partial write)
35
+ if (
36
+ !raw ||
37
+ typeof raw !== "object" ||
38
+ !("accessToken" in raw) ||
39
+ !("user" in raw) ||
40
+ !("expiresAt" in raw)
41
+ ) {
42
+ clearCredentials();
43
+ return null;
44
+ }
45
+
46
+ return raw as StoredCredentials;
47
+ }
48
+
49
+ export function clearCredentials(): void {
50
+ storage.delete("credentials");
51
+ }
52
+
53
+ export function isExpired(credentials: StoredCredentials): boolean {
54
+ return Date.now() > credentials.expiresAt;
55
+ }
56
+
57
+ export function requireAuth(): StoredCredentials {
58
+ const credentials = loadCredentials();
59
+ if (!credentials) {
60
+ log.error("Not logged in. Run 'auth login' first.");
61
+ process.exit(1);
62
+ }
63
+ if (isExpired(credentials)) {
64
+ log.error("Session expired. Run 'auth login' again.");
65
+ clearCredentials();
66
+ process.exit(1);
67
+ }
68
+ return credentials;
69
+ }
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { status, logout } from "../src/commands/auth";
3
+ import { loadCredentials, clearCredentials, saveCredentials } from "../src/lib/auth/storage";
4
+ import { testServer } from "./setup";
5
+
6
+ // Mock @clack/prompts before importing auth commands
7
+ vi.mock("@clack/prompts", () => ({
8
+ log: {
9
+ info: vi.fn(),
10
+ success: vi.fn(),
11
+ warn: vi.fn(),
12
+ error: vi.fn(),
13
+ message: vi.fn(),
14
+ },
15
+ }));
16
+
17
+ // Mock open before importing device-flow
18
+ vi.mock("../src/lib/auth/device-flow", async () => {
19
+ const actual = await vi.importActual("../src/lib/auth/device-flow");
20
+ return {
21
+ ...actual,
22
+ startDeviceFlow: vi.fn().mockImplementation(async () => ({
23
+ accessToken: "test-access-token",
24
+ user: {
25
+ id: "test-user-id",
26
+ name: "Test User",
27
+ email: "test@example.com",
28
+ },
29
+ })),
30
+ };
31
+ });
32
+
33
+ // Import login AFTER mocking
34
+ const { login } = await import("../src/commands/auth");
35
+
36
+ describe("CLI Auth Commands", () => {
37
+ describe("login", () => {
38
+ it("should login successfully with device flow", async () => {
39
+ // Mock open to prevent browser from actually opening
40
+ const openMock = vi.fn();
41
+ vi.stubGlobal("open", Object.assign(openMock, { default: openMock }));
42
+
43
+ // Start login - it uses mocked startDeviceFlow internally
44
+ await login();
45
+
46
+ // Verify credentials were stored
47
+ const creds = loadCredentials();
48
+ expect(creds).toBeDefined();
49
+ expect(creds?.accessToken).toBe("test-access-token");
50
+ expect(creds?.user).toBeDefined();
51
+ expect(creds?.user.email).toBe("test@example.com");
52
+
53
+ vi.unstubAllGlobals();
54
+ });
55
+ });
56
+
57
+ describe("status", () => {
58
+ it("should show not logged in message when no credentials", async () => {
59
+ const { log } = await import("@clack/prompts");
60
+ clearCredentials();
61
+
62
+ await status();
63
+
64
+ expect(log.info).toHaveBeenCalledWith(
65
+ "Not logged in. Run 'auth login' to authenticate."
66
+ );
67
+ });
68
+
69
+ it("should show logged in message when credentials exist", async () => {
70
+ const { log } = await import("@clack/prompts");
71
+
72
+ // Simulate logged in state
73
+ saveCredentials({
74
+ accessToken: "test-token",
75
+ user: { id: "1", name: "Test User", email: "test@example.com" },
76
+ expiresAt: Date.now() + 86400000, // 1 day from now
77
+ });
78
+
79
+ // Mock the fetch call for session verification
80
+ const fetchMock = vi.fn().mockResolvedValue(
81
+ new Response(JSON.stringify({ user: { name: "Test User", email: "test@example.com" } }), {
82
+ status: 200,
83
+ headers: { "Content-Type": "application/json" },
84
+ })
85
+ );
86
+ vi.stubGlobal("fetch", fetchMock);
87
+
88
+ await status();
89
+
90
+ expect(log.success).toHaveBeenCalledWith(
91
+ expect.stringContaining("Test User")
92
+ );
93
+ vi.unstubAllGlobals();
94
+ });
95
+ });
96
+
97
+ describe("logout", () => {
98
+ it("should show not logged in when no credentials", async () => {
99
+ const { log } = await import("@clack/prompts");
100
+ clearCredentials();
101
+
102
+ await logout();
103
+
104
+ expect(log.info).toHaveBeenCalledWith("Not logged in.");
105
+ });
106
+
107
+ it("should clear credentials and confirm logout", async () => {
108
+ const { log } = await import("@clack/prompts");
109
+
110
+ // Set up credentials
111
+ saveCredentials({
112
+ accessToken: "test-token",
113
+ user: { id: "1", name: "Test User", email: "test@example.com" },
114
+ expiresAt: Date.now() + 86400000,
115
+ });
116
+
117
+ await logout();
118
+
119
+ expect(log.success).toHaveBeenCalledWith("Successfully logged out.");
120
+ expect(loadCredentials() ?? null).toBeNull();
121
+ });
122
+ });
123
+ });
package/tests/setup.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { createTestServerWithAuth } from "@complete-web-template/test-utils";
2
+ import { createTempDir, mockOpen, restoreOpen } from "@complete-web-template/test-utils";
3
+ import { clearCredentials } from "../src/lib/auth/storage";
4
+ import type { TestServer } from "@complete-web-template/test-utils";
5
+
6
+ // Test server instance
7
+ export let testServer: Awaited<ReturnType<typeof createTestServerWithAuth>> | null = null;
8
+
9
+ // Temp directory for config
10
+ export let tempDir: { configPath: string; cleanup: () => void };
11
+
12
+ // Set up test environment before all tests
13
+ beforeAll(async () => {
14
+ // Create temp directory for config storage
15
+ tempDir = createTempDir();
16
+ process.env.CLI_AUTH_CONFIG_PATH = tempDir.configPath;
17
+
18
+ // Start test server with auth
19
+ testServer = await createTestServerWithAuth();
20
+
21
+ // Set API URL for CLI
22
+ process.env.CLI_AUTH_API_URL = testServer.baseUrl;
23
+
24
+ // Mock open to prevent browser launching
25
+ mockOpen();
26
+ });
27
+
28
+ // Clean up after all tests
29
+ afterAll(async () => {
30
+ if (testServer) {
31
+ await testServer.close();
32
+ }
33
+ restoreOpen();
34
+ tempDir.cleanup();
35
+ delete process.env.CLI_AUTH_CONFIG_PATH;
36
+ delete process.env.CLI_AUTH_API_URL;
37
+ });
38
+
39
+ // Clean up before each test (reset credentials)
40
+ beforeEach(() => {
41
+ // Reset the config storage between tests
42
+ clearCredentials();
43
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2020"],
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ setupFiles: ["./tests/setup.ts"],
7
+ pool: "forks",
8
+ },
9
+ });