@geekmidas/cli 1.8.0 → 1.9.1
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/dist/{HostingerProvider-BiXdHjiq.cjs → HostingerProvider-5KYmwoK2.cjs} +1 -1
- package/dist/{HostingerProvider-BiXdHjiq.cjs.map → HostingerProvider-5KYmwoK2.cjs.map} +1 -1
- package/dist/{HostingerProvider-402UdK89.mjs → HostingerProvider-ANWchdiK.mjs} +1 -1
- package/dist/{HostingerProvider-402UdK89.mjs.map → HostingerProvider-ANWchdiK.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CdspeSVL.cjs → LocalStateProvider-CLifRC0Y.cjs} +1 -1
- package/dist/{LocalStateProvider-CdspeSVL.cjs.map → LocalStateProvider-CLifRC0Y.cjs.map} +1 -1
- package/dist/{LocalStateProvider-BDm7ZqJo.mjs → LocalStateProvider-Dp0KkRcw.mjs} +1 -1
- package/dist/{LocalStateProvider-BDm7ZqJo.mjs.map → LocalStateProvider-Dp0KkRcw.mjs.map} +1 -1
- package/dist/{Route53Provider-DbBo7Uz5.mjs → Route53Provider-QoPgcXxn.mjs} +1 -1
- package/dist/{Route53Provider-DbBo7Uz5.mjs.map → Route53Provider-QoPgcXxn.mjs.map} +1 -1
- package/dist/{Route53Provider-kfJ77LmL.cjs → Route53Provider-owQQ4pn6.cjs} +1 -1
- package/dist/{Route53Provider-kfJ77LmL.cjs.map → Route53Provider-owQQ4pn6.cjs.map} +1 -1
- package/dist/{SSMStateProvider-DGrqYll0.cjs → SSMStateProvider-CT8tjl9o.cjs} +1 -1
- package/dist/{SSMStateProvider-DGrqYll0.cjs.map → SSMStateProvider-CT8tjl9o.cjs.map} +1 -1
- package/dist/{SSMStateProvider-DT0WV-E_.mjs → SSMStateProvider-CksOTB8M.mjs} +1 -1
- package/dist/{SSMStateProvider-DT0WV-E_.mjs.map → SSMStateProvider-CksOTB8M.mjs.map} +1 -1
- package/dist/{backup-provisioner-BIArpmTr.mjs → backup-provisioner-BEXoHTuC.mjs} +1 -1
- package/dist/{backup-provisioner-BIArpmTr.mjs.map → backup-provisioner-BEXoHTuC.mjs.map} +1 -1
- package/dist/{backup-provisioner-B5e-F6zX.cjs → backup-provisioner-C4noe75O.cjs} +1 -1
- package/dist/{backup-provisioner-B5e-F6zX.cjs.map → backup-provisioner-C4noe75O.cjs.map} +1 -1
- package/dist/{bundler-DgXsOSxc.mjs → bundler-DQYjKFPm.mjs} +1 -1
- package/dist/{bundler-DgXsOSxc.mjs.map → bundler-DQYjKFPm.mjs.map} +1 -1
- package/dist/{bundler-tHLLwYuU.cjs → bundler-NpfYPBUo.cjs} +1 -1
- package/dist/{bundler-tHLLwYuU.cjs.map → bundler-NpfYPBUo.cjs.map} +1 -1
- package/dist/config.d.mts +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-C-KxSGGK.d.mts → index-Bt2kX0-R.d.mts} +2 -2
- package/dist/{index-C-KxSGGK.d.mts.map → index-Bt2kX0-R.d.mts.map} +1 -1
- package/dist/index.cjs +1063 -730
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1054 -721
- package/dist/index.mjs.map +1 -1
- 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.d.mts +1 -1
- 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-6FoT41G3.mjs +3 -0
- package/dist/sync-CbeKrnQV.mjs +76 -0
- package/dist/sync-CbeKrnQV.mjs.map +1 -0
- package/dist/sync-DdkKaHqP.cjs +93 -0
- package/dist/sync-DdkKaHqP.cjs.map +1 -0
- package/dist/sync-RsnjXYwG.cjs +4 -0
- package/dist/{types-CZg5iUgD.d.mts → types-wXMIMOyK.d.mts} +1 -1
- package/dist/{types-CZg5iUgD.d.mts.map → types-wXMIMOyK.d.mts.map} +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +5 -5
- package/src/dev/__tests__/index.spec.ts +49 -0
- package/src/dev/index.ts +85 -64
- package/src/generators/SubscriberGenerator.ts +1 -0
- package/src/index.ts +171 -0
- package/src/init/index.ts +4 -23
- package/src/init/utils.ts +103 -2
- package/src/init/versions.ts +4 -4
- package/src/secrets/__tests__/reconcile.spec.ts +123 -0
- package/src/secrets/index.ts +20 -1
- package/src/secrets/reconcile.ts +53 -0
- package/src/secrets/sync.ts +136 -0
- package/src/setup/fullstack-secrets.ts +123 -0
- package/src/setup/index.ts +212 -0
- package/src/test/__tests__/web.spec.ts +1 -1
- package/src/upgrade/__tests__/index.spec.ts +354 -0
- package/src/upgrade/index.ts +253 -0
package/src/dev/index.ts
CHANGED
|
@@ -1041,7 +1041,7 @@ export async function loadDevSecrets(
|
|
|
1041
1041
|
}
|
|
1042
1042
|
|
|
1043
1043
|
logger.warn(
|
|
1044
|
-
'⚠️ Secrets enabled but no dev/development secrets found. Run "gkm
|
|
1044
|
+
'⚠️ Secrets enabled but no dev/development secrets found. Run "gkm setup" to initialize your development environment',
|
|
1045
1045
|
);
|
|
1046
1046
|
return {};
|
|
1047
1047
|
}
|
|
@@ -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
|
}
|
|
@@ -164,6 +164,7 @@ export const handler = adapter.handler;
|
|
|
164
164
|
* - sqs://region/account-id/queue-name (SQS queue)
|
|
165
165
|
* - sns://region/account-id/topic-name (SNS topic)
|
|
166
166
|
* - rabbitmq://host:port/queue-name (RabbitMQ)
|
|
167
|
+
* - pgboss://user:pass@host:port/database (pg-boss / PostgreSQL)
|
|
167
168
|
* - basic://in-memory (In-memory for testing)
|
|
168
169
|
*/
|
|
169
170
|
import type { EnvironmentParser } from '@geekmidas/envkit';
|
package/src/index.ts
CHANGED
|
@@ -24,8 +24,10 @@ import {
|
|
|
24
24
|
secretsSetCommand,
|
|
25
25
|
secretsShowCommand,
|
|
26
26
|
} from './secrets';
|
|
27
|
+
import { type SetupOptions, setupCommand } from './setup/index';
|
|
27
28
|
import { type TestOptions, testCommand } from './test/index';
|
|
28
29
|
import type { ComposeServiceName, LegacyProvider, MainProvider } from './types';
|
|
30
|
+
import { type UpgradeOptions, upgradeCommand } from './upgrade/index';
|
|
29
31
|
|
|
30
32
|
const program = new Command();
|
|
31
33
|
|
|
@@ -61,6 +63,26 @@ program
|
|
|
61
63
|
}
|
|
62
64
|
});
|
|
63
65
|
|
|
66
|
+
program
|
|
67
|
+
.command('setup')
|
|
68
|
+
.description('Setup development environment (secrets, Docker, database)')
|
|
69
|
+
.option('--stage <stage>', 'Stage name', 'development')
|
|
70
|
+
.option('--force', 'Regenerate secrets even if they exist')
|
|
71
|
+
.option('--skip-docker', 'Skip starting Docker services')
|
|
72
|
+
.option('-y, --yes', 'Skip prompts')
|
|
73
|
+
.action(async (options: SetupOptions) => {
|
|
74
|
+
try {
|
|
75
|
+
const globalOptions = program.opts();
|
|
76
|
+
if (globalOptions.cwd) {
|
|
77
|
+
process.chdir(globalOptions.cwd);
|
|
78
|
+
}
|
|
79
|
+
await setupCommand(options);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
64
86
|
program
|
|
65
87
|
.command('build')
|
|
66
88
|
.description('Build handlers from endpoints, functions, and crons')
|
|
@@ -483,6 +505,138 @@ program
|
|
|
483
505
|
}
|
|
484
506
|
});
|
|
485
507
|
|
|
508
|
+
program
|
|
509
|
+
.command('secrets:push')
|
|
510
|
+
.description('Push secrets to remote provider (SSM)')
|
|
511
|
+
.requiredOption('--stage <stage>', 'Stage name')
|
|
512
|
+
.action(async (options: { stage: string }) => {
|
|
513
|
+
try {
|
|
514
|
+
const globalOptions = program.opts();
|
|
515
|
+
if (globalOptions.cwd) {
|
|
516
|
+
process.chdir(globalOptions.cwd);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const { loadWorkspaceConfig } = await import('./config');
|
|
520
|
+
const { pushSecrets } = await import('./secrets/sync');
|
|
521
|
+
const { reconcileMissingSecrets } = await import('./secrets/reconcile');
|
|
522
|
+
const { readStageSecrets, writeStageSecrets } = await import(
|
|
523
|
+
'./secrets/storage'
|
|
524
|
+
);
|
|
525
|
+
|
|
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
|
+
|
|
542
|
+
await pushSecrets(options.stage, workspace);
|
|
543
|
+
console.log(`\n✓ Secrets pushed for stage "${options.stage}"`);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
program
|
|
551
|
+
.command('secrets:pull')
|
|
552
|
+
.description('Pull secrets from remote provider (SSM)')
|
|
553
|
+
.requiredOption('--stage <stage>', 'Stage name')
|
|
554
|
+
.action(async (options: { stage: string }) => {
|
|
555
|
+
try {
|
|
556
|
+
const globalOptions = program.opts();
|
|
557
|
+
if (globalOptions.cwd) {
|
|
558
|
+
process.chdir(globalOptions.cwd);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const { loadWorkspaceConfig } = await import('./config');
|
|
562
|
+
const { pullSecrets } = await import('./secrets/sync');
|
|
563
|
+
const { writeStageSecrets } = await import('./secrets/storage');
|
|
564
|
+
const { reconcileMissingSecrets } = await import('./secrets/reconcile');
|
|
565
|
+
|
|
566
|
+
const { workspace } = await loadWorkspaceConfig();
|
|
567
|
+
let secrets = await pullSecrets(options.stage, workspace);
|
|
568
|
+
|
|
569
|
+
if (!secrets) {
|
|
570
|
+
console.error(`No remote secrets found for stage "${options.stage}".`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
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
|
+
|
|
585
|
+
await writeStageSecrets(secrets, workspace.root);
|
|
586
|
+
console.log(`\n✓ Secrets pulled for stage "${options.stage}"`);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
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
|
+
|
|
486
640
|
// Deploy command
|
|
487
641
|
program
|
|
488
642
|
.command('deploy')
|
|
@@ -798,4 +952,21 @@ program
|
|
|
798
952
|
}
|
|
799
953
|
});
|
|
800
954
|
|
|
955
|
+
program
|
|
956
|
+
.command('upgrade')
|
|
957
|
+
.description('Upgrade all @geekmidas packages to their latest versions')
|
|
958
|
+
.option('--dry-run', 'Show what would be upgraded without making changes')
|
|
959
|
+
.action(async (options: UpgradeOptions) => {
|
|
960
|
+
try {
|
|
961
|
+
const globalOptions = program.opts();
|
|
962
|
+
if (globalOptions.cwd) {
|
|
963
|
+
process.chdir(globalOptions.cwd);
|
|
964
|
+
}
|
|
965
|
+
await upgradeCommand(options);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
console.error(error instanceof Error ? error.message : 'Command failed');
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
801
972
|
program.parse();
|
package/src/init/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ import prompts from 'prompts';
|
|
|
5
5
|
import { createStageSecrets } from '../secrets/generator.js';
|
|
6
6
|
import { getKeyPath } from '../secrets/keystore.js';
|
|
7
7
|
import { writeStageSecrets } from '../secrets/storage.js';
|
|
8
|
+
import {
|
|
9
|
+
generateDbPassword,
|
|
10
|
+
generateDbUrl,
|
|
11
|
+
} from '../setup/fullstack-secrets.js';
|
|
8
12
|
import type { ComposeServiceName } from '../types.js';
|
|
9
13
|
import { generateAuthAppFiles } from './generators/auth.js';
|
|
10
14
|
import { generateConfigFiles } from './generators/config.js';
|
|
@@ -60,29 +64,6 @@ export interface InitOptions {
|
|
|
60
64
|
pm?: PackageManager;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
/**
|
|
64
|
-
* Generate a secure random password for database users
|
|
65
|
-
*/
|
|
66
|
-
function generateDbPassword(): string {
|
|
67
|
-
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Generate database URL for an app
|
|
72
|
-
* All apps connect to the same database, but use different users/schemas
|
|
73
|
-
*/
|
|
74
|
-
function generateDbUrl(
|
|
75
|
-
appName: string,
|
|
76
|
-
password: string,
|
|
77
|
-
projectName: string,
|
|
78
|
-
host = 'localhost',
|
|
79
|
-
port = 5432,
|
|
80
|
-
): string {
|
|
81
|
-
const userName = appName.replace(/-/g, '_');
|
|
82
|
-
const dbName = `${projectName.replace(/-/g, '_')}_dev`;
|
|
83
|
-
return `postgresql://${userName}:${password}@${host}:${port}/${dbName}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
67
|
/**
|
|
87
68
|
* Main init command - scaffolds a new project
|
|
88
69
|
*/
|
package/src/init/utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, parse } from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
5
|
|
|
4
6
|
export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
|
|
5
7
|
|
|
@@ -94,3 +96,102 @@ export function getRunCommand(
|
|
|
94
96
|
return `npm run ${script}`;
|
|
95
97
|
}
|
|
96
98
|
}
|
|
99
|
+
|
|
100
|
+
const lockfileByPm: Record<PackageManager, string> = {
|
|
101
|
+
pnpm: 'pnpm-lock.yaml',
|
|
102
|
+
yarn: 'yarn.lock',
|
|
103
|
+
npm: 'package-lock.json',
|
|
104
|
+
bun: 'bun.lockb',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find the workspace/project root by walking up from cwd.
|
|
109
|
+
* Checks for PM-specific workspace config, package.json#workspaces,
|
|
110
|
+
* and lockfiles.
|
|
111
|
+
*/
|
|
112
|
+
export function findWorkspaceRoot(cwd: string, pm: PackageManager): string {
|
|
113
|
+
let dir = cwd;
|
|
114
|
+
const root = parse(dir).root;
|
|
115
|
+
const lockfile = lockfileByPm[pm];
|
|
116
|
+
|
|
117
|
+
while (dir !== root) {
|
|
118
|
+
if (pm === 'pnpm' && existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
|
119
|
+
return dir;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const pkgJsonPath = join(dir, 'package.json');
|
|
123
|
+
if (existsSync(pkgJsonPath)) {
|
|
124
|
+
try {
|
|
125
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
126
|
+
if (pkg.workspaces) return dir;
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore malformed package.json
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (existsSync(join(dir, lockfile))) {
|
|
133
|
+
return dir;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
dir = dirname(dir);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return cwd;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get workspace package glob patterns from pnpm-workspace.yaml
|
|
144
|
+
* or package.json#workspaces.
|
|
145
|
+
*/
|
|
146
|
+
export function getWorkspaceGlobs(root: string): string[] {
|
|
147
|
+
const pnpmWorkspacePath = join(root, 'pnpm-workspace.yaml');
|
|
148
|
+
if (existsSync(pnpmWorkspacePath)) {
|
|
149
|
+
const content = readFileSync(pnpmWorkspacePath, 'utf-8');
|
|
150
|
+
const parsed = parseYaml(content);
|
|
151
|
+
return parsed?.packages ?? [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rootPkgJsonPath = join(root, 'package.json');
|
|
155
|
+
if (existsSync(rootPkgJsonPath)) {
|
|
156
|
+
const pkg = JSON.parse(readFileSync(rootPkgJsonPath, 'utf-8'));
|
|
157
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
158
|
+
return pkg.workspaces;
|
|
159
|
+
}
|
|
160
|
+
if (pkg.workspaces?.packages) {
|
|
161
|
+
return pkg.workspaces.packages;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Find all package.json files across a workspace.
|
|
170
|
+
* Returns the root package.json plus all workspace member package.json paths.
|
|
171
|
+
*/
|
|
172
|
+
export function findWorkspacePackages(
|
|
173
|
+
cwd: string,
|
|
174
|
+
pm: PackageManager,
|
|
175
|
+
): string[] {
|
|
176
|
+
const workspaceRoot = findWorkspaceRoot(cwd, pm);
|
|
177
|
+
const results: string[] = [];
|
|
178
|
+
|
|
179
|
+
const rootPkgJson = join(workspaceRoot, 'package.json');
|
|
180
|
+
if (existsSync(rootPkgJson)) {
|
|
181
|
+
results.push(rootPkgJson);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const globs = getWorkspaceGlobs(workspaceRoot);
|
|
185
|
+
|
|
186
|
+
for (const glob of globs) {
|
|
187
|
+
const pattern = `${glob}/package.json`;
|
|
188
|
+
const matches = fg.sync(pattern, {
|
|
189
|
+
cwd: workspaceRoot,
|
|
190
|
+
absolute: true,
|
|
191
|
+
ignore: ['**/node_modules/**'],
|
|
192
|
+
});
|
|
193
|
+
results.push(...matches);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return [...new Set(results)];
|
|
197
|
+
}
|
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
|
|
|
@@ -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
|
+
});
|