@geekmidas/cli 0.9.0 → 0.12.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 (146) hide show
  1. package/README.md +525 -0
  2. package/dist/bundler-DRXCw_YR.mjs +70 -0
  3. package/dist/bundler-DRXCw_YR.mjs.map +1 -0
  4. package/dist/bundler-WsEvH_b2.cjs +71 -0
  5. package/dist/bundler-WsEvH_b2.cjs.map +1 -0
  6. package/dist/{config-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +2 -1
  12. package/dist/config.d.cts.map +1 -0
  13. package/dist/config.d.mts +2 -1
  14. package/dist/config.d.mts.map +1 -0
  15. package/dist/config.mjs +1 -1
  16. package/dist/encryption-C8H-38Yy.mjs +42 -0
  17. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  18. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  19. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  20. package/dist/index.cjs +2125 -184
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.mjs +2143 -197
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
  25. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  26. package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
  27. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  28. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  29. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  30. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  31. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  32. package/dist/openapi-react-query.cjs +1 -1
  33. package/dist/openapi-react-query.d.cts.map +1 -0
  34. package/dist/openapi-react-query.d.mts.map +1 -0
  35. package/dist/openapi-react-query.mjs +1 -1
  36. package/dist/openapi.cjs +2 -2
  37. package/dist/openapi.d.cts +1 -1
  38. package/dist/openapi.d.cts.map +1 -0
  39. package/dist/openapi.d.mts +1 -1
  40. package/dist/openapi.d.mts.map +1 -0
  41. package/dist/openapi.mjs +2 -2
  42. package/dist/storage-BUYQJgz7.cjs +4 -0
  43. package/dist/storage-BXoJvmv2.cjs +149 -0
  44. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  45. package/dist/storage-C9PU_30f.mjs +101 -0
  46. package/dist/storage-C9PU_30f.mjs.map +1 -0
  47. package/dist/storage-DLJAYxzJ.mjs +3 -0
  48. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  49. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  50. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  51. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  52. package/examples/cron-example.ts +27 -27
  53. package/examples/env.ts +27 -27
  54. package/examples/function-example.ts +31 -31
  55. package/examples/gkm.config.json +20 -20
  56. package/examples/gkm.config.ts +8 -8
  57. package/examples/gkm.minimal.config.json +5 -5
  58. package/examples/gkm.production.config.json +25 -25
  59. package/examples/logger.ts +2 -2
  60. package/package.json +6 -6
  61. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  62. package/src/__tests__/config.spec.ts +55 -55
  63. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  64. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  65. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  66. package/src/__tests__/openapi.spec.ts +428 -428
  67. package/src/__tests__/test-helpers.ts +77 -76
  68. package/src/auth/__tests__/credentials.spec.ts +204 -0
  69. package/src/auth/__tests__/index.spec.ts +168 -0
  70. package/src/auth/credentials.ts +187 -0
  71. package/src/auth/index.ts +226 -0
  72. package/src/build/__tests__/index-new.spec.ts +474 -474
  73. package/src/build/__tests__/manifests.spec.ts +333 -333
  74. package/src/build/bundler.ts +141 -0
  75. package/src/build/endpoint-analyzer.ts +236 -0
  76. package/src/build/handler-templates.ts +1253 -0
  77. package/src/build/index.ts +250 -179
  78. package/src/build/manifests.ts +52 -52
  79. package/src/build/providerResolver.ts +145 -145
  80. package/src/build/types.ts +64 -43
  81. package/src/config.ts +39 -37
  82. package/src/deploy/__tests__/docker.spec.ts +111 -0
  83. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  84. package/src/deploy/__tests__/init.spec.ts +662 -0
  85. package/src/deploy/docker.ts +128 -0
  86. package/src/deploy/dokploy.ts +204 -0
  87. package/src/deploy/index.ts +136 -0
  88. package/src/deploy/init.ts +484 -0
  89. package/src/deploy/types.ts +48 -0
  90. package/src/dev/__tests__/index.spec.ts +266 -266
  91. package/src/dev/index.ts +647 -593
  92. package/src/docker/__tests__/compose.spec.ts +531 -0
  93. package/src/docker/__tests__/templates.spec.ts +280 -0
  94. package/src/docker/compose.ts +273 -0
  95. package/src/docker/index.ts +230 -0
  96. package/src/docker/templates.ts +446 -0
  97. package/src/generators/CronGenerator.ts +72 -72
  98. package/src/generators/EndpointGenerator.ts +699 -398
  99. package/src/generators/FunctionGenerator.ts +84 -84
  100. package/src/generators/Generator.ts +72 -72
  101. package/src/generators/OpenApiTsGenerator.ts +589 -589
  102. package/src/generators/SubscriberGenerator.ts +124 -124
  103. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  104. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  105. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  106. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  107. package/src/generators/index.ts +4 -4
  108. package/src/index.ts +628 -206
  109. package/src/init/__tests__/generators.spec.ts +334 -334
  110. package/src/init/__tests__/init.spec.ts +332 -332
  111. package/src/init/__tests__/utils.spec.ts +89 -89
  112. package/src/init/generators/config.ts +175 -175
  113. package/src/init/generators/docker.ts +41 -41
  114. package/src/init/generators/env.ts +72 -72
  115. package/src/init/generators/index.ts +1 -1
  116. package/src/init/generators/models.ts +64 -64
  117. package/src/init/generators/monorepo.ts +161 -161
  118. package/src/init/generators/package.ts +71 -71
  119. package/src/init/generators/source.ts +6 -6
  120. package/src/init/index.ts +203 -208
  121. package/src/init/templates/api.ts +115 -115
  122. package/src/init/templates/index.ts +75 -75
  123. package/src/init/templates/minimal.ts +98 -98
  124. package/src/init/templates/serverless.ts +89 -89
  125. package/src/init/templates/worker.ts +98 -98
  126. package/src/init/utils.ts +54 -56
  127. package/src/openapi-react-query.ts +194 -194
  128. package/src/openapi.ts +63 -63
  129. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  130. package/src/secrets/__tests__/generator.spec.ts +319 -0
  131. package/src/secrets/__tests__/index.spec.ts +91 -0
  132. package/src/secrets/__tests__/storage.spec.ts +403 -0
  133. package/src/secrets/encryption.ts +91 -0
  134. package/src/secrets/generator.ts +164 -0
  135. package/src/secrets/index.ts +383 -0
  136. package/src/secrets/storage.ts +134 -0
  137. package/src/secrets/types.ts +53 -0
  138. package/src/types.ts +295 -176
  139. package/tsconfig.json +9 -0
  140. package/tsdown.config.ts +11 -8
  141. package/dist/config-Bq72aj8e.mjs.map +0 -1
  142. package/dist/config-CFls09Ey.cjs.map +0 -1
  143. package/dist/openapi--vOy9mo4.mjs.map +0 -1
  144. package/dist/openapi-CHhTPief.cjs.map +0 -1
  145. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  146. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
@@ -2,8 +2,8 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import {
5
- CronBuilder,
6
- type ScheduleExpression,
5
+ CronBuilder,
6
+ type ScheduleExpression,
7
7
  } from '@geekmidas/constructs/crons';
8
8
  import { e } from '@geekmidas/constructs/endpoints';
9
9
  import { z } from 'zod';
@@ -12,50 +12,50 @@ import { z } from 'zod';
12
12
  * Creates a temporary directory for testing
13
13
  */
14
14
  export async function createTempDir(prefix = 'cli-test-'): Promise<string> {
15
- const tempPath = join(
16
- tmpdir(),
17
- `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`,
18
- );
19
- await mkdir(tempPath, { recursive: true });
20
- return tempPath;
15
+ const tempPath = join(
16
+ tmpdir(),
17
+ `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`,
18
+ );
19
+ await mkdir(tempPath, { recursive: true });
20
+ return tempPath;
21
21
  }
22
22
 
23
23
  /**
24
24
  * Cleans up a directory
25
25
  */
26
26
  export async function cleanupDir(path: string): Promise<void> {
27
- try {
28
- await rm(path, { recursive: true, force: true });
29
- } catch (error) {
30
- // Ignore errors during cleanup
31
- }
27
+ try {
28
+ await rm(path, { recursive: true, force: true });
29
+ } catch (_error) {
30
+ // Ignore errors during cleanup
31
+ }
32
32
  }
33
33
 
34
34
  /**
35
35
  * Creates a test file with content
36
36
  */
37
37
  export async function createTestFile(
38
- dir: string,
39
- filename: string,
40
- content: string,
38
+ dir: string,
39
+ filename: string,
40
+ content: string,
41
41
  ): Promise<string> {
42
- const filePath = join(dir, filename);
43
- await mkdir(dirname(filePath), { recursive: true });
44
- await writeFile(filePath, content);
45
- return filePath;
42
+ const filePath = join(dir, filename);
43
+ await mkdir(dirname(filePath), { recursive: true });
44
+ await writeFile(filePath, content);
45
+ return filePath;
46
46
  }
47
47
 
48
48
  /**
49
49
  * Creates a mock endpoint file with real endpoint construct
50
50
  */
51
51
  export async function createMockEndpointFile(
52
- dir: string,
53
- filename: string,
54
- exportName: string,
55
- path: string = '/test',
56
- method: string = 'GET',
52
+ dir: string,
53
+ filename: string,
54
+ exportName: string,
55
+ path: string = '/test',
56
+ method: string = 'GET',
57
57
  ): Promise<string> {
58
- const content = `
58
+ const content = `
59
59
  import { e } from '@geekmidas/constructs/endpoints';
60
60
  import { z } from 'zod';
61
61
 
@@ -64,19 +64,19 @@ export const ${exportName} = e
64
64
  .output(z.object({ message: z.string() }))
65
65
  .handle(async () => ({ message: 'Hello from ${exportName}' }));
66
66
  `;
67
- return createTestFile(dir, filename, content);
67
+ return createTestFile(dir, filename, content);
68
68
  }
69
69
 
70
70
  /**
71
71
  * Creates a mock function file with real function construct
72
72
  */
73
73
  export async function createMockFunctionFile(
74
- dir: string,
75
- filename: string,
76
- exportName: string,
77
- timeout = 30,
74
+ dir: string,
75
+ filename: string,
76
+ exportName: string,
77
+ timeout = 30,
78
78
  ): Promise<string> {
79
- const content = `
79
+ const content = `
80
80
  import { f } from '@geekmidas/constructs/functions';
81
81
  import { z } from 'zod';
82
82
 
@@ -86,19 +86,19 @@ export const ${exportName} = f
86
86
  .timeout(${timeout})
87
87
  .handle(async ({ input }) => ({ greeting: \`Hello, \${input.name}!\` }));
88
88
  `;
89
- return createTestFile(dir, filename, content);
89
+ return createTestFile(dir, filename, content);
90
90
  }
91
91
 
92
92
  /**
93
93
  * Creates a mock cron file with real cron construct
94
94
  */
95
95
  export async function createMockCronFile(
96
- dir: string,
97
- filename: string,
98
- exportName: string,
99
- schedule = 'rate(1 hour)',
96
+ dir: string,
97
+ filename: string,
98
+ exportName: string,
99
+ schedule = 'rate(1 hour)',
100
100
  ): Promise<string> {
101
- const content = `
101
+ const content = `
102
102
  import { CronBuilder } from '@geekmidas/constructs/crons';
103
103
  import { z } from 'zod';
104
104
 
@@ -110,71 +110,72 @@ export const ${exportName} = new CronBuilder()
110
110
  return { processed: 10 };
111
111
  });
112
112
  `;
113
- return createTestFile(dir, filename, content);
113
+ return createTestFile(dir, filename, content);
114
114
  }
115
115
 
116
116
  /**
117
117
  * Helper functions to create real constructs for testing
118
118
  */
119
119
  export function createTestEndpoint(path: string, method: HttpMethod = 'GET') {
120
- const m = method.toLowerCase() as Lowercase<HttpMethod>;
121
- const builder = e[m](path);
122
- builder.output(z.object({ message: z.string() }));
123
- return builder.handle(async () => ({ message: `Hello from ${path}` }));
120
+ const m = method.toLowerCase() as Lowercase<HttpMethod>;
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ const builder = (e as any)[m](path);
123
+ builder.output(z.object({ message: z.string() }));
124
+ return builder.handle(async () => ({ message: `Hello from ${path}` }));
124
125
  }
125
126
 
126
127
  export function createTestFunction(timeout: number = 30) {
127
- const builder = new FunctionBuilder();
128
- builder.input(z.object({ name: z.string() }));
129
- builder.output(z.object({ greeting: z.string() }));
130
- builder.timeout(timeout);
131
- return builder.handle(async ({ input }: any) => ({
132
- greeting: `Hello, ${input.name}!`,
133
- }));
128
+ const builder = new FunctionBuilder();
129
+ builder.input(z.object({ name: z.string() }));
130
+ builder.output(z.object({ greeting: z.string() }));
131
+ builder.timeout(timeout);
132
+ return builder.handle(async ({ input }: any) => ({
133
+ greeting: `Hello, ${input.name}!`,
134
+ }));
134
135
  }
135
136
 
136
137
  export function createTestCron(
137
- schedule: ScheduleExpression = 'rate(1 hour)',
138
- timeout: number = 30,
138
+ schedule: ScheduleExpression = 'rate(1 hour)',
139
+ timeout: number = 30,
139
140
  ) {
140
- const builder = new CronBuilder();
141
- builder.schedule(schedule);
142
- builder.output(z.object({ processed: z.number() }));
143
- builder.timeout(timeout);
144
- return builder.handle(async () => {
145
- return { processed: 10 };
146
- });
141
+ const builder = new CronBuilder();
142
+ builder.schedule(schedule);
143
+ builder.output(z.object({ processed: z.number() }));
144
+ builder.timeout(timeout);
145
+ return builder.handle(async () => {
146
+ return { processed: 10 };
147
+ });
147
148
  }
148
149
 
149
150
  /**
150
151
  * Creates a mock build context
151
152
  */
152
153
  export function createMockBuildContext() {
153
- return {
154
- envParserPath: './env',
155
- envParserImportPattern: 'envParser',
156
- loggerPath: './logger',
157
- loggerImportPattern: 'logger',
158
- };
154
+ return {
155
+ envParserPath: './env',
156
+ envParserImportPattern: 'envParser',
157
+ loggerPath: './logger',
158
+ loggerImportPattern: 'logger',
159
+ };
159
160
  }
160
161
 
161
162
  /**
162
163
  * Waits for a condition to be true
163
164
  */
164
165
  export async function waitFor(
165
- condition: () => boolean,
166
- timeout = 5000,
167
- interval = 100,
166
+ condition: () => boolean,
167
+ timeout = 5000,
168
+ interval = 100,
168
169
  ): Promise<void> {
169
- const start = Date.now();
170
- while (!condition() && Date.now() - start < timeout) {
171
- await new Promise((resolve) => setTimeout(resolve, interval));
172
- }
173
- if (!condition()) {
174
- throw new Error('Timeout waiting for condition');
175
- }
170
+ const start = Date.now();
171
+ while (!condition() && Date.now() - start < timeout) {
172
+ await new Promise((resolve) => setTimeout(resolve, interval));
173
+ }
174
+ if (!condition()) {
175
+ throw new Error('Timeout waiting for condition');
176
+ }
176
177
  }
177
178
 
178
179
  import { dirname } from 'node:path';
179
180
  import { FunctionBuilder } from '@geekmidas/constructs/functions';
180
- import type { HttpMethod } from '../../../api/src/constructs/types';
181
+ import type { HttpMethod } from '@geekmidas/constructs/types';
@@ -0,0 +1,204 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import {
8
+ getCredentialsDir,
9
+ getCredentialsPath,
10
+ getDokployCredentials,
11
+ getDokployToken,
12
+ readCredentials,
13
+ removeDokployCredentials,
14
+ storeDokployCredentials,
15
+ writeCredentials,
16
+ } from '../credentials';
17
+
18
+ describe('credentials storage', () => {
19
+ let tempDir: string;
20
+
21
+ beforeEach(async () => {
22
+ tempDir = join(tmpdir(), `gkm-auth-test-${Date.now()}`);
23
+ await mkdir(tempDir, { recursive: true });
24
+ });
25
+
26
+ afterEach(async () => {
27
+ if (existsSync(tempDir)) {
28
+ await rm(tempDir, { recursive: true });
29
+ }
30
+ });
31
+
32
+ describe('path utilities', () => {
33
+ it('should return credentials dir in specified root directory', () => {
34
+ const dir = getCredentialsDir({ root: tempDir });
35
+ expect(dir).toBe(join(tempDir, '.gkm'));
36
+ });
37
+
38
+ it('should return credentials path', () => {
39
+ const path = getCredentialsPath({ root: tempDir });
40
+ expect(path).toBe(join(tempDir, '.gkm', 'credentials.json'));
41
+ });
42
+ });
43
+
44
+ describe('readCredentials / writeCredentials', () => {
45
+ it('should return empty object when no credentials file exists', async () => {
46
+ const creds = await readCredentials({ root: tempDir });
47
+ expect(creds).toEqual({});
48
+ });
49
+
50
+ it('should write and read credentials', async () => {
51
+ const credentials = {
52
+ dokploy: {
53
+ token: 'test-token',
54
+ endpoint: 'https://dokploy.example.com',
55
+ storedAt: new Date().toISOString(),
56
+ },
57
+ };
58
+
59
+ await writeCredentials(credentials, { root: tempDir });
60
+ const read = await readCredentials({ root: tempDir });
61
+
62
+ expect(read).toEqual(credentials);
63
+ });
64
+
65
+ it('should create credentials directory if it does not exist', async () => {
66
+ const credentials = {
67
+ dokploy: {
68
+ token: 'test',
69
+ endpoint: 'https://test.com',
70
+ storedAt: new Date().toISOString(),
71
+ },
72
+ };
73
+
74
+ await writeCredentials(credentials, { root: tempDir });
75
+
76
+ expect(existsSync(join(tempDir, '.gkm'))).toBe(true);
77
+ expect(existsSync(join(tempDir, '.gkm', 'credentials.json'))).toBe(true);
78
+ });
79
+
80
+ it('should set secure file permissions', async () => {
81
+ const credentials = {
82
+ dokploy: {
83
+ token: 'test',
84
+ endpoint: 'https://test.com',
85
+ storedAt: new Date().toISOString(),
86
+ },
87
+ };
88
+
89
+ await writeCredentials(credentials, { root: tempDir });
90
+
91
+ // Verify the file was created (we can't easily check permissions in tests)
92
+ const content = await readFile(
93
+ getCredentialsPath({ root: tempDir }),
94
+ 'utf-8',
95
+ );
96
+ expect(JSON.parse(content)).toEqual(credentials);
97
+ });
98
+ });
99
+
100
+ describe('storeDokployCredentials', () => {
101
+ it('should store dokploy credentials', async () => {
102
+ await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
103
+ root: tempDir,
104
+ });
105
+
106
+ const creds = await readCredentials({ root: tempDir });
107
+ expect(creds.dokploy).toBeDefined();
108
+ expect(creds.dokploy!.token).toBe('my-token');
109
+ expect(creds.dokploy!.endpoint).toBe('https://dokploy.example.com');
110
+ expect(creds.dokploy!.storedAt).toBeDefined();
111
+ });
112
+
113
+ it('should overwrite existing credentials', async () => {
114
+ await storeDokployCredentials('old-token', 'https://old.com', {
115
+ root: tempDir,
116
+ });
117
+ await storeDokployCredentials('new-token', 'https://new.com', {
118
+ root: tempDir,
119
+ });
120
+
121
+ const creds = await getDokployCredentials({ root: tempDir });
122
+ expect(creds!.token).toBe('new-token');
123
+ expect(creds!.endpoint).toBe('https://new.com');
124
+ });
125
+ });
126
+
127
+ describe('getDokployCredentials', () => {
128
+ it('should return null when no credentials stored', async () => {
129
+ const creds = await getDokployCredentials({ root: tempDir });
130
+ expect(creds).toBeNull();
131
+ });
132
+
133
+ it('should return stored credentials', async () => {
134
+ await storeDokployCredentials('test-token', 'https://test.com', {
135
+ root: tempDir,
136
+ });
137
+
138
+ const creds = await getDokployCredentials({ root: tempDir });
139
+ expect(creds).toEqual({
140
+ token: 'test-token',
141
+ endpoint: 'https://test.com',
142
+ });
143
+ });
144
+ });
145
+
146
+ describe('removeDokployCredentials', () => {
147
+ it('should return false when no credentials to remove', async () => {
148
+ const removed = await removeDokployCredentials({ root: tempDir });
149
+ expect(removed).toBe(false);
150
+ });
151
+
152
+ it('should remove dokploy credentials', async () => {
153
+ await storeDokployCredentials('test-token', 'https://test.com', {
154
+ root: tempDir,
155
+ });
156
+
157
+ const removed = await removeDokployCredentials({ root: tempDir });
158
+ expect(removed).toBe(true);
159
+
160
+ const creds = await getDokployCredentials({ root: tempDir });
161
+ expect(creds).toBeNull();
162
+ });
163
+ });
164
+
165
+ describe('getDokployToken', () => {
166
+ it('should return null when no token available', async () => {
167
+ const token = await getDokployToken({ root: tempDir });
168
+ expect(token).toBeNull();
169
+ });
170
+
171
+ it('should return stored token', async () => {
172
+ await storeDokployCredentials('stored-token', 'https://test.com', {
173
+ root: tempDir,
174
+ });
175
+
176
+ const token = await getDokployToken({ root: tempDir });
177
+ expect(token).toBe('stored-token');
178
+ });
179
+
180
+ it('should prefer environment variable over stored token', async () => {
181
+ await storeDokployCredentials('stored-token', 'https://test.com', {
182
+ root: tempDir,
183
+ });
184
+ process.env.DOKPLOY_API_TOKEN = 'env-token';
185
+
186
+ try {
187
+ const token = await getDokployToken({ root: tempDir });
188
+ expect(token).toBe('env-token');
189
+ } finally {
190
+ delete process.env.DOKPLOY_API_TOKEN;
191
+ }
192
+ });
193
+
194
+ it('should fall back to stored token when env var not set', async () => {
195
+ delete process.env.DOKPLOY_API_TOKEN;
196
+ await storeDokployCredentials('stored-token', 'https://test.com', {
197
+ root: tempDir,
198
+ });
199
+
200
+ const token = await getDokployToken({ root: tempDir });
201
+ expect(token).toBe('stored-token');
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,168 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { HttpResponse, http } from 'msw';
6
+ import { setupServer } from 'msw/node';
7
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
8
+
9
+ import {
10
+ getDokployCredentials,
11
+ removeDokployCredentials,
12
+ storeDokployCredentials,
13
+ } from '../credentials';
14
+ import { maskToken, validateDokployToken } from '../index';
15
+
16
+ // MSW server for mocking API calls
17
+ const server = setupServer();
18
+
19
+ describe('auth commands', () => {
20
+ let tempDir: string;
21
+
22
+ beforeEach(async () => {
23
+ tempDir = join(tmpdir(), `gkm-auth-cmd-test-${Date.now()}`);
24
+ await mkdir(tempDir, { recursive: true });
25
+ server.listen({ onUnhandledRequest: 'bypass' });
26
+ });
27
+
28
+ afterEach(async () => {
29
+ server.resetHandlers();
30
+ server.close();
31
+ if (existsSync(tempDir)) {
32
+ await rm(tempDir, { recursive: true });
33
+ }
34
+ });
35
+
36
+ describe('validateDokployToken', () => {
37
+ it('should return true for valid token', async () => {
38
+ server.use(
39
+ http.get('https://dokploy.example.com/api/project.all', () => {
40
+ return HttpResponse.json([{ projectId: 'proj_1', name: 'Test' }]);
41
+ }),
42
+ );
43
+
44
+ const result = await validateDokployToken(
45
+ 'https://dokploy.example.com',
46
+ 'valid-token',
47
+ );
48
+
49
+ expect(result).toBe(true);
50
+ });
51
+
52
+ it('should return false for invalid token (401)', async () => {
53
+ server.use(
54
+ http.get('https://dokploy.example.com/api/project.all', () => {
55
+ return new HttpResponse(null, { status: 401 });
56
+ }),
57
+ );
58
+
59
+ const result = await validateDokployToken(
60
+ 'https://dokploy.example.com',
61
+ 'invalid-token',
62
+ );
63
+
64
+ expect(result).toBe(false);
65
+ });
66
+
67
+ it('should return false for server error (500)', async () => {
68
+ server.use(
69
+ http.get('https://dokploy.example.com/api/project.all', () => {
70
+ return new HttpResponse(null, { status: 500 });
71
+ }),
72
+ );
73
+
74
+ const result = await validateDokployToken(
75
+ 'https://dokploy.example.com',
76
+ 'token',
77
+ );
78
+
79
+ expect(result).toBe(false);
80
+ });
81
+
82
+ it('should return false on network error', async () => {
83
+ server.use(
84
+ http.get('https://dokploy.example.com/api/project.all', () => {
85
+ return HttpResponse.error();
86
+ }),
87
+ );
88
+
89
+ const result = await validateDokployToken(
90
+ 'https://dokploy.example.com',
91
+ 'token',
92
+ );
93
+
94
+ expect(result).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('maskToken', () => {
99
+ it('should mask long tokens showing first and last 4 chars', () => {
100
+ expect(maskToken('abcdefghijklmnop')).toBe('abcd...mnop');
101
+ });
102
+
103
+ it('should fully mask tokens 8 chars or less', () => {
104
+ expect(maskToken('short')).toBe('****');
105
+ expect(maskToken('12345678')).toBe('****');
106
+ });
107
+
108
+ it('should handle exactly 9 character token', () => {
109
+ expect(maskToken('123456789')).toBe('1234...6789');
110
+ });
111
+
112
+ it('should handle very long tokens', () => {
113
+ const longToken = 'a'.repeat(100);
114
+ expect(maskToken(longToken)).toBe('aaaa...aaaa');
115
+ });
116
+
117
+ it('should handle empty string', () => {
118
+ expect(maskToken('')).toBe('****');
119
+ });
120
+ });
121
+
122
+ describe('credentials flow', () => {
123
+ it('should store and retrieve credentials', async () => {
124
+ await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
125
+ root: tempDir,
126
+ });
127
+
128
+ const creds = await getDokployCredentials({ root: tempDir });
129
+ expect(creds).not.toBeNull();
130
+ expect(creds!.token).toBe('my-token');
131
+ expect(creds!.endpoint).toBe('https://dokploy.example.com');
132
+ });
133
+
134
+ it('should remove credentials', async () => {
135
+ await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
136
+ root: tempDir,
137
+ });
138
+
139
+ const removed = await removeDokployCredentials({ root: tempDir });
140
+ expect(removed).toBe(true);
141
+
142
+ const creds = await getDokployCredentials({ root: tempDir });
143
+ expect(creds).toBeNull();
144
+ });
145
+
146
+ it('should return false when removing non-existent credentials', async () => {
147
+ const removed = await removeDokployCredentials({ root: tempDir });
148
+ expect(removed).toBe(false);
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('URL normalization', () => {
154
+ it('should remove trailing slash from endpoint', () => {
155
+ const endpoint = 'https://dokploy.example.com/'.replace(/\/$/, '');
156
+ expect(endpoint).toBe('https://dokploy.example.com');
157
+ });
158
+
159
+ it('should not modify endpoint without trailing slash', () => {
160
+ const endpoint = 'https://dokploy.example.com'.replace(/\/$/, '');
161
+ expect(endpoint).toBe('https://dokploy.example.com');
162
+ });
163
+
164
+ it('should validate URL format', () => {
165
+ expect(() => new URL('https://dokploy.example.com')).not.toThrow();
166
+ expect(() => new URL('invalid-url')).toThrow();
167
+ });
168
+ });