@geekmidas/cli 1.10.41 → 1.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 (115) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +14 -0
  3. package/dist/{CachedStateProvider-37pG1HBx.cjs → CachedStateProvider-BdX7klRc.cjs} +1 -1
  4. package/dist/{CachedStateProvider-C8A-cCm5.mjs → CachedStateProvider-CzyFct42.mjs} +1 -1
  5. package/dist/{CachedStateProvider-C8A-cCm5.mjs.map → CachedStateProvider-CzyFct42.mjs.map} +1 -1
  6. package/dist/CachedStateProvider-GcGziXh1.mjs +3 -0
  7. package/dist/{CachedStateProvider-CxsXOKkg.cjs → CachedStateProvider-klLWZXWF.cjs} +1 -1
  8. package/dist/{CachedStateProvider-CxsXOKkg.cjs.map → CachedStateProvider-klLWZXWF.cjs.map} +1 -1
  9. package/dist/{HostingerProvider-ANWchdiK.mjs → HostingerProvider-B2ZYz7Yc.mjs} +1 -1
  10. package/dist/{HostingerProvider-ANWchdiK.mjs.map → HostingerProvider-B2ZYz7Yc.mjs.map} +1 -1
  11. package/dist/{HostingerProvider-5KYmwoK2.cjs → HostingerProvider-CrHfsgz9.cjs} +1 -1
  12. package/dist/{HostingerProvider-5KYmwoK2.cjs.map → HostingerProvider-CrHfsgz9.cjs.map} +1 -1
  13. package/dist/{LocalStateProvider-Dp0KkRcw.mjs → LocalStateProvider-CP49rjzt.mjs} +1 -1
  14. package/dist/{LocalStateProvider-Dp0KkRcw.mjs.map → LocalStateProvider-CP49rjzt.mjs.map} +1 -1
  15. package/dist/{LocalStateProvider-CLifRC0Y.cjs → LocalStateProvider-CalzNfNA.cjs} +1 -1
  16. package/dist/{LocalStateProvider-CLifRC0Y.cjs.map → LocalStateProvider-CalzNfNA.cjs.map} +1 -1
  17. package/dist/{Route53Provider-owQQ4pn6.cjs → Route53Provider-DwgXiLxt.cjs} +1 -1
  18. package/dist/{Route53Provider-owQQ4pn6.cjs.map → Route53Provider-DwgXiLxt.cjs.map} +1 -1
  19. package/dist/{Route53Provider-QoPgcXxn.mjs → Route53Provider-tlgBzrRQ.mjs} +1 -1
  20. package/dist/{Route53Provider-QoPgcXxn.mjs.map → Route53Provider-tlgBzrRQ.mjs.map} +1 -1
  21. package/dist/{SSMStateProvider-CksOTB8M.mjs → SSMStateProvider-BlrTiV5m.mjs} +1 -1
  22. package/dist/{SSMStateProvider-CksOTB8M.mjs.map → SSMStateProvider-BlrTiV5m.mjs.map} +1 -1
  23. package/dist/{SSMStateProvider-CT8tjl9o.cjs → SSMStateProvider-Bt6r8CTL.cjs} +1 -1
  24. package/dist/{SSMStateProvider-CT8tjl9o.cjs.map → SSMStateProvider-Bt6r8CTL.cjs.map} +1 -1
  25. package/dist/{backup-provisioner-C4noe75O.cjs → backup-provisioner-B3HBN-2k.cjs} +1 -1
  26. package/dist/{backup-provisioner-C4noe75O.cjs.map → backup-provisioner-B3HBN-2k.cjs.map} +1 -1
  27. package/dist/{backup-provisioner-BEXoHTuC.mjs → backup-provisioner-Xunq-p_8.mjs} +1 -1
  28. package/dist/{backup-provisioner-BEXoHTuC.mjs.map → backup-provisioner-Xunq-p_8.mjs.map} +1 -1
  29. package/dist/{bundler-c6QvGUBM.mjs → bundler-DkA4qzUD.mjs} +2 -2
  30. package/dist/{bundler-c6QvGUBM.mjs.map → bundler-DkA4qzUD.mjs.map} +1 -1
  31. package/dist/{bundler-BG6P249p.cjs → bundler-Dufq3uvI.cjs} +2 -2
  32. package/dist/{bundler-BG6P249p.cjs.map → bundler-Dufq3uvI.cjs.map} +1 -1
  33. package/dist/config.d.cts +2 -2
  34. package/dist/config.d.mts +2 -2
  35. package/dist/dokploy-api-BPYkG-ZZ.cjs +3 -0
  36. package/dist/dokploy-api-BiSzWM4v.mjs +3 -0
  37. package/dist/{dokploy-api-Dvyq-LsM.cjs → dokploy-api-CqOIKHme.cjs} +1 -1
  38. package/dist/{dokploy-api-Dvyq-LsM.cjs.map → dokploy-api-CqOIKHme.cjs.map} +1 -1
  39. package/dist/{dokploy-api-BIT0683o.mjs → dokploy-api-sfZFlWuE.mjs} +1 -1
  40. package/dist/{dokploy-api-BIT0683o.mjs.map → dokploy-api-sfZFlWuE.mjs.map} +1 -1
  41. package/dist/{encryption-CipsS0KW.cjs → encryption-BVS7Vmzs.cjs} +1 -1
  42. package/dist/encryption-Bd_m4q3g.mjs +3 -0
  43. package/dist/{encryption-DSkIlEZ6.mjs → encryption-CO5Q4Bhf.mjs} +1 -1
  44. package/dist/{encryption-DSkIlEZ6.mjs.map → encryption-CO5Q4Bhf.mjs.map} +1 -1
  45. package/dist/{encryption-DgKS-Dv9.cjs → encryption-MfXAiFTm.cjs} +1 -1
  46. package/dist/{encryption-DgKS-Dv9.cjs.map → encryption-MfXAiFTm.cjs.map} +1 -1
  47. package/dist/{index-BBvGMG_A.d.mts → index-Bw548Bta.d.mts} +2 -2
  48. package/dist/{index-Dz2a7xQU.d.cts.map → index-Bw548Bta.d.mts.map} +1 -1
  49. package/dist/{index-Dz2a7xQU.d.cts → index-DTpkeFb8.d.cts} +2 -2
  50. package/dist/{index-BBvGMG_A.d.mts.map → index-DTpkeFb8.d.cts.map} +1 -1
  51. package/dist/index.cjs +863 -1330
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.mjs +990 -1457
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/{openapi-DHp49UJV.cjs → openapi-BzNOC7_i.cjs} +14 -2
  56. package/dist/openapi-BzNOC7_i.cjs.map +1 -0
  57. package/dist/{openapi-DF9GyhyZ.mjs → openapi-Cfwi0fqF.mjs} +14 -2
  58. package/dist/openapi-Cfwi0fqF.mjs.map +1 -0
  59. package/dist/{openapi-react-query-C4UdILaI.mjs → openapi-react-query-C4iIvpPf.mjs} +1 -1
  60. package/dist/{openapi-react-query-C4UdILaI.mjs.map → openapi-react-query-C4iIvpPf.mjs.map} +1 -1
  61. package/dist/{openapi-react-query-DYbBq-WJ.cjs → openapi-react-query-CYWik72n.cjs} +1 -1
  62. package/dist/{openapi-react-query-DYbBq-WJ.cjs.map → openapi-react-query-CYWik72n.cjs.map} +1 -1
  63. package/dist/openapi-react-query.cjs +1 -1
  64. package/dist/openapi-react-query.mjs +1 -1
  65. package/dist/openapi.cjs +1 -1
  66. package/dist/openapi.d.cts +1 -1
  67. package/dist/openapi.d.mts +1 -1
  68. package/dist/openapi.mjs +1 -1
  69. package/dist/{reconcile-CYGmqXvq.mjs → reconcile-B2FoJE0a.mjs} +1 -1
  70. package/dist/{reconcile-CYGmqXvq.mjs.map → reconcile-B2FoJE0a.mjs.map} +1 -1
  71. package/dist/{reconcile-D93YG7G5.cjs → reconcile-BzqKJ2Rt.cjs} +1 -1
  72. package/dist/{reconcile-D93YG7G5.cjs.map → reconcile-BzqKJ2Rt.cjs.map} +1 -1
  73. package/dist/setup-DDXUXzzl.mjs +765 -0
  74. package/dist/setup-DDXUXzzl.mjs.map +1 -0
  75. package/dist/setup-DLwhfyt-.cjs +8 -0
  76. package/dist/setup-Dh-B7YTX.mjs +8 -0
  77. package/dist/setup-WZ-xqAQO.cjs +849 -0
  78. package/dist/setup-WZ-xqAQO.cjs.map +1 -0
  79. package/dist/{sync-COnAugP-.mjs → sync-B5UHXwRM.mjs} +1 -1
  80. package/dist/sync-BLA94rgC.cjs +4 -0
  81. package/dist/{sync-D_NowTkZ.mjs → sync-CSooqRQ8.mjs} +1 -1
  82. package/dist/{sync-D_NowTkZ.mjs.map → sync-CSooqRQ8.mjs.map} +1 -1
  83. package/dist/{sync-DGXXSk2v.cjs → sync-o_CtEbvR.cjs} +1 -1
  84. package/dist/{sync-DGXXSk2v.cjs.map → sync-o_CtEbvR.cjs.map} +1 -1
  85. package/dist/{types-DdHfUbxk.d.cts → types-B3GQUg3k.d.mts} +3 -1
  86. package/dist/{types-DdHfUbxk.d.cts.map → types-B3GQUg3k.d.mts.map} +1 -1
  87. package/dist/{types-OszPdw9m.d.mts → types-BWQDiooe.d.cts} +3 -1
  88. package/dist/{types-OszPdw9m.d.mts.map → types-BWQDiooe.d.cts.map} +1 -1
  89. package/dist/workspace/index.d.cts +2 -2
  90. package/dist/workspace/index.d.mts +2 -2
  91. package/package.json +6 -5
  92. package/src/build/__tests__/index-new.spec.ts +3 -1
  93. package/src/build/__tests__/manifests.spec.ts +2 -2
  94. package/src/build/index.ts +61 -21
  95. package/src/build/manifests.ts +62 -5
  96. package/src/dev/index.ts +25 -11
  97. package/src/generators/EndpointGenerator.ts +14 -2
  98. package/src/generators/QueueGenerator.ts +245 -0
  99. package/src/generators/SubscriberGenerator.ts +5 -0
  100. package/src/generators/TopicGenerator.ts +45 -0
  101. package/src/generators/__tests__/QueueGenerator.spec.ts +141 -0
  102. package/src/generators/index.ts +2 -0
  103. package/src/index.ts +4 -0
  104. package/src/init/versions.ts +6 -6
  105. package/src/setup/__tests__/auto-setup.spec.ts +147 -0
  106. package/src/setup/index.ts +46 -6
  107. package/src/test/index.ts +17 -0
  108. package/src/types.ts +14 -51
  109. package/dist/CachedStateProvider-CC0DyO09.mjs +0 -3
  110. package/dist/dokploy-api-BOx71j6F.cjs +0 -3
  111. package/dist/dokploy-api-BZGk-lkD.mjs +0 -3
  112. package/dist/encryption-Db2zvGqM.mjs +0 -3
  113. package/dist/openapi-DF9GyhyZ.mjs.map +0 -1
  114. package/dist/openapi-DHp49UJV.cjs.map +0 -1
  115. package/dist/sync-D1Pa30oV.cjs +0 -4
@@ -0,0 +1,141 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { type Queue, QueueBuilder } from '@geekmidas/constructs/queue';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { z } from 'zod';
6
+ import {
7
+ cleanupDir,
8
+ createMockBuildContext,
9
+ createTempDir,
10
+ } from '../../__tests__/test-helpers';
11
+ import type { GeneratedConstruct } from '../Generator';
12
+ import { QueueGenerator } from '../QueueGenerator';
13
+
14
+ const schema = z.object({ orderId: z.string() });
15
+
16
+ describe('QueueGenerator', () => {
17
+ let tempDir: string;
18
+ let outputDir: string;
19
+ let generator: QueueGenerator;
20
+ let context: ReturnType<typeof createMockBuildContext>;
21
+
22
+ beforeEach(async () => {
23
+ tempDir = await createTempDir();
24
+ outputDir = join(tempDir, 'output');
25
+ generator = new QueueGenerator();
26
+ context = createMockBuildContext();
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await cleanupDir(tempDir);
31
+ });
32
+
33
+ const createQueueConstruct = (
34
+ key: string,
35
+ name: string,
36
+ ): GeneratedConstruct<Queue<any, any, any, any>> => {
37
+ const queue = new QueueBuilder()
38
+ .queue(name)
39
+ .batchSize(5)
40
+ .message(schema)
41
+ .handle(async () => {});
42
+
43
+ return {
44
+ key,
45
+ name: key.toLowerCase(),
46
+ construct: queue,
47
+ path: {
48
+ absolute: join(tempDir, `${key}.ts`),
49
+ relative: `${key}.ts`,
50
+ },
51
+ };
52
+ };
53
+
54
+ describe('isConstruct', () => {
55
+ it('identifies queues and rejects everything else', () => {
56
+ const queue = new QueueBuilder()
57
+ .queue('orders')
58
+ .message(schema)
59
+ .handle(async () => {});
60
+
61
+ expect(generator.isConstruct(queue)).toBe(true);
62
+ expect(generator.isConstruct({})).toBe(false);
63
+ expect(generator.isConstruct(null)).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('aws-lambda provider', () => {
68
+ it('generates one AWSLambdaQueue handler per queue', async () => {
69
+ const constructs = [
70
+ createQueueConstruct('ordersQueue', 'orders'),
71
+ createQueueConstruct('emailsQueue', 'emails'),
72
+ ];
73
+
74
+ const infos = await generator.build(context, constructs, outputDir, {
75
+ provider: 'aws-lambda',
76
+ });
77
+
78
+ expect(infos).toHaveLength(2);
79
+ expect(infos[0]).toMatchObject({
80
+ name: 'orders',
81
+ handler: expect.stringContaining('queues/ordersQueue.handler'),
82
+ batchSize: 5,
83
+ });
84
+ // Env requirement from the (sniffed) producer flows nowhere here, but the
85
+ // consumer queue's own services would; assert the field exists.
86
+ expect(infos[0].environment).toBeDefined();
87
+
88
+ const handler = await readFile(
89
+ join(outputDir, 'queues', 'ordersQueue.ts'),
90
+ 'utf-8',
91
+ );
92
+ expect(handler).toContain(
93
+ "import { AWSLambdaQueue } from '@geekmidas/constructs/aws'",
94
+ );
95
+ expect(handler).toContain('import { ordersQueue }');
96
+ expect(handler).toContain('new AWSLambdaQueue(envParser, ordersQueue)');
97
+ expect(handler).toContain('export const handler = adapter.handler');
98
+ });
99
+
100
+ it('returns an empty array for no queues', async () => {
101
+ const infos = await generator.build(context, [], outputDir, {
102
+ provider: 'aws-lambda',
103
+ });
104
+ expect(infos).toEqual([]);
105
+ });
106
+ });
107
+
108
+ describe('server provider', () => {
109
+ it('generates queues.ts with the setupQueues poller even when empty', async () => {
110
+ const infos = await generator.build(context, [], outputDir, {
111
+ provider: 'server',
112
+ });
113
+
114
+ expect(infos).toEqual([]);
115
+
116
+ const content = await readFile(join(outputDir, 'queues.ts'), 'utf-8');
117
+ expect(content).toContain('export async function setupQueues');
118
+ expect(content).toContain('const queues = [');
119
+ expect(content).toContain('EVENT_SUBSCRIBER_CONNECTION_STRING');
120
+ });
121
+
122
+ it('subscribes each queue by its name and validates the payload', async () => {
123
+ const constructs = [
124
+ createQueueConstruct('ordersQueue', 'orders'),
125
+ createQueueConstruct('emailsQueue', 'emails'),
126
+ ];
127
+
128
+ await generator.build(context, constructs, outputDir, {
129
+ provider: 'server',
130
+ });
131
+
132
+ const content = await readFile(join(outputDir, 'queues.ts'), 'utf-8');
133
+ expect(content).toContain('import { ordersQueue }');
134
+ expect(content).toContain('import { emailsQueue }');
135
+ // A queue subscribes to a single "type" — its own name.
136
+ expect(content).toContain('eventSubscriber.subscribe([queue.name]');
137
+ expect(content).toContain("queue.messageSchema['~standard'].validate");
138
+ expect(content).toContain('messages: [validation.value]');
139
+ });
140
+ });
141
+ });
@@ -6,4 +6,6 @@ export {
6
6
  type GeneratedConstruct,
7
7
  type GeneratorOptions,
8
8
  } from './Generator';
9
+ export { QueueGenerator } from './QueueGenerator';
9
10
  export { SubscriberGenerator } from './SubscriberGenerator';
11
+ export { TopicGenerator } from './TopicGenerator';
package/src/index.ts CHANGED
@@ -236,6 +236,10 @@ program
236
236
  .option('--watch', 'Enable watch mode')
237
237
  .option('--coverage', 'Generate coverage report')
238
238
  .option('--ui', 'Open Vitest UI')
239
+ .option(
240
+ '--auto-setup',
241
+ 'Generate a fresh stage (secrets + key) from the workspace config when none exists (for CI; also via GKM_AUTO_SETUP)',
242
+ )
239
243
  .argument('[pattern]', 'Pattern to filter tests')
240
244
  .action(async (pattern: string | undefined, options: TestOptions) => {
241
245
  try {
@@ -30,22 +30,22 @@ export const GEEKMIDAS_VERSIONS = {
30
30
  '@geekmidas/audit': '~2.0.1',
31
31
  '@geekmidas/auth': '~2.0.1',
32
32
  '@geekmidas/cache': '~1.1.1',
33
- '@geekmidas/client': '~5.0.0',
33
+ '@geekmidas/client': '~6.0.0',
34
34
  '@geekmidas/cloud': '~1.0.1',
35
- '@geekmidas/constructs': '~3.1.0',
35
+ '@geekmidas/constructs': '~4.0.1',
36
36
  '@geekmidas/db': '~1.0.2',
37
37
  '@geekmidas/emailkit': '~1.0.1',
38
38
  '@geekmidas/envkit': '~1.0.7',
39
39
  '@geekmidas/errors': '~1.0.1',
40
40
  '@geekmidas/events': '~1.1.4',
41
41
  '@geekmidas/logger': '~1.0.2',
42
- '@geekmidas/rate-limit': '~2.0.1',
43
- '@geekmidas/schema': '~1.0.2',
44
- '@geekmidas/services': '~1.0.4',
42
+ '@geekmidas/rate-limit': '~3.0.0',
43
+ '@geekmidas/schema': '~1.0.3',
44
+ '@geekmidas/services': '~1.1.1',
45
45
  '@geekmidas/storage': '~2.0.3',
46
46
  '@geekmidas/studio': '~1.0.1',
47
47
  '@geekmidas/telescope': '~1.0.1',
48
- '@geekmidas/testkit': '~1.0.9',
48
+ '@geekmidas/testkit': '~2.0.0',
49
49
  '@geekmidas/cli': CLI_VERSION,
50
50
  } as const;
51
51
 
@@ -0,0 +1,147 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { rm } from 'node:fs/promises';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { basename, join } from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { readStageSecrets, secretsExist } from '../../secrets/storage.js';
7
+ import type { NormalizedWorkspace } from '../../workspace/types.js';
8
+ import { createFreshWorkspaceSecrets, ensureStageSecrets } from '../index.js';
9
+
10
+ function createWorkspace(
11
+ overrides: Partial<NormalizedWorkspace> = {},
12
+ ): NormalizedWorkspace {
13
+ return {
14
+ name: 'test-project',
15
+ root: '/tmp/test-project',
16
+ apps: {
17
+ api: {
18
+ type: 'backend',
19
+ port: 3000,
20
+ root: '/tmp/test-project/apps/api',
21
+ packageName: '@test/api',
22
+ routes: './src/endpoints/**/*.ts',
23
+ dependencies: [],
24
+ },
25
+ },
26
+ services: { db: true },
27
+ deploy: {},
28
+ shared: {},
29
+ secrets: {},
30
+ ...overrides,
31
+ } as NormalizedWorkspace;
32
+ }
33
+
34
+ describe('createFreshWorkspaceSecrets', () => {
35
+ it('generates postgres credentials when db service is enabled', () => {
36
+ const secrets = createFreshWorkspaceSecrets(
37
+ 'development',
38
+ createWorkspace(),
39
+ );
40
+
41
+ expect(secrets.stage).toBe('development');
42
+ expect(secrets.services.postgres).toBeDefined();
43
+ expect(secrets.services.postgres?.password).toBeTruthy();
44
+ expect(secrets.urls.DATABASE_URL).toContain('postgresql://');
45
+ });
46
+
47
+ it('generates a randomized password each call', () => {
48
+ const a = createFreshWorkspaceSecrets('development', createWorkspace());
49
+ const b = createFreshWorkspaceSecrets('development', createWorkspace());
50
+
51
+ expect(a.services.postgres?.password).not.toBe(
52
+ b.services.postgres?.password,
53
+ );
54
+ });
55
+
56
+ it('adds single-app custom secrets', () => {
57
+ const secrets = createFreshWorkspaceSecrets(
58
+ 'development',
59
+ createWorkspace(),
60
+ );
61
+
62
+ expect(secrets.custom.NODE_ENV).toBe('development');
63
+ expect(secrets.custom.JWT_SECRET).toBeTruthy();
64
+ });
65
+ });
66
+
67
+ describe('ensureStageSecrets', () => {
68
+ let testDir: string;
69
+ const originalGkmConfigPath = process.env.GKM_CONFIG_PATH;
70
+
71
+ beforeEach(() => {
72
+ testDir = join(
73
+ tmpdir(),
74
+ `gkm-auto-setup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
75
+ );
76
+ mkdirSync(testDir, { recursive: true });
77
+
78
+ // Minimal single-app workspace config with a db service.
79
+ writeFileSync(
80
+ join(testDir, 'gkm.config.ts'),
81
+ `import { defineWorkspace } from '@geekmidas/cli/config';
82
+
83
+ export default defineWorkspace({
84
+ name: 'test-workspace',
85
+ apps: {
86
+ api: {
87
+ type: 'backend',
88
+ path: 'apps/api',
89
+ port: 3001,
90
+ routes: './src/endpoints/**/*.ts',
91
+ envParser: './src/config/env#envParser',
92
+ logger: './src/config/logger#logger',
93
+ },
94
+ },
95
+ services: { db: true },
96
+ });
97
+ `,
98
+ );
99
+ const apiDir = join(testDir, 'apps', 'api');
100
+ mkdirSync(apiDir, { recursive: true });
101
+ writeFileSync(
102
+ join(apiDir, 'package.json'),
103
+ JSON.stringify({ name: '@test/api', version: '0.0.1' }),
104
+ );
105
+
106
+ process.env.GKM_CONFIG_PATH = join(testDir, 'gkm.config.ts');
107
+ });
108
+
109
+ afterEach(async () => {
110
+ if (originalGkmConfigPath === undefined) {
111
+ delete process.env.GKM_CONFIG_PATH;
112
+ } else {
113
+ process.env.GKM_CONFIG_PATH = originalGkmConfigPath;
114
+ }
115
+
116
+ await rm(testDir, { recursive: true, force: true });
117
+ // writeStageSecrets mints a key at ~/.gkm/{basename(root)}
118
+ await rm(join(homedir(), '.gkm', basename(testDir)), {
119
+ recursive: true,
120
+ force: true,
121
+ });
122
+ });
123
+
124
+ it('generates a decryptable stage when none exists', async () => {
125
+ expect(secretsExist('development', testDir)).toBe(false);
126
+
127
+ const generated = await ensureStageSecrets('development', testDir);
128
+
129
+ expect(generated).toBe(true);
130
+ expect(secretsExist('development', testDir)).toBe(true);
131
+
132
+ // Round-trips through the keystore the same way `gkm test` reads it.
133
+ const read = await readStageSecrets('development', testDir);
134
+ expect(read?.services.postgres).toBeDefined();
135
+ });
136
+
137
+ it('is a no-op when secrets already exist', async () => {
138
+ await ensureStageSecrets('development', testDir);
139
+ const before = await readStageSecrets('development', testDir);
140
+
141
+ const generated = await ensureStageSecrets('development', testDir);
142
+
143
+ expect(generated).toBe(false);
144
+ const after = await readStageSecrets('development', testDir);
145
+ expect(after).toEqual(before);
146
+ });
147
+ });
@@ -288,13 +288,16 @@ export function reconcileSecrets(
288
288
  }
289
289
 
290
290
  /**
291
- * Generate fresh secrets for the workspace.
291
+ * Build a fresh StageSecrets object for a workspace: service credentials,
292
+ * connection URLs, and custom secrets. Pure — does not write to disk or touch
293
+ * SSM. Service passwords are freshly randomized, so the result must be the same
294
+ * one used to start the matching Docker containers.
295
+ * @internal Exported for reuse by `gkm test` auto-setup.
292
296
  */
293
- async function generateFreshSecrets(
297
+ export function createFreshWorkspaceSecrets(
294
298
  stage: string,
295
299
  workspace: NormalizedWorkspace,
296
- options: SetupOptions,
297
- ) {
300
+ ): StageSecrets {
298
301
  // Determine services from workspace config
299
302
  const serviceNames: ComposeServiceName[] = [];
300
303
  if (workspace.services.db) serviceNames.push('postgres');
@@ -313,8 +316,7 @@ async function generateFreshSecrets(
313
316
  // Generate fullstack-aware custom secrets
314
317
  const isMultiApp = Object.keys(workspace.apps).length > 1;
315
318
  if (isMultiApp) {
316
- const customSecrets = generateFullstackCustomSecrets(workspace);
317
- secrets.custom = customSecrets;
319
+ secrets.custom = generateFullstackCustomSecrets(workspace);
318
320
  } else {
319
321
  secrets.custom = {
320
322
  NODE_ENV: 'development',
@@ -324,6 +326,44 @@ async function generateFreshSecrets(
324
326
  };
325
327
  }
326
328
 
329
+ return secrets;
330
+ }
331
+
332
+ /**
333
+ * Ensure a usable secret set exists for a stage, generating a fresh one from the
334
+ * workspace config when none is present. No-op if secrets already exist.
335
+ *
336
+ * Powers `gkm test --auto-setup` / `GKM_AUTO_SETUP`: CI can run without a
337
+ * committed secrets file or a shared encryption key — the stage is regenerated
338
+ * from the committed `gkm.config.ts`, and `writeStageSecrets` mints a local key.
339
+ *
340
+ * @returns true if fresh secrets were generated, false if existing ones were kept
341
+ */
342
+ export async function ensureStageSecrets(
343
+ stage: string,
344
+ cwd: string = process.cwd(),
345
+ ): Promise<boolean> {
346
+ const { workspace } = await loadWorkspaceConfig(cwd);
347
+
348
+ if (secretsExist(stage, workspace.root)) {
349
+ return false;
350
+ }
351
+
352
+ const secrets = createFreshWorkspaceSecrets(stage, workspace);
353
+ await writeStageSecrets(secrets, workspace.root);
354
+ return true;
355
+ }
356
+
357
+ /**
358
+ * Generate fresh secrets for the workspace.
359
+ */
360
+ async function generateFreshSecrets(
361
+ stage: string,
362
+ workspace: NormalizedWorkspace,
363
+ options: SetupOptions,
364
+ ) {
365
+ const secrets = createFreshWorkspaceSecrets(stage, workspace);
366
+
327
367
  // Write secrets
328
368
  await writeStageSecrets(secrets, workspace.root);
329
369
  logger.log(` Secrets written to .gkm/secrets/${stage}.json`);
package/src/test/index.ts CHANGED
@@ -21,6 +21,12 @@ export interface TestOptions {
21
21
  ui?: boolean;
22
22
  /** Pattern to filter tests */
23
23
  pattern?: string;
24
+ /**
25
+ * Generate a fresh stage (secrets + encryption key) from the workspace config
26
+ * when none exists, instead of requiring committed secrets / a shared key.
27
+ * Intended for CI. Also enabled by the GKM_AUTO_SETUP env var.
28
+ */
29
+ autoSetup?: boolean;
24
30
  }
25
31
 
26
32
  /**
@@ -33,6 +39,17 @@ export async function testCommand(options: TestOptions = {}): Promise<void> {
33
39
 
34
40
  console.log(`\n🧪 Running tests with ${stage} environment...\n`);
35
41
 
42
+ // 0. Auto-setup (CI): regenerate a fresh stage from the workspace config when
43
+ // none exists, so tests run without committed secrets or a shared key.
44
+ const autoSetup = options.autoSetup || Boolean(process.env.GKM_AUTO_SETUP);
45
+ if (autoSetup) {
46
+ const { ensureStageSecrets } = await import('../setup/index.js');
47
+ const generated = await ensureStageSecrets(stage, cwd);
48
+ if (generated) {
49
+ console.log(` 🔐 Generated fresh ${stage} secrets (auto-setup)`);
50
+ }
51
+ }
52
+
36
53
  // 1. Load .env files
37
54
  const defaultEnv = loadEnvFiles('.env');
38
55
  if (defaultEnv.loaded.length > 0) {
package/src/types.ts CHANGED
@@ -239,6 +239,8 @@ export interface GkmConfig {
239
239
  functions?: Routes;
240
240
  crons?: Routes;
241
241
  subscribers?: Routes;
242
+ queues?: Routes;
243
+ topics?: Routes;
242
244
  envParser: string;
243
245
  logger: string;
244
246
  providers?: ProvidersConfig;
@@ -347,54 +349,15 @@ export interface BuildResult {
347
349
  masterKey?: string;
348
350
  }
349
351
 
350
- export interface RouteInfo {
351
- path: string;
352
- method: string;
353
- handler: string;
354
- timeout?: number;
355
- memorySize?: number;
356
- environment?: string[];
357
- authorizer: string;
358
- }
359
-
360
- export interface FunctionInfo {
361
- name: string;
362
- handler: string;
363
- timeout?: number;
364
- memorySize?: number;
365
- environment?: string[];
366
- }
367
-
368
- export interface CronInfo {
369
- name: string;
370
- handler: string;
371
- schedule: string;
372
- timeout?: number;
373
- memorySize?: number;
374
- environment?: string[];
375
- }
376
-
377
- export interface SubscriberInfo {
378
- name: string;
379
- handler: string;
380
- subscribedEvents: string[];
381
- timeout?: number;
382
- memorySize?: number;
383
- environment?: string[];
384
- }
385
-
386
- export interface RoutesManifest {
387
- routes: RouteInfo[];
388
- }
389
-
390
- export interface FunctionsManifest {
391
- functions: FunctionInfo[];
392
- }
393
-
394
- export interface CronsManifest {
395
- crons: CronInfo[];
396
- }
397
-
398
- export interface SubscribersManifest {
399
- subscribers: SubscriberInfo[];
400
- }
352
+ // The deployment manifest types are a shared, dependency-free data contract
353
+ // in `@geekmidas/manifest`, re-exported here for back-compat with existing
354
+ // `@geekmidas/cli` imports.
355
+ export type {
356
+ CronInfo,
357
+ FunctionInfo,
358
+ Manifest,
359
+ QueueInfo,
360
+ RouteInfo,
361
+ SubscriberInfo,
362
+ TopicInfo,
363
+ } from '@geekmidas/manifest';
@@ -1,3 +0,0 @@
1
- import { CachedStateProvider } from "./CachedStateProvider-C8A-cCm5.mjs";
2
-
3
- export { CachedStateProvider };
@@ -1,3 +0,0 @@
1
- const require_dokploy_api = require('./dokploy-api-Dvyq-LsM.cjs');
2
-
3
- exports.DokployApi = require_dokploy_api.DokployApi;
@@ -1,3 +0,0 @@
1
- import { DokployApi, DokployApiError } from "./dokploy-api-BIT0683o.mjs";
2
-
3
- export { DokployApi };
@@ -1,3 +0,0 @@
1
- import { encryptSecrets, generateDefineOptions } from "./encryption-DSkIlEZ6.mjs";
2
-
3
- export { encryptSecrets, generateDefineOptions };