@geekmidas/cli 0.54.0 → 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 (152) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +26 -5
  3. package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
  4. package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
  5. package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
  6. package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
  7. package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
  8. package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
  9. package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
  10. package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
  11. package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
  12. package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
  13. package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
  14. package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
  15. package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
  16. package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
  17. package/dist/Route53Provider-CpRIqu69.cjs +157 -0
  18. package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
  19. package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
  20. package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
  21. package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
  22. package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
  23. package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
  24. package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
  25. package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
  26. package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
  27. package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
  28. package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
  29. package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
  30. package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
  31. package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
  32. package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
  33. package/dist/config.cjs +2 -2
  34. package/dist/config.d.cts +1 -1
  35. package/dist/config.d.mts +2 -2
  36. package/dist/config.mjs +2 -2
  37. package/dist/credentials-C8DWtnMY.cjs +174 -0
  38. package/dist/credentials-C8DWtnMY.cjs.map +1 -0
  39. package/dist/credentials-DT1dSxIx.mjs +126 -0
  40. package/dist/credentials-DT1dSxIx.mjs.map +1 -0
  41. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
  42. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
  43. package/dist/deploy/sniffer-loader.cjs +1 -1
  44. package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
  45. package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
  46. package/dist/dokploy-api-CHa8G51l.mjs +3 -0
  47. package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
  48. package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
  49. package/dist/dokploy-api-CWc02yyg.cjs +3 -0
  50. package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
  51. package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
  52. package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
  53. package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
  54. package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
  55. package/dist/encryption-UUmaWAmz.mjs +3 -0
  56. package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
  57. package/dist/index-B5rGIc4g.d.cts.map +1 -0
  58. package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
  59. package/dist/index-KFEbMIRa.d.mts.map +1 -0
  60. package/dist/index.cjs +2223 -606
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.mjs +2200 -583
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
  65. package/dist/openapi-BMFmLnX6.mjs.map +1 -0
  66. package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
  67. package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
  68. package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
  69. package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
  70. package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
  71. package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
  72. package/dist/openapi-react-query.cjs +1 -1
  73. package/dist/openapi-react-query.mjs +1 -1
  74. package/dist/openapi.cjs +3 -3
  75. package/dist/openapi.d.cts +1 -1
  76. package/dist/openapi.d.mts +2 -2
  77. package/dist/openapi.mjs +3 -3
  78. package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
  79. package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
  80. package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
  81. package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
  82. package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
  83. package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
  84. package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
  85. package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
  86. package/dist/workspace/index.cjs +1 -1
  87. package/dist/workspace/index.d.cts +1 -1
  88. package/dist/workspace/index.d.mts +2 -2
  89. package/dist/workspace/index.mjs +1 -1
  90. package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
  91. package/dist/workspace-BFRUOOrh.cjs.map +1 -0
  92. package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
  93. package/dist/workspace-DAxG3_H2.mjs.map +1 -0
  94. package/package.json +12 -8
  95. package/src/build/__tests__/handler-templates.spec.ts +115 -47
  96. package/src/deploy/CachedStateProvider.ts +86 -0
  97. package/src/deploy/LocalStateProvider.ts +57 -0
  98. package/src/deploy/SSMStateProvider.ts +93 -0
  99. package/src/deploy/StateProvider.ts +171 -0
  100. package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
  101. package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
  102. package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
  103. package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
  104. package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
  105. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
  106. package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +28 -19
  107. package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
  108. package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
  109. package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
  110. package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
  111. package/src/deploy/__tests__/env-resolver.spec.ts +37 -15
  112. package/src/deploy/__tests__/sniffer.spec.ts +4 -20
  113. package/src/deploy/__tests__/state.spec.ts +13 -5
  114. package/src/deploy/dns/DnsProvider.ts +163 -0
  115. package/src/deploy/dns/HostingerProvider.ts +100 -0
  116. package/src/deploy/dns/Route53Provider.ts +256 -0
  117. package/src/deploy/dns/index.ts +257 -165
  118. package/src/deploy/env-resolver.ts +12 -5
  119. package/src/deploy/index.ts +16 -13
  120. package/src/deploy/sniffer-envkit-patch.ts +3 -1
  121. package/src/deploy/sniffer-routes-worker.ts +104 -0
  122. package/src/deploy/sniffer.ts +77 -55
  123. package/src/deploy/state-commands.ts +274 -0
  124. package/src/dev/__tests__/entry.spec.ts +8 -2
  125. package/src/dev/__tests__/index.spec.ts +1 -3
  126. package/src/dev/index.ts +9 -3
  127. package/src/docker/__tests__/templates.spec.ts +3 -1
  128. package/src/index.ts +88 -0
  129. package/src/init/__tests__/generators.spec.ts +273 -0
  130. package/src/init/__tests__/init.spec.ts +3 -3
  131. package/src/init/generators/auth.ts +1 -0
  132. package/src/init/generators/config.ts +2 -0
  133. package/src/init/generators/models.ts +6 -1
  134. package/src/init/generators/monorepo.ts +3 -0
  135. package/src/init/generators/ui.ts +1472 -0
  136. package/src/init/generators/web.ts +134 -87
  137. package/src/init/index.ts +22 -3
  138. package/src/init/templates/api.ts +109 -3
  139. package/src/openapi.ts +99 -13
  140. package/src/workspace/__tests__/schema.spec.ts +107 -0
  141. package/src/workspace/schema.ts +314 -4
  142. package/src/workspace/types.ts +22 -36
  143. package/dist/dokploy-api-CItuaWTq.mjs +0 -3
  144. package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
  145. package/dist/encryption-CQXBZGkt.mjs +0 -3
  146. package/dist/index-A70abJ1m.d.mts.map +0 -1
  147. package/dist/index-pOA56MWT.d.cts.map +0 -1
  148. package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
  149. package/dist/openapi-D7WwlpPF.cjs.map +0 -1
  150. package/dist/workspace-CaVW6j2q.cjs.map +0 -1
  151. package/dist/workspace-DLFRaDc-.mjs.map +0 -1
  152. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,171 @@
1
+ /**
2
+ * State Provider Interface
3
+ *
4
+ * Abstracts the storage backend for deployment state.
5
+ * Built-in providers: LocalStateProvider, SSMStateProvider
6
+ * Users can also supply custom implementations.
7
+ */
8
+
9
+ import type { DokployStageState } from './state';
10
+
11
+ /**
12
+ * Interface for deployment state storage providers.
13
+ *
14
+ * Implementations must handle:
15
+ * - Reading state for a stage (returns null if not found)
16
+ * - Writing state for a stage (creates or updates)
17
+ */
18
+ export interface StateProvider {
19
+ /**
20
+ * Read deployment state for a stage.
21
+ *
22
+ * @param stage - The deployment stage (e.g., 'development', 'production')
23
+ * @returns The state object or null if not found
24
+ */
25
+ read(stage: string): Promise<DokployStageState | null>;
26
+
27
+ /**
28
+ * Write deployment state for a stage.
29
+ *
30
+ * @param stage - The deployment stage
31
+ * @param state - The state object to persist
32
+ */
33
+ write(stage: string, state: DokployStageState): Promise<void>;
34
+ }
35
+
36
+ /**
37
+ * Valid AWS regions.
38
+ */
39
+ export type AwsRegion =
40
+ | 'us-east-1'
41
+ | 'us-east-2'
42
+ | 'us-west-1'
43
+ | 'us-west-2'
44
+ | 'af-south-1'
45
+ | 'ap-east-1'
46
+ | 'ap-south-1'
47
+ | 'ap-south-2'
48
+ | 'ap-southeast-1'
49
+ | 'ap-southeast-2'
50
+ | 'ap-southeast-3'
51
+ | 'ap-southeast-4'
52
+ | 'ap-northeast-1'
53
+ | 'ap-northeast-2'
54
+ | 'ap-northeast-3'
55
+ | 'ca-central-1'
56
+ | 'eu-central-1'
57
+ | 'eu-central-2'
58
+ | 'eu-west-1'
59
+ | 'eu-west-2'
60
+ | 'eu-west-3'
61
+ | 'eu-south-1'
62
+ | 'eu-south-2'
63
+ | 'eu-north-1'
64
+ | 'me-south-1'
65
+ | 'me-central-1'
66
+ | 'sa-east-1';
67
+
68
+ /**
69
+ * Local state provider config.
70
+ */
71
+ export interface LocalStateConfig {
72
+ provider: 'local';
73
+ }
74
+
75
+ /**
76
+ * SSM state provider config (requires region).
77
+ */
78
+ export interface SSMStateConfig {
79
+ provider: 'ssm';
80
+ /** AWS region (required for SSM provider) */
81
+ region: AwsRegion;
82
+ }
83
+
84
+ /**
85
+ * Custom state provider config.
86
+ */
87
+ export interface CustomStateConfig {
88
+ /** Custom StateProvider implementation */
89
+ provider: StateProvider;
90
+ }
91
+
92
+ /**
93
+ * State configuration types.
94
+ */
95
+ export type StateConfig = LocalStateConfig | SSMStateConfig | CustomStateConfig;
96
+
97
+ /**
98
+ * Check if value is a StateProvider implementation.
99
+ */
100
+ export function isStateProvider(value: unknown): value is StateProvider {
101
+ return (
102
+ typeof value === 'object' &&
103
+ value !== null &&
104
+ typeof (value as StateProvider).read === 'function' &&
105
+ typeof (value as StateProvider).write === 'function'
106
+ );
107
+ }
108
+
109
+ export interface CreateStateProviderOptions {
110
+ /** State config from workspace */
111
+ config?: StateConfig;
112
+ /** Workspace root directory (for local provider) */
113
+ workspaceRoot: string;
114
+ /** Workspace name (for SSM parameter path) */
115
+ workspaceName: string;
116
+ }
117
+
118
+ /**
119
+ * Create a state provider based on configuration.
120
+ *
121
+ * - 'local': LocalStateProvider (default)
122
+ * - 'ssm': CachedStateProvider with SSM as source of truth
123
+ * - Custom: Use provided StateProvider implementation
124
+ */
125
+ export async function createStateProvider(
126
+ options: CreateStateProviderOptions,
127
+ ): Promise<StateProvider> {
128
+ const { config, workspaceRoot, workspaceName } = options;
129
+
130
+ // Default to local provider if no config
131
+ if (!config) {
132
+ const { LocalStateProvider } = await import('./LocalStateProvider');
133
+ return new LocalStateProvider(workspaceRoot);
134
+ }
135
+
136
+ // Custom provider implementation
137
+ if (isStateProvider(config.provider)) {
138
+ return config.provider;
139
+ }
140
+
141
+ // Built-in providers (discriminated by provider string)
142
+ const provider = config.provider;
143
+
144
+ if (provider === 'local') {
145
+ const { LocalStateProvider } = await import('./LocalStateProvider');
146
+ return new LocalStateProvider(workspaceRoot);
147
+ }
148
+
149
+ if (provider === 'ssm') {
150
+ if (!workspaceName) {
151
+ throw new Error(
152
+ 'Workspace name is required for SSM state provider. Set "name" in gkm.config.ts.',
153
+ );
154
+ }
155
+
156
+ const { LocalStateProvider } = await import('./LocalStateProvider');
157
+ const { SSMStateProvider } = await import('./SSMStateProvider');
158
+ const { CachedStateProvider } = await import('./CachedStateProvider');
159
+
160
+ const local = new LocalStateProvider(workspaceRoot);
161
+ const ssm = new SSMStateProvider({
162
+ workspaceName,
163
+ region: (config as SSMStateConfig).region,
164
+ });
165
+
166
+ return new CachedStateProvider(ssm, local);
167
+ }
168
+
169
+ // Should never reach here - custom providers handled above
170
+ throw new Error(`Unknown state provider: ${JSON.stringify(config)}`);
171
+ }
@@ -0,0 +1,228 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { CachedStateProvider } from '../CachedStateProvider';
3
+ import type { StateProvider } from '../StateProvider';
4
+ import type { DokployStageState } from '../state';
5
+
6
+ const createMockProvider = (): StateProvider & {
7
+ readCalls: string[];
8
+ writeCalls: Array<{ stage: string; state: DokployStageState }>;
9
+ storage: Map<string, DokployStageState>;
10
+ } => {
11
+ const storage = new Map<string, DokployStageState>();
12
+ const readCalls: string[] = [];
13
+ const writeCalls: Array<{ stage: string; state: DokployStageState }> = [];
14
+
15
+ return {
16
+ storage,
17
+ readCalls,
18
+ writeCalls,
19
+ async read(stage: string): Promise<DokployStageState | null> {
20
+ readCalls.push(stage);
21
+ return storage.get(stage) ?? null;
22
+ },
23
+ async write(stage: string, state: DokployStageState): Promise<void> {
24
+ writeCalls.push({ stage, state });
25
+ storage.set(stage, { ...state });
26
+ },
27
+ };
28
+ };
29
+
30
+ describe('CachedStateProvider', () => {
31
+ describe('read', () => {
32
+ it('should return local state if available', async () => {
33
+ const local = createMockProvider();
34
+ const remote = createMockProvider();
35
+
36
+ const state: DokployStageState = {
37
+ provider: 'dokploy',
38
+ stage: 'production',
39
+ environmentId: 'env_123',
40
+ applications: { api: 'local_app' },
41
+ services: {},
42
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
43
+ };
44
+ local.storage.set('production', state);
45
+
46
+ const cached = new CachedStateProvider(remote, local);
47
+ const result = await cached.read('production');
48
+
49
+ expect(result).toEqual(state);
50
+ expect(local.readCalls).toEqual(['production']);
51
+ expect(remote.readCalls).toEqual([]);
52
+ });
53
+
54
+ it('should fetch from remote and cache locally if local missing', async () => {
55
+ const local = createMockProvider();
56
+ const remote = createMockProvider();
57
+
58
+ const state: DokployStageState = {
59
+ provider: 'dokploy',
60
+ stage: 'production',
61
+ environmentId: 'env_123',
62
+ applications: { api: 'remote_app' },
63
+ services: {},
64
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
65
+ };
66
+ remote.storage.set('production', state);
67
+
68
+ const cached = new CachedStateProvider(remote, local);
69
+ const result = await cached.read('production');
70
+
71
+ expect(result).toEqual(state);
72
+ expect(local.readCalls).toEqual(['production']);
73
+ expect(remote.readCalls).toEqual(['production']);
74
+ expect(local.writeCalls.length).toBe(1);
75
+ expect(local.writeCalls[0].stage).toBe('production');
76
+ });
77
+
78
+ it('should return null if both local and remote are empty', async () => {
79
+ const local = createMockProvider();
80
+ const remote = createMockProvider();
81
+
82
+ const cached = new CachedStateProvider(remote, local);
83
+ const result = await cached.read('nonexistent');
84
+
85
+ expect(result).toBeNull();
86
+ expect(local.readCalls).toEqual(['nonexistent']);
87
+ expect(remote.readCalls).toEqual(['nonexistent']);
88
+ });
89
+ });
90
+
91
+ describe('write', () => {
92
+ it('should write to both remote and local', async () => {
93
+ const local = createMockProvider();
94
+ const remote = createMockProvider();
95
+
96
+ const state: DokployStageState = {
97
+ provider: 'dokploy',
98
+ stage: 'production',
99
+ environmentId: 'env_123',
100
+ applications: { api: 'app_123' },
101
+ services: {},
102
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
103
+ };
104
+
105
+ const cached = new CachedStateProvider(remote, local);
106
+ await cached.write('production', state);
107
+
108
+ expect(remote.writeCalls.length).toBe(1);
109
+ expect(remote.writeCalls[0].stage).toBe('production');
110
+ expect(local.writeCalls.length).toBe(1);
111
+ expect(local.writeCalls[0].stage).toBe('production');
112
+ });
113
+ });
114
+
115
+ describe('pull', () => {
116
+ it('should fetch from remote and update local', async () => {
117
+ const local = createMockProvider();
118
+ const remote = createMockProvider();
119
+
120
+ const remoteState: DokployStageState = {
121
+ provider: 'dokploy',
122
+ stage: 'production',
123
+ environmentId: 'env_123',
124
+ applications: { api: 'remote_app' },
125
+ services: {},
126
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
127
+ };
128
+ remote.storage.set('production', remoteState);
129
+
130
+ const cached = new CachedStateProvider(remote, local);
131
+ const result = await cached.pull('production');
132
+
133
+ expect(result).toEqual(remoteState);
134
+ expect(remote.readCalls).toEqual(['production']);
135
+ expect(local.writeCalls.length).toBe(1);
136
+ });
137
+
138
+ it('should return null if remote is empty', async () => {
139
+ const local = createMockProvider();
140
+ const remote = createMockProvider();
141
+
142
+ const cached = new CachedStateProvider(remote, local);
143
+ const result = await cached.pull('nonexistent');
144
+
145
+ expect(result).toBeNull();
146
+ expect(local.writeCalls.length).toBe(0);
147
+ });
148
+ });
149
+
150
+ describe('push', () => {
151
+ it('should push local state to remote', async () => {
152
+ const local = createMockProvider();
153
+ const remote = createMockProvider();
154
+
155
+ const localState: DokployStageState = {
156
+ provider: 'dokploy',
157
+ stage: 'production',
158
+ environmentId: 'env_123',
159
+ applications: { api: 'local_app' },
160
+ services: {},
161
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
162
+ };
163
+ local.storage.set('production', localState);
164
+
165
+ const cached = new CachedStateProvider(remote, local);
166
+ const result = await cached.push('production');
167
+
168
+ expect(result).toEqual(localState);
169
+ expect(local.readCalls).toEqual(['production']);
170
+ expect(remote.writeCalls.length).toBe(1);
171
+ });
172
+
173
+ it('should return null if local is empty', async () => {
174
+ const local = createMockProvider();
175
+ const remote = createMockProvider();
176
+
177
+ const cached = new CachedStateProvider(remote, local);
178
+ const result = await cached.push('nonexistent');
179
+
180
+ expect(result).toBeNull();
181
+ expect(remote.writeCalls.length).toBe(0);
182
+ });
183
+ });
184
+
185
+ describe('diff', () => {
186
+ it('should return both local and remote state', async () => {
187
+ const local = createMockProvider();
188
+ const remote = createMockProvider();
189
+
190
+ const localState: DokployStageState = {
191
+ provider: 'dokploy',
192
+ stage: 'production',
193
+ environmentId: 'env_123',
194
+ applications: { api: 'local_app' },
195
+ services: {},
196
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
197
+ };
198
+ const remoteState: DokployStageState = {
199
+ provider: 'dokploy',
200
+ stage: 'production',
201
+ environmentId: 'env_123',
202
+ applications: { api: 'remote_app' },
203
+ services: {},
204
+ lastDeployedAt: '2024-01-02T00:00:00.000Z',
205
+ };
206
+
207
+ local.storage.set('production', localState);
208
+ remote.storage.set('production', remoteState);
209
+
210
+ const cached = new CachedStateProvider(remote, local);
211
+ const result = await cached.diff('production');
212
+
213
+ expect(result.local).toEqual(localState);
214
+ expect(result.remote).toEqual(remoteState);
215
+ });
216
+
217
+ it('should handle missing states', async () => {
218
+ const local = createMockProvider();
219
+ const remote = createMockProvider();
220
+
221
+ const cached = new CachedStateProvider(remote, local);
222
+ const result = await cached.diff('nonexistent');
223
+
224
+ expect(result.local).toBeNull();
225
+ expect(result.remote).toBeNull();
226
+ });
227
+ });
228
+ });