@geekmidas/cli 1.9.0 → 1.10.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.
- package/CHANGELOG.md +21 -0
- package/README.md +42 -6
- package/dist/{HostingerProvider-CEsQbmpY.cjs → HostingerProvider-5KYmwoK2.cjs} +1 -1
- package/dist/{HostingerProvider-CEsQbmpY.cjs.map → HostingerProvider-5KYmwoK2.cjs.map} +1 -1
- package/dist/{HostingerProvider-DkahM5AP.mjs → HostingerProvider-ANWchdiK.mjs} +1 -1
- package/dist/{HostingerProvider-DkahM5AP.mjs.map → HostingerProvider-ANWchdiK.mjs.map} +1 -1
- package/dist/{LocalStateProvider-Roi202l7.cjs → LocalStateProvider-CLifRC0Y.cjs} +1 -1
- package/dist/{LocalStateProvider-Roi202l7.cjs.map → LocalStateProvider-CLifRC0Y.cjs.map} +1 -1
- package/dist/{LocalStateProvider-DXIwWb7k.mjs → LocalStateProvider-Dp0KkRcw.mjs} +1 -1
- package/dist/{LocalStateProvider-DXIwWb7k.mjs.map → LocalStateProvider-Dp0KkRcw.mjs.map} +1 -1
- package/dist/{Route53Provider-Ckq_n5Be.mjs → Route53Provider-QoPgcXxn.mjs} +1 -1
- package/dist/{Route53Provider-Ckq_n5Be.mjs.map → Route53Provider-QoPgcXxn.mjs.map} +1 -1
- package/dist/{Route53Provider-BqXeHzuc.cjs → Route53Provider-owQQ4pn6.cjs} +1 -1
- package/dist/{Route53Provider-BqXeHzuc.cjs.map → Route53Provider-owQQ4pn6.cjs.map} +1 -1
- package/dist/{SSMStateProvider-BReQA5re.cjs → SSMStateProvider-CT8tjl9o.cjs} +1 -1
- package/dist/{SSMStateProvider-BReQA5re.cjs.map → SSMStateProvider-CT8tjl9o.cjs.map} +1 -1
- package/dist/{SSMStateProvider-wddd0_-d.mjs → SSMStateProvider-CksOTB8M.mjs} +1 -1
- package/dist/{SSMStateProvider-wddd0_-d.mjs.map → SSMStateProvider-CksOTB8M.mjs.map} +1 -1
- package/dist/{backup-provisioner-BAExdDtc.mjs → backup-provisioner-BEXoHTuC.mjs} +1 -1
- package/dist/{backup-provisioner-BAExdDtc.mjs.map → backup-provisioner-BEXoHTuC.mjs.map} +1 -1
- package/dist/{backup-provisioner-C8VK63I-.cjs → backup-provisioner-C4noe75O.cjs} +1 -1
- package/dist/{backup-provisioner-C8VK63I-.cjs.map → backup-provisioner-C4noe75O.cjs.map} +1 -1
- package/dist/{bundler-BxHyDhdt.mjs → bundler-DQYjKFPm.mjs} +1 -1
- package/dist/{bundler-BxHyDhdt.mjs.map → bundler-DQYjKFPm.mjs.map} +1 -1
- package/dist/{bundler-CuMIfXw5.cjs → bundler-NpfYPBUo.cjs} +1 -1
- package/dist/{bundler-CuMIfXw5.cjs.map → bundler-NpfYPBUo.cjs.map} +1 -1
- package/dist/{config-6JHOwLCx.cjs → config-D3ORuiUs.cjs} +2 -2
- package/dist/{config-6JHOwLCx.cjs.map → config-D3ORuiUs.cjs.map} +1 -1
- package/dist/{config-DxASSNjr.mjs → config-jsRYHOHU.mjs} +2 -2
- package/dist/{config-DxASSNjr.mjs.map → config-jsRYHOHU.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +2 -2
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/fullstack-secrets-COWz084x.cjs +238 -0
- package/dist/fullstack-secrets-COWz084x.cjs.map +1 -0
- package/dist/fullstack-secrets-UZAFWuH4.mjs +202 -0
- package/dist/fullstack-secrets-UZAFWuH4.mjs.map +1 -0
- package/dist/{index-BVNXOydm.d.mts → index-3n-giNaw.d.mts} +18 -6
- package/dist/index-3n-giNaw.d.mts.map +1 -0
- package/dist/{index-Cyk2rTyj.d.cts → index-CiEOtKEX.d.cts} +18 -6
- package/dist/index-CiEOtKEX.d.cts.map +1 -0
- package/dist/index.cjs +322 -433
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +306 -417
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CnvwSRDU.cjs → openapi-BYxAWwok.cjs} +178 -32
- package/dist/openapi-BYxAWwok.cjs.map +1 -0
- package/dist/{openapi-BYlyAbH3.mjs → openapi-DenF-okj.mjs} +148 -32
- package/dist/openapi-DenF-okj.mjs.map +1 -0
- package/dist/{openapi-react-query-DaTMSPD5.mjs → openapi-react-query-C4UdILaI.mjs} +1 -1
- package/dist/{openapi-react-query-DaTMSPD5.mjs.map → openapi-react-query-C4UdILaI.mjs.map} +1 -1
- package/dist/{openapi-react-query-BeXvk-wa.cjs → openapi-react-query-DYbBq-WJ.cjs} +1 -1
- package/dist/{openapi-react-query-BeXvk-wa.cjs.map → openapi-react-query-DYbBq-WJ.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/reconcile-7yarEvmK.cjs +36 -0
- package/dist/reconcile-7yarEvmK.cjs.map +1 -0
- package/dist/reconcile-D2WCDQue.mjs +36 -0
- package/dist/reconcile-D2WCDQue.mjs.map +1 -0
- package/dist/{sync-BnqNNc6O.mjs → sync-6FoT41G3.mjs} +1 -1
- package/dist/{sync-CHfhmXF3.mjs → sync-CbeKrnQV.mjs} +1 -1
- package/dist/{sync-CHfhmXF3.mjs.map → sync-CbeKrnQV.mjs.map} +1 -1
- package/dist/{sync-BOS0jKLn.cjs → sync-DdkKaHqP.cjs} +1 -1
- package/dist/{sync-BOS0jKLn.cjs.map → sync-DdkKaHqP.cjs.map} +1 -1
- package/dist/sync-RsnjXYwG.cjs +4 -0
- package/dist/{types-eTlj5f2M.d.mts → types-C7QJJl9f.d.cts} +6 -2
- package/dist/types-C7QJJl9f.d.cts.map +1 -0
- package/dist/{types-l53qUmGt.d.cts → types-Iqsq_FIG.d.mts} +6 -2
- package/dist/types-Iqsq_FIG.d.mts.map +1 -0
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-D2ocAlpl.cjs → workspace-4SP3Gx4Y.cjs} +11 -3
- package/dist/{workspace-D2ocAlpl.cjs.map → workspace-4SP3Gx4Y.cjs.map} +1 -1
- package/dist/{workspace-9IQIjwkQ.mjs → workspace-D4z4A4cq.mjs} +11 -3
- package/dist/{workspace-9IQIjwkQ.mjs.map → workspace-D4z4A4cq.mjs.map} +1 -1
- package/package.json +2 -2
- package/src/build/__tests__/manifests.spec.ts +171 -0
- package/src/build/__tests__/partitions.spec.ts +110 -0
- package/src/build/index.ts +58 -15
- package/src/build/manifests.ts +153 -32
- package/src/build/partitions.ts +58 -0
- package/src/deploy/sniffer.ts +6 -1
- package/src/dev/__tests__/index.spec.ts +49 -0
- package/src/dev/index.ts +84 -63
- package/src/generators/Generator.ts +27 -7
- package/src/generators/OpenApiTsGenerator.ts +4 -4
- package/src/index.ts +79 -1
- package/src/init/versions.ts +4 -4
- package/src/openapi.ts +2 -1
- package/src/secrets/__tests__/reconcile.spec.ts +123 -0
- package/src/secrets/reconcile.ts +53 -0
- package/src/setup/fullstack-secrets.ts +2 -0
- package/src/types.ts +17 -1
- package/src/workspace/client-generator.ts +6 -3
- package/src/workspace/schema.ts +13 -3
- package/dist/index-BVNXOydm.d.mts.map +0 -1
- package/dist/index-Cyk2rTyj.d.cts.map +0 -1
- package/dist/openapi-BYlyAbH3.mjs.map +0 -1
- package/dist/openapi-CnvwSRDU.cjs.map +0 -1
- package/dist/sync-BxFB34zW.cjs +0 -4
- package/dist/types-eTlj5f2M.d.mts.map +0 -1
- package/dist/types-l53qUmGt.d.cts.map +0 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Construct } from '@geekmidas/constructs';
|
|
2
|
+
import type { GeneratedConstruct } from '../generators/Generator';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PARTITION = 'default';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Group constructs by their partition field.
|
|
8
|
+
* Constructs without a partition are placed in the 'default' group.
|
|
9
|
+
*/
|
|
10
|
+
export function groupByPartition<T extends Construct>(
|
|
11
|
+
constructs: GeneratedConstruct<T>[],
|
|
12
|
+
): Map<string, GeneratedConstruct<T>[]> {
|
|
13
|
+
const groups = new Map<string, GeneratedConstruct<T>[]>();
|
|
14
|
+
|
|
15
|
+
for (const construct of constructs) {
|
|
16
|
+
const partition = construct.partition ?? DEFAULT_PARTITION;
|
|
17
|
+
const group = groups.get(partition);
|
|
18
|
+
if (group) {
|
|
19
|
+
group.push(construct);
|
|
20
|
+
} else {
|
|
21
|
+
groups.set(partition, [construct]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return groups;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if any construct across the given arrays has a non-undefined partition.
|
|
30
|
+
* When true, the manifest should use the partitioned shape for that construct type.
|
|
31
|
+
*/
|
|
32
|
+
export function hasPartitions<T extends Construct>(
|
|
33
|
+
constructs: GeneratedConstruct<T>[],
|
|
34
|
+
): boolean {
|
|
35
|
+
return constructs.some((c) => c.partition !== undefined);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Group an info array by partition, using the partition values from
|
|
40
|
+
* the corresponding construct array. Both arrays must be the same length
|
|
41
|
+
* and in the same order.
|
|
42
|
+
*/
|
|
43
|
+
export function groupInfosByPartition<T>(
|
|
44
|
+
infos: T[],
|
|
45
|
+
constructs: GeneratedConstruct<any>[],
|
|
46
|
+
): Record<string, T[]> {
|
|
47
|
+
const groups: Record<string, T[]> = {};
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < infos.length; i++) {
|
|
50
|
+
const partition = constructs[i]?.partition ?? DEFAULT_PARTITION;
|
|
51
|
+
if (!groups[partition]) {
|
|
52
|
+
groups[partition] = [];
|
|
53
|
+
}
|
|
54
|
+
groups[partition].push(infos[i]!);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return groups;
|
|
58
|
+
}
|
package/src/deploy/sniffer.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { dirname, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import type { SniffResult } from '@geekmidas/envkit/sniffer';
|
|
7
|
+
import { normalizeRoutes } from '../workspace/client-generator.js';
|
|
7
8
|
import type { NormalizedAppConfig } from '../workspace/types.js';
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -155,7 +156,11 @@ export async function sniffAppEnvironment(
|
|
|
155
156
|
|
|
156
157
|
// 4. Route-based apps - load routes and call getEnvironment() on each construct
|
|
157
158
|
if (app.routes) {
|
|
158
|
-
const result = await sniffRouteFiles(
|
|
159
|
+
const result = await sniffRouteFiles(
|
|
160
|
+
normalizeRoutes(app.routes),
|
|
161
|
+
app.path,
|
|
162
|
+
workspacePath,
|
|
163
|
+
);
|
|
159
164
|
|
|
160
165
|
if (logWarnings && result.error) {
|
|
161
166
|
console.warn(
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
checkPortConflicts,
|
|
13
13
|
findAvailablePort,
|
|
14
14
|
generateAllDependencyEnvVars,
|
|
15
|
+
generateServerEntryContent,
|
|
15
16
|
isPortAvailable,
|
|
16
17
|
loadPortState,
|
|
17
18
|
loadSecretsForApp,
|
|
@@ -1678,3 +1679,51 @@ services:
|
|
|
1678
1679
|
expect(mappings).toEqual([]);
|
|
1679
1680
|
});
|
|
1680
1681
|
});
|
|
1682
|
+
|
|
1683
|
+
describe('generateServerEntryContent', () => {
|
|
1684
|
+
it('should use dynamic import for createApp when secrets are provided', () => {
|
|
1685
|
+
const content = generateServerEntryContent({
|
|
1686
|
+
secretsJsonPath: '/tmp/dev-secrets.json',
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
// createApp must be a dynamic import so Credentials are populated first
|
|
1690
|
+
expect(content).toContain("await import('./app.js')");
|
|
1691
|
+
// Must NOT have a static import of createApp
|
|
1692
|
+
expect(content).not.toMatch(/^import.*createApp/m);
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
it('should use dynamic import for createApp without secrets', () => {
|
|
1696
|
+
const content = generateServerEntryContent({});
|
|
1697
|
+
|
|
1698
|
+
expect(content).toContain("await import('./app.js')");
|
|
1699
|
+
expect(content).not.toMatch(/^import.*createApp/m);
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
it('should inject Credentials assignment before dynamic import', () => {
|
|
1703
|
+
const content = generateServerEntryContent({
|
|
1704
|
+
secretsJsonPath: '/tmp/dev-secrets.json',
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
const credentialsAssignIdx = content.indexOf('Object.assign(Credentials');
|
|
1708
|
+
const dynamicImportIdx = content.indexOf("await import('./app.js')");
|
|
1709
|
+
|
|
1710
|
+
expect(credentialsAssignIdx).toBeGreaterThan(-1);
|
|
1711
|
+
expect(dynamicImportIdx).toBeGreaterThan(-1);
|
|
1712
|
+
expect(credentialsAssignIdx).toBeLessThan(dynamicImportIdx);
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
it('should not include credentials injection when no secrets path', () => {
|
|
1716
|
+
const content = generateServerEntryContent({});
|
|
1717
|
+
|
|
1718
|
+
expect(content).not.toContain('Object.assign(Credentials');
|
|
1719
|
+
expect(content).not.toContain('existsSync');
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
it('should use custom app import path when provided', () => {
|
|
1723
|
+
const content = generateServerEntryContent({
|
|
1724
|
+
appImportPath: './custom-app.js',
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
expect(content).toContain("await import('./custom-app.js')");
|
|
1728
|
+
});
|
|
1729
|
+
});
|
package/src/dev/index.ts
CHANGED
|
@@ -1832,6 +1832,85 @@ class EntryRunner {
|
|
|
1832
1832
|
}
|
|
1833
1833
|
}
|
|
1834
1834
|
|
|
1835
|
+
/**
|
|
1836
|
+
* Generate the content of the dev server entry file (server.ts).
|
|
1837
|
+
* Uses dynamic import for createApp so Credentials are populated
|
|
1838
|
+
* before any app modules evaluate.
|
|
1839
|
+
* @internal Exported for testing
|
|
1840
|
+
*/
|
|
1841
|
+
export function generateServerEntryContent(options: {
|
|
1842
|
+
secretsJsonPath?: string;
|
|
1843
|
+
runtime?: Runtime;
|
|
1844
|
+
enableOpenApi?: boolean;
|
|
1845
|
+
appImportPath?: string;
|
|
1846
|
+
}): string {
|
|
1847
|
+
const {
|
|
1848
|
+
secretsJsonPath,
|
|
1849
|
+
runtime = 'node',
|
|
1850
|
+
enableOpenApi = false,
|
|
1851
|
+
appImportPath = './app.js',
|
|
1852
|
+
} = options;
|
|
1853
|
+
|
|
1854
|
+
const credentialsInjection = secretsJsonPath
|
|
1855
|
+
? `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
1856
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
1857
|
+
|
|
1858
|
+
// Inject dev secrets into Credentials (must happen before app import)
|
|
1859
|
+
const secretsPath = '${secretsJsonPath}';
|
|
1860
|
+
if (existsSync(secretsPath)) {
|
|
1861
|
+
Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
`
|
|
1865
|
+
: '';
|
|
1866
|
+
|
|
1867
|
+
const serveCode =
|
|
1868
|
+
runtime === 'bun'
|
|
1869
|
+
? `Bun.serve({
|
|
1870
|
+
port,
|
|
1871
|
+
fetch: app.fetch,
|
|
1872
|
+
});`
|
|
1873
|
+
: `const { serve } = await import('@hono/node-server');
|
|
1874
|
+
const server = serve({
|
|
1875
|
+
fetch: app.fetch,
|
|
1876
|
+
port,
|
|
1877
|
+
});
|
|
1878
|
+
// Inject WebSocket support if available
|
|
1879
|
+
const injectWs = (app as any).__injectWebSocket;
|
|
1880
|
+
if (injectWs) {
|
|
1881
|
+
injectWs(server);
|
|
1882
|
+
console.log('🔌 Telescope real-time updates enabled');
|
|
1883
|
+
}`;
|
|
1884
|
+
|
|
1885
|
+
return `#!/usr/bin/env node
|
|
1886
|
+
/**
|
|
1887
|
+
* Development server entry point
|
|
1888
|
+
* This file is auto-generated by 'gkm dev'
|
|
1889
|
+
*/
|
|
1890
|
+
${credentialsInjection}
|
|
1891
|
+
const port = process.argv.includes('--port')
|
|
1892
|
+
? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
|
|
1893
|
+
: 3000;
|
|
1894
|
+
|
|
1895
|
+
// Dynamic import so Credentials are populated before env.ts evaluates
|
|
1896
|
+
const { createApp } = await import('${appImportPath}');
|
|
1897
|
+
|
|
1898
|
+
// createApp is async to support optional WebSocket setup
|
|
1899
|
+
const { app, start } = await createApp(undefined, ${enableOpenApi});
|
|
1900
|
+
|
|
1901
|
+
// Start the server
|
|
1902
|
+
start({
|
|
1903
|
+
port,
|
|
1904
|
+
serve: async (app, port) => {
|
|
1905
|
+
${serveCode}
|
|
1906
|
+
},
|
|
1907
|
+
}).catch((error) => {
|
|
1908
|
+
console.error('Failed to start server:', error);
|
|
1909
|
+
process.exit(1);
|
|
1910
|
+
});
|
|
1911
|
+
`;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1835
1914
|
class DevServer {
|
|
1836
1915
|
private serverProcess: ChildProcess | null = null;
|
|
1837
1916
|
private isRunning = false;
|
|
@@ -1997,72 +2076,14 @@ class DevServer {
|
|
|
1997
2076
|
|
|
1998
2077
|
private async createServerEntry(): Promise<void> {
|
|
1999
2078
|
const { writeFile: fsWriteFile } = await import('node:fs/promises');
|
|
2000
|
-
const { relative, dirname } = await import('node:path');
|
|
2001
2079
|
|
|
2002
2080
|
const serverPath = join(this.appRoot, '.gkm', this.provider, 'server.ts');
|
|
2003
2081
|
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
// Generate credentials injection code if secrets are available
|
|
2010
|
-
const credentialsInjection = this.secretsJsonPath
|
|
2011
|
-
? `import { Credentials } from '@geekmidas/envkit/credentials';
|
|
2012
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2013
|
-
|
|
2014
|
-
// Inject dev secrets into Credentials (must happen before app import)
|
|
2015
|
-
const secretsPath = '${this.secretsJsonPath}';
|
|
2016
|
-
if (existsSync(secretsPath)) {
|
|
2017
|
-
Object.assign(Credentials, JSON.parse(readFileSync(secretsPath, 'utf-8')));
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
`
|
|
2021
|
-
: '';
|
|
2022
|
-
|
|
2023
|
-
const serveCode =
|
|
2024
|
-
this.runtime === 'bun'
|
|
2025
|
-
? `Bun.serve({
|
|
2026
|
-
port,
|
|
2027
|
-
fetch: app.fetch,
|
|
2028
|
-
});`
|
|
2029
|
-
: `const { serve } = await import('@hono/node-server');
|
|
2030
|
-
const server = serve({
|
|
2031
|
-
fetch: app.fetch,
|
|
2032
|
-
port,
|
|
2033
|
-
});
|
|
2034
|
-
// Inject WebSocket support if available
|
|
2035
|
-
const injectWs = (app as any).__injectWebSocket;
|
|
2036
|
-
if (injectWs) {
|
|
2037
|
-
injectWs(server);
|
|
2038
|
-
console.log('🔌 Telescope real-time updates enabled');
|
|
2039
|
-
}`;
|
|
2040
|
-
|
|
2041
|
-
const content = `#!/usr/bin/env node
|
|
2042
|
-
/**
|
|
2043
|
-
* Development server entry point
|
|
2044
|
-
* This file is auto-generated by 'gkm dev'
|
|
2045
|
-
*/
|
|
2046
|
-
${credentialsInjection}import { createApp } from './${relativeAppPath.startsWith('.') ? relativeAppPath : `./${relativeAppPath}`}';
|
|
2047
|
-
|
|
2048
|
-
const port = process.argv.includes('--port')
|
|
2049
|
-
? Number.parseInt(process.argv[process.argv.indexOf('--port') + 1])
|
|
2050
|
-
: 3000;
|
|
2051
|
-
|
|
2052
|
-
// createApp is async to support optional WebSocket setup
|
|
2053
|
-
const { app, start } = await createApp(undefined, ${this.enableOpenApi});
|
|
2054
|
-
|
|
2055
|
-
// Start the server
|
|
2056
|
-
start({
|
|
2057
|
-
port,
|
|
2058
|
-
serve: async (app, port) => {
|
|
2059
|
-
${serveCode}
|
|
2060
|
-
},
|
|
2061
|
-
}).catch((error) => {
|
|
2062
|
-
console.error('Failed to start server:', error);
|
|
2063
|
-
process.exit(1);
|
|
2064
|
-
});
|
|
2065
|
-
`;
|
|
2082
|
+
const content = generateServerEntryContent({
|
|
2083
|
+
secretsJsonPath: this.secretsJsonPath,
|
|
2084
|
+
runtime: this.runtime,
|
|
2085
|
+
enableOpenApi: this.enableOpenApi,
|
|
2086
|
+
});
|
|
2066
2087
|
|
|
2067
2088
|
await fsWriteFile(serverPath, content);
|
|
2068
2089
|
}
|
|
@@ -3,7 +3,11 @@ import type { Construct } from '@geekmidas/constructs';
|
|
|
3
3
|
import fg from 'fast-glob';
|
|
4
4
|
import kebabCase from 'lodash.kebabcase';
|
|
5
5
|
import type { BuildContext } from '../build/types';
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
isPartitionedRoutes,
|
|
8
|
+
type LegacyProvider,
|
|
9
|
+
type Routes,
|
|
10
|
+
} from '../types';
|
|
7
11
|
|
|
8
12
|
export interface GeneratorOptions {
|
|
9
13
|
provider?: LegacyProvider;
|
|
@@ -38,12 +42,22 @@ export abstract class ConstructGenerator<T extends Construct, R = void> {
|
|
|
38
42
|
): Promise<GeneratedConstruct<T>[]> {
|
|
39
43
|
const logger = console;
|
|
40
44
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Extract glob patterns and optional partition function
|
|
46
|
+
let globPatterns: string[];
|
|
47
|
+
let partitionFn: ((filepath: string) => string) | undefined;
|
|
48
|
+
|
|
49
|
+
if (isPartitionedRoutes(patterns)) {
|
|
50
|
+
globPatterns = Array.isArray(patterns.paths)
|
|
51
|
+
? patterns.paths
|
|
52
|
+
: [patterns.paths];
|
|
53
|
+
partitionFn = patterns.partition;
|
|
54
|
+
} else {
|
|
55
|
+
globPatterns = Array.isArray(patterns)
|
|
56
|
+
? patterns
|
|
57
|
+
: patterns
|
|
58
|
+
? [patterns]
|
|
59
|
+
: [];
|
|
60
|
+
}
|
|
47
61
|
|
|
48
62
|
// Find all files
|
|
49
63
|
const files = fg.stream(globPatterns, {
|
|
@@ -61,6 +75,9 @@ export abstract class ConstructGenerator<T extends Construct, R = void> {
|
|
|
61
75
|
const importPath = bustCache ? `${file}?t=${Date.now()}` : file;
|
|
62
76
|
const module = await import(importPath);
|
|
63
77
|
|
|
78
|
+
// Compute partition name for this file (if partition function provided)
|
|
79
|
+
const partition = partitionFn ? partitionFn(file) : undefined;
|
|
80
|
+
|
|
64
81
|
// Check all exports for constructs
|
|
65
82
|
for (const [key, construct] of Object.entries(module)) {
|
|
66
83
|
if (this.isConstruct(construct)) {
|
|
@@ -72,6 +89,7 @@ export abstract class ConstructGenerator<T extends Construct, R = void> {
|
|
|
72
89
|
absolute: file,
|
|
73
90
|
relative: relative(process.cwd(), file),
|
|
74
91
|
},
|
|
92
|
+
partition,
|
|
75
93
|
});
|
|
76
94
|
}
|
|
77
95
|
}
|
|
@@ -95,4 +113,6 @@ export interface GeneratedConstruct<T extends Construct> {
|
|
|
95
113
|
absolute: string;
|
|
96
114
|
relative: string;
|
|
97
115
|
};
|
|
116
|
+
/** Partition name assigned by the partition function, if configured. */
|
|
117
|
+
partition?: string;
|
|
98
118
|
}
|
|
@@ -681,7 +681,7 @@ export function createApi(options: CreateApiOptions) {
|
|
|
681
681
|
// API Client Factory
|
|
682
682
|
// ============================================================
|
|
683
683
|
|
|
684
|
-
import {
|
|
684
|
+
import { createTypedFetcher, type FetcherOptions } from '@geekmidas/client/fetcher';
|
|
685
685
|
import { createEndpointHooks } from '@geekmidas/client/endpoint-hooks';
|
|
686
686
|
import type { QueryClient } from '@tanstack/react-query';
|
|
687
687
|
|
|
@@ -713,11 +713,11 @@ export interface CreateApiOptions extends Omit<FetcherOptions, 'baseURL'> {
|
|
|
713
713
|
*/
|
|
714
714
|
export function createApi(options: CreateApiOptions) {
|
|
715
715
|
const { queryClient, ...fetcherOptions } = options;
|
|
716
|
-
const
|
|
716
|
+
const request = createTypedFetcher<paths>(fetcherOptions);
|
|
717
717
|
|
|
718
|
-
const hooks = createEndpointHooks<paths>(
|
|
718
|
+
const hooks = createEndpointHooks<paths>(request, { queryClient });
|
|
719
719
|
|
|
720
|
-
return Object.assign(
|
|
720
|
+
return Object.assign(request, hooks);
|
|
721
721
|
}
|
|
722
722
|
`;
|
|
723
723
|
|
package/src/index.ts
CHANGED
|
@@ -518,8 +518,27 @@ program
|
|
|
518
518
|
|
|
519
519
|
const { loadWorkspaceConfig } = await import('./config');
|
|
520
520
|
const { pushSecrets } = await import('./secrets/sync');
|
|
521
|
+
const { reconcileMissingSecrets } = await import('./secrets/reconcile');
|
|
522
|
+
const { readStageSecrets, writeStageSecrets } = await import(
|
|
523
|
+
'./secrets/storage'
|
|
524
|
+
);
|
|
521
525
|
|
|
522
526
|
const { workspace } = await loadWorkspaceConfig();
|
|
527
|
+
|
|
528
|
+
const secrets = await readStageSecrets(options.stage, workspace.root);
|
|
529
|
+
if (secrets) {
|
|
530
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
531
|
+
if (result) {
|
|
532
|
+
await writeStageSecrets(result.secrets, workspace.root);
|
|
533
|
+
console.log(
|
|
534
|
+
` Reconciled ${result.addedKeys.length} missing secret(s):`,
|
|
535
|
+
);
|
|
536
|
+
for (const key of result.addedKeys) {
|
|
537
|
+
console.log(` + ${key}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
523
542
|
await pushSecrets(options.stage, workspace);
|
|
524
543
|
console.log(`\n✓ Secrets pushed for stage "${options.stage}"`);
|
|
525
544
|
} catch (error) {
|
|
@@ -542,15 +561,27 @@ program
|
|
|
542
561
|
const { loadWorkspaceConfig } = await import('./config');
|
|
543
562
|
const { pullSecrets } = await import('./secrets/sync');
|
|
544
563
|
const { writeStageSecrets } = await import('./secrets/storage');
|
|
564
|
+
const { reconcileMissingSecrets } = await import('./secrets/reconcile');
|
|
545
565
|
|
|
546
566
|
const { workspace } = await loadWorkspaceConfig();
|
|
547
|
-
|
|
567
|
+
let secrets = await pullSecrets(options.stage, workspace);
|
|
548
568
|
|
|
549
569
|
if (!secrets) {
|
|
550
570
|
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
551
571
|
process.exit(1);
|
|
552
572
|
}
|
|
553
573
|
|
|
574
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
575
|
+
if (result) {
|
|
576
|
+
secrets = result.secrets;
|
|
577
|
+
console.log(
|
|
578
|
+
` Reconciled ${result.addedKeys.length} missing secret(s):`,
|
|
579
|
+
);
|
|
580
|
+
for (const key of result.addedKeys) {
|
|
581
|
+
console.log(` + ${key}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
554
585
|
await writeStageSecrets(secrets, workspace.root);
|
|
555
586
|
console.log(`\n✓ Secrets pulled for stage "${options.stage}"`);
|
|
556
587
|
} catch (error) {
|
|
@@ -559,6 +590,53 @@ program
|
|
|
559
590
|
}
|
|
560
591
|
});
|
|
561
592
|
|
|
593
|
+
program
|
|
594
|
+
.command('secrets:reconcile')
|
|
595
|
+
.description('Backfill missing custom secrets from workspace config')
|
|
596
|
+
.option('--stage <stage>', 'Stage name', 'development')
|
|
597
|
+
.action(async (options: { stage: string }) => {
|
|
598
|
+
try {
|
|
599
|
+
const globalOptions = program.opts();
|
|
600
|
+
if (globalOptions.cwd) {
|
|
601
|
+
process.chdir(globalOptions.cwd);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const { loadWorkspaceConfig } = await import('./config');
|
|
605
|
+
const { reconcileMissingSecrets } = await import('./secrets/reconcile');
|
|
606
|
+
const { readStageSecrets, writeStageSecrets } = await import(
|
|
607
|
+
'./secrets/storage'
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
611
|
+
const secrets = await readStageSecrets(options.stage, workspace.root);
|
|
612
|
+
|
|
613
|
+
if (!secrets) {
|
|
614
|
+
console.error(
|
|
615
|
+
`No secrets found for stage "${options.stage}". Run "gkm secrets:init --stage ${options.stage}" first.`,
|
|
616
|
+
);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
621
|
+
|
|
622
|
+
if (!result) {
|
|
623
|
+
console.log(`\n✓ Secrets for stage "${options.stage}" are up-to-date`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
await writeStageSecrets(result.secrets, workspace.root);
|
|
628
|
+
console.log(
|
|
629
|
+
`\n✓ Reconciled ${result.addedKeys.length} missing secret(s) for stage "${options.stage}":`,
|
|
630
|
+
);
|
|
631
|
+
for (const key of result.addedKeys) {
|
|
632
|
+
console.log(` + ${key}`);
|
|
633
|
+
}
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
562
640
|
// Deploy command
|
|
563
641
|
program
|
|
564
642
|
.command('deploy')
|
package/src/init/versions.ts
CHANGED
|
@@ -30,14 +30,14 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
30
30
|
'@geekmidas/audit': '~1.0.0',
|
|
31
31
|
'@geekmidas/auth': '~1.0.0',
|
|
32
32
|
'@geekmidas/cache': '~1.0.0',
|
|
33
|
-
'@geekmidas/client': '~
|
|
33
|
+
'@geekmidas/client': '~3.0.0',
|
|
34
34
|
'@geekmidas/cloud': '~1.0.0',
|
|
35
|
-
'@geekmidas/constructs': '~
|
|
35
|
+
'@geekmidas/constructs': '~2.0.0',
|
|
36
36
|
'@geekmidas/db': '~1.0.0',
|
|
37
37
|
'@geekmidas/emailkit': '~1.0.0',
|
|
38
38
|
'@geekmidas/envkit': '~1.0.3',
|
|
39
39
|
'@geekmidas/errors': '~1.0.0',
|
|
40
|
-
'@geekmidas/events': '~1.
|
|
40
|
+
'@geekmidas/events': '~1.1.0',
|
|
41
41
|
'@geekmidas/logger': '~1.0.0',
|
|
42
42
|
'@geekmidas/rate-limit': '~1.0.0',
|
|
43
43
|
'@geekmidas/schema': '~1.0.0',
|
|
@@ -45,7 +45,7 @@ export const GEEKMIDAS_VERSIONS = {
|
|
|
45
45
|
'@geekmidas/storage': '~1.0.0',
|
|
46
46
|
'@geekmidas/studio': '~1.0.0',
|
|
47
47
|
'@geekmidas/telescope': '~1.0.0',
|
|
48
|
-
'@geekmidas/testkit': '~1.0.
|
|
48
|
+
'@geekmidas/testkit': '~1.0.2',
|
|
49
49
|
'@geekmidas/cli': CLI_VERSION,
|
|
50
50
|
} as const;
|
|
51
51
|
|
package/src/openapi.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { loadWorkspaceConfig } from './config.js';
|
|
|
6
6
|
import { EndpointGenerator } from './generators/EndpointGenerator.js';
|
|
7
7
|
import { OpenApiTsGenerator } from './generators/OpenApiTsGenerator.js';
|
|
8
8
|
import type { GkmConfig, OpenApiConfig } from './types.js';
|
|
9
|
+
import { normalizeRoutes } from './workspace/client-generator.js';
|
|
9
10
|
|
|
10
11
|
interface OpenAPIOptions {
|
|
11
12
|
cwd?: string;
|
|
@@ -141,7 +142,7 @@ export async function openapiCommand(
|
|
|
141
142
|
if (app.type !== 'backend' || !app.routes) continue;
|
|
142
143
|
|
|
143
144
|
const appPath = join(workspaceRoot, app.path);
|
|
144
|
-
const routes =
|
|
145
|
+
const routes = normalizeRoutes(app.routes);
|
|
145
146
|
const routesGlob = routes.map((r) => join(appPath, r));
|
|
146
147
|
|
|
147
148
|
const gkmConfig: GkmConfig = {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateFullstackCustomSecrets } from '../../setup/fullstack-secrets';
|
|
3
|
+
import type { NormalizedWorkspace } from '../../workspace/types';
|
|
4
|
+
import { reconcileMissingSecrets } from '../reconcile';
|
|
5
|
+
import type { StageSecrets } from '../types';
|
|
6
|
+
|
|
7
|
+
function createMultiAppWorkspace(
|
|
8
|
+
overrides?: Partial<NormalizedWorkspace>,
|
|
9
|
+
): NormalizedWorkspace {
|
|
10
|
+
return {
|
|
11
|
+
name: 'test-project',
|
|
12
|
+
root: '/tmp/test',
|
|
13
|
+
apps: {
|
|
14
|
+
api: {
|
|
15
|
+
type: 'backend',
|
|
16
|
+
path: 'apps/api',
|
|
17
|
+
port: 3000,
|
|
18
|
+
dependencies: [],
|
|
19
|
+
resolvedDeployTarget: 'dokploy',
|
|
20
|
+
},
|
|
21
|
+
web: {
|
|
22
|
+
type: 'frontend',
|
|
23
|
+
path: 'apps/web',
|
|
24
|
+
port: 3001,
|
|
25
|
+
dependencies: ['api'],
|
|
26
|
+
resolvedDeployTarget: 'dokploy',
|
|
27
|
+
framework: 'nextjs',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
services: { db: true },
|
|
31
|
+
deploy: {},
|
|
32
|
+
shared: {},
|
|
33
|
+
secrets: {},
|
|
34
|
+
...overrides,
|
|
35
|
+
} as NormalizedWorkspace;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createSecrets(custom: Record<string, string> = {}): StageSecrets {
|
|
39
|
+
return {
|
|
40
|
+
stage: 'development',
|
|
41
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
42
|
+
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
43
|
+
services: {},
|
|
44
|
+
urls: {},
|
|
45
|
+
custom,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('reconcileMissingSecrets', () => {
|
|
50
|
+
it('should return null for single-app workspace', () => {
|
|
51
|
+
const workspace = createMultiAppWorkspace({
|
|
52
|
+
apps: {
|
|
53
|
+
api: {
|
|
54
|
+
type: 'backend',
|
|
55
|
+
path: 'apps/api',
|
|
56
|
+
port: 3000,
|
|
57
|
+
dependencies: [],
|
|
58
|
+
resolvedDeployTarget: 'dokploy',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
}) as NormalizedWorkspace;
|
|
62
|
+
|
|
63
|
+
const result = reconcileMissingSecrets(createSecrets(), workspace);
|
|
64
|
+
expect(result).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return null when no keys are missing', () => {
|
|
68
|
+
const workspace = createMultiAppWorkspace();
|
|
69
|
+
const expected = generateFullstackCustomSecrets(workspace);
|
|
70
|
+
|
|
71
|
+
const result = reconcileMissingSecrets(createSecrets(expected), workspace);
|
|
72
|
+
expect(result).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should backfill missing keys', () => {
|
|
76
|
+
const workspace = createMultiAppWorkspace();
|
|
77
|
+
const secrets = createSecrets();
|
|
78
|
+
|
|
79
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
80
|
+
|
|
81
|
+
expect(result).not.toBeNull();
|
|
82
|
+
expect(result!.addedKeys.length).toBeGreaterThan(0);
|
|
83
|
+
for (const key of result!.addedKeys) {
|
|
84
|
+
expect(result!.secrets.custom[key]).toBeDefined();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should never overwrite existing keys', () => {
|
|
89
|
+
const workspace = createMultiAppWorkspace();
|
|
90
|
+
const existingValue = 'my-custom-jwt-secret';
|
|
91
|
+
const secrets = createSecrets({
|
|
92
|
+
JWT_SECRET: existingValue,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
96
|
+
|
|
97
|
+
expect(result).not.toBeNull();
|
|
98
|
+
expect(result!.secrets.custom.JWT_SECRET).toBe(existingValue);
|
|
99
|
+
expect(result!.addedKeys).not.toContain('JWT_SECRET');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should update updatedAt timestamp', () => {
|
|
103
|
+
const workspace = createMultiAppWorkspace();
|
|
104
|
+
const secrets = createSecrets();
|
|
105
|
+
const originalUpdatedAt = secrets.updatedAt;
|
|
106
|
+
|
|
107
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
108
|
+
|
|
109
|
+
expect(result).not.toBeNull();
|
|
110
|
+
expect(result!.secrets.updatedAt).not.toBe(originalUpdatedAt);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should backfill frontend URL keys', () => {
|
|
114
|
+
const workspace = createMultiAppWorkspace();
|
|
115
|
+
const secrets = createSecrets();
|
|
116
|
+
|
|
117
|
+
const result = reconcileMissingSecrets(secrets, workspace);
|
|
118
|
+
|
|
119
|
+
expect(result).not.toBeNull();
|
|
120
|
+
expect(result!.secrets.custom.WEB_URL).toBe('http://localhost:3001');
|
|
121
|
+
expect(result!.addedKeys).toContain('WEB_URL');
|
|
122
|
+
});
|
|
123
|
+
});
|