@geekmidas/cli 0.10.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 (145) 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-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +1 -1
  14. package/dist/encryption-C8H-38Yy.mjs +42 -0
  15. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  16. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  17. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  18. package/dist/index.cjs +2116 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2134 -192
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
  23. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  24. package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
  25. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  26. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  27. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  28. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  29. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  30. package/dist/openapi-react-query.cjs +1 -1
  31. package/dist/openapi-react-query.d.cts.map +1 -1
  32. package/dist/openapi-react-query.d.mts.map +1 -1
  33. package/dist/openapi-react-query.mjs +1 -1
  34. package/dist/openapi.cjs +2 -2
  35. package/dist/openapi.d.cts +1 -1
  36. package/dist/openapi.d.cts.map +1 -1
  37. package/dist/openapi.d.mts +1 -1
  38. package/dist/openapi.d.mts.map +1 -1
  39. package/dist/openapi.mjs +2 -2
  40. package/dist/storage-BUYQJgz7.cjs +4 -0
  41. package/dist/storage-BXoJvmv2.cjs +149 -0
  42. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  43. package/dist/storage-C9PU_30f.mjs +101 -0
  44. package/dist/storage-C9PU_30f.mjs.map +1 -0
  45. package/dist/storage-DLJAYxzJ.mjs +3 -0
  46. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  47. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  48. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  49. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  50. package/examples/cron-example.ts +27 -27
  51. package/examples/env.ts +27 -27
  52. package/examples/function-example.ts +31 -31
  53. package/examples/gkm.config.json +20 -20
  54. package/examples/gkm.config.ts +8 -8
  55. package/examples/gkm.minimal.config.json +5 -5
  56. package/examples/gkm.production.config.json +25 -25
  57. package/examples/logger.ts +2 -2
  58. package/package.json +6 -6
  59. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  60. package/src/__tests__/config.spec.ts +55 -55
  61. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  62. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  63. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  64. package/src/__tests__/openapi.spec.ts +428 -428
  65. package/src/__tests__/test-helpers.ts +76 -76
  66. package/src/auth/__tests__/credentials.spec.ts +204 -0
  67. package/src/auth/__tests__/index.spec.ts +168 -0
  68. package/src/auth/credentials.ts +187 -0
  69. package/src/auth/index.ts +226 -0
  70. package/src/build/__tests__/index-new.spec.ts +474 -474
  71. package/src/build/__tests__/manifests.spec.ts +333 -333
  72. package/src/build/bundler.ts +141 -0
  73. package/src/build/endpoint-analyzer.ts +236 -0
  74. package/src/build/handler-templates.ts +1253 -0
  75. package/src/build/index.ts +250 -179
  76. package/src/build/manifests.ts +52 -52
  77. package/src/build/providerResolver.ts +145 -145
  78. package/src/build/types.ts +64 -43
  79. package/src/config.ts +39 -39
  80. package/src/deploy/__tests__/docker.spec.ts +111 -0
  81. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  82. package/src/deploy/__tests__/init.spec.ts +662 -0
  83. package/src/deploy/docker.ts +128 -0
  84. package/src/deploy/dokploy.ts +204 -0
  85. package/src/deploy/index.ts +136 -0
  86. package/src/deploy/init.ts +484 -0
  87. package/src/deploy/types.ts +48 -0
  88. package/src/dev/__tests__/index.spec.ts +266 -266
  89. package/src/dev/index.ts +647 -601
  90. package/src/docker/__tests__/compose.spec.ts +531 -0
  91. package/src/docker/__tests__/templates.spec.ts +280 -0
  92. package/src/docker/compose.ts +273 -0
  93. package/src/docker/index.ts +230 -0
  94. package/src/docker/templates.ts +446 -0
  95. package/src/generators/CronGenerator.ts +72 -72
  96. package/src/generators/EndpointGenerator.ts +699 -398
  97. package/src/generators/FunctionGenerator.ts +84 -84
  98. package/src/generators/Generator.ts +72 -72
  99. package/src/generators/OpenApiTsGenerator.ts +577 -577
  100. package/src/generators/SubscriberGenerator.ts +124 -124
  101. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  102. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  103. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  104. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  105. package/src/generators/index.ts +4 -4
  106. package/src/index.ts +623 -201
  107. package/src/init/__tests__/generators.spec.ts +334 -334
  108. package/src/init/__tests__/init.spec.ts +332 -332
  109. package/src/init/__tests__/utils.spec.ts +89 -89
  110. package/src/init/generators/config.ts +175 -175
  111. package/src/init/generators/docker.ts +41 -41
  112. package/src/init/generators/env.ts +72 -72
  113. package/src/init/generators/index.ts +1 -1
  114. package/src/init/generators/models.ts +64 -64
  115. package/src/init/generators/monorepo.ts +161 -161
  116. package/src/init/generators/package.ts +71 -71
  117. package/src/init/generators/source.ts +6 -6
  118. package/src/init/index.ts +203 -208
  119. package/src/init/templates/api.ts +115 -115
  120. package/src/init/templates/index.ts +75 -75
  121. package/src/init/templates/minimal.ts +98 -98
  122. package/src/init/templates/serverless.ts +89 -89
  123. package/src/init/templates/worker.ts +98 -98
  124. package/src/init/utils.ts +54 -56
  125. package/src/openapi-react-query.ts +194 -194
  126. package/src/openapi.ts +63 -63
  127. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  128. package/src/secrets/__tests__/generator.spec.ts +319 -0
  129. package/src/secrets/__tests__/index.spec.ts +91 -0
  130. package/src/secrets/__tests__/storage.spec.ts +403 -0
  131. package/src/secrets/encryption.ts +91 -0
  132. package/src/secrets/generator.ts +164 -0
  133. package/src/secrets/index.ts +383 -0
  134. package/src/secrets/storage.ts +134 -0
  135. package/src/secrets/types.ts +53 -0
  136. package/src/types.ts +295 -176
  137. package/tsdown.config.ts +11 -8
  138. package/dist/config-BrkUalUh.mjs.map +0 -1
  139. package/dist/config-C9aXOHBe.cjs.map +0 -1
  140. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  141. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  142. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  143. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  144. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  145. package/dist/types-b-vwGpqc.d.cts.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,70 +110,70 @@ 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
- // 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}` }));
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}` }));
125
125
  }
126
126
 
127
127
  export function createTestFunction(timeout: number = 30) {
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
- }));
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
+ }));
135
135
  }
136
136
 
137
137
  export function createTestCron(
138
- schedule: ScheduleExpression = 'rate(1 hour)',
139
- timeout: number = 30,
138
+ schedule: ScheduleExpression = 'rate(1 hour)',
139
+ timeout: number = 30,
140
140
  ) {
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
- });
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
+ });
148
148
  }
149
149
 
150
150
  /**
151
151
  * Creates a mock build context
152
152
  */
153
153
  export function createMockBuildContext() {
154
- return {
155
- envParserPath: './env',
156
- envParserImportPattern: 'envParser',
157
- loggerPath: './logger',
158
- loggerImportPattern: 'logger',
159
- };
154
+ return {
155
+ envParserPath: './env',
156
+ envParserImportPattern: 'envParser',
157
+ loggerPath: './logger',
158
+ loggerImportPattern: 'logger',
159
+ };
160
160
  }
161
161
 
162
162
  /**
163
163
  * Waits for a condition to be true
164
164
  */
165
165
  export async function waitFor(
166
- condition: () => boolean,
167
- timeout = 5000,
168
- interval = 100,
166
+ condition: () => boolean,
167
+ timeout = 5000,
168
+ interval = 100,
169
169
  ): Promise<void> {
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
- }
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
+ }
177
177
  }
178
178
 
179
179
  import { dirname } from 'node:path';
@@ -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
+ });