@geekmidas/cli 1.7.0 → 1.9.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/dist/{HostingerProvider-BiXdHjiq.cjs → HostingerProvider-CEsQbmpY.cjs} +1 -1
- package/dist/{HostingerProvider-BiXdHjiq.cjs.map → HostingerProvider-CEsQbmpY.cjs.map} +1 -1
- package/dist/{HostingerProvider-402UdK89.mjs → HostingerProvider-DkahM5AP.mjs} +1 -1
- package/dist/{HostingerProvider-402UdK89.mjs.map → HostingerProvider-DkahM5AP.mjs.map} +1 -1
- package/dist/{LocalStateProvider-BDm7ZqJo.mjs → LocalStateProvider-DXIwWb7k.mjs} +1 -1
- package/dist/{LocalStateProvider-BDm7ZqJo.mjs.map → LocalStateProvider-DXIwWb7k.mjs.map} +1 -1
- package/dist/{LocalStateProvider-CdspeSVL.cjs → LocalStateProvider-Roi202l7.cjs} +1 -1
- package/dist/{LocalStateProvider-CdspeSVL.cjs.map → LocalStateProvider-Roi202l7.cjs.map} +1 -1
- package/dist/{Route53Provider-kfJ77LmL.cjs → Route53Provider-BqXeHzuc.cjs} +1 -1
- package/dist/{Route53Provider-kfJ77LmL.cjs.map → Route53Provider-BqXeHzuc.cjs.map} +1 -1
- package/dist/{Route53Provider-DbBo7Uz5.mjs → Route53Provider-Ckq_n5Be.mjs} +1 -1
- package/dist/{Route53Provider-DbBo7Uz5.mjs.map → Route53Provider-Ckq_n5Be.mjs.map} +1 -1
- package/dist/{SSMStateProvider-DGrqYll0.cjs → SSMStateProvider-BReQA5re.cjs} +1 -1
- package/dist/{SSMStateProvider-DGrqYll0.cjs.map → SSMStateProvider-BReQA5re.cjs.map} +1 -1
- package/dist/{SSMStateProvider-DT0WV-E_.mjs → SSMStateProvider-wddd0_-d.mjs} +1 -1
- package/dist/{SSMStateProvider-DT0WV-E_.mjs.map → SSMStateProvider-wddd0_-d.mjs.map} +1 -1
- package/dist/{backup-provisioner-BIArpmTr.mjs → backup-provisioner-BAExdDtc.mjs} +1 -1
- package/dist/{backup-provisioner-BIArpmTr.mjs.map → backup-provisioner-BAExdDtc.mjs.map} +1 -1
- package/dist/{backup-provisioner-B5e-F6zX.cjs → backup-provisioner-C8VK63I-.cjs} +1 -1
- package/dist/{backup-provisioner-B5e-F6zX.cjs.map → backup-provisioner-C8VK63I-.cjs.map} +1 -1
- package/dist/{bundler-DgXsOSxc.mjs → bundler-BxHyDhdt.mjs} +1 -1
- package/dist/{bundler-DgXsOSxc.mjs.map → bundler-BxHyDhdt.mjs.map} +1 -1
- package/dist/{bundler-tHLLwYuU.cjs → bundler-CuMIfXw5.cjs} +1 -1
- package/dist/{bundler-tHLLwYuU.cjs.map → bundler-CuMIfXw5.cjs.map} +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/{index-C-KxSGGK.d.mts → index-BVNXOydm.d.mts} +2 -2
- package/dist/{index-C-KxSGGK.d.mts.map → index-BVNXOydm.d.mts.map} +1 -1
- package/dist/index.cjs +1019 -551
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1017 -549
- package/dist/index.mjs.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/sync-BOS0jKLn.cjs +93 -0
- package/dist/sync-BOS0jKLn.cjs.map +1 -0
- package/dist/sync-BnqNNc6O.mjs +3 -0
- package/dist/sync-BxFB34zW.cjs +4 -0
- package/dist/sync-CHfhmXF3.mjs +76 -0
- package/dist/sync-CHfhmXF3.mjs.map +1 -0
- package/dist/{types-CZg5iUgD.d.mts → types-eTlj5f2M.d.mts} +1 -1
- package/dist/{types-CZg5iUgD.d.mts.map → types-eTlj5f2M.d.mts.map} +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/package.json +4 -4
- package/src/dev/index.ts +1 -1
- package/src/generators/SubscriberGenerator.ts +1 -0
- package/src/index.ts +93 -0
- package/src/init/index.ts +4 -23
- package/src/init/utils.ts +103 -2
- package/src/init/versions.ts +1 -1
- package/src/secrets/index.ts +20 -1
- package/src/secrets/sync.ts +136 -0
- package/src/setup/fullstack-secrets.ts +121 -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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Sync via AWS SSM Parameter Store
|
|
3
|
+
*
|
|
4
|
+
* Stores and retrieves encrypted StageSecrets as SecureString parameters.
|
|
5
|
+
* Reuses the SSM infrastructure from the state provider.
|
|
6
|
+
*
|
|
7
|
+
* Parameter naming: /gkm/{workspaceName}/{stage}/secrets
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
GetParameterCommand,
|
|
12
|
+
ParameterNotFound,
|
|
13
|
+
PutParameterCommand,
|
|
14
|
+
SSMClient,
|
|
15
|
+
type SSMClientConfig,
|
|
16
|
+
} from '@aws-sdk/client-ssm';
|
|
17
|
+
import type { SSMStateConfig } from '../deploy/StateProvider.js';
|
|
18
|
+
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
19
|
+
import type { StageSecrets } from './types.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the SSM parameter name for secrets.
|
|
23
|
+
*/
|
|
24
|
+
function getSecretsParameterName(workspaceName: string, stage: string): string {
|
|
25
|
+
return `/gkm/${workspaceName}/${stage}/secrets`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create an SSM client from workspace state config.
|
|
30
|
+
*/
|
|
31
|
+
function createSSMClient(config: SSMStateConfig): SSMClient {
|
|
32
|
+
const clientConfig: SSMClientConfig = {
|
|
33
|
+
region: config.region,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (config.profile) {
|
|
37
|
+
const { fromIni } = require('@aws-sdk/credential-providers');
|
|
38
|
+
clientConfig.credentials = fromIni({ profile: config.profile });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return new SSMClient(clientConfig);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Push secrets to SSM Parameter Store.
|
|
46
|
+
*
|
|
47
|
+
* Stores the full StageSecrets object as a SecureString parameter.
|
|
48
|
+
*/
|
|
49
|
+
export async function pushSecrets(
|
|
50
|
+
stage: string,
|
|
51
|
+
workspace: NormalizedWorkspace,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const config = workspace.state;
|
|
54
|
+
if (!config || config.provider !== 'ssm') {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'SSM state provider not configured. Add state: { provider: "ssm", region: "..." } to gkm.config.ts.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!workspace.name) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
'Workspace name is required for SSM secrets sync. Set "name" in gkm.config.ts.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const client = createSSMClient(config as SSMStateConfig);
|
|
67
|
+
const parameterName = getSecretsParameterName(workspace.name, stage);
|
|
68
|
+
|
|
69
|
+
const { readStageSecrets } = await import('./storage.js');
|
|
70
|
+
const secrets = await readStageSecrets(stage, workspace.root);
|
|
71
|
+
|
|
72
|
+
if (!secrets) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await client.send(
|
|
79
|
+
new PutParameterCommand({
|
|
80
|
+
Name: parameterName,
|
|
81
|
+
Value: JSON.stringify(secrets),
|
|
82
|
+
Type: 'SecureString',
|
|
83
|
+
Overwrite: true,
|
|
84
|
+
Description: `GKM secrets for ${workspace.name}/${stage}`,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pull secrets from SSM Parameter Store.
|
|
91
|
+
*
|
|
92
|
+
* @returns StageSecrets or null if no secrets are stored remotely
|
|
93
|
+
*/
|
|
94
|
+
export async function pullSecrets(
|
|
95
|
+
stage: string,
|
|
96
|
+
workspace: NormalizedWorkspace,
|
|
97
|
+
): Promise<StageSecrets | null> {
|
|
98
|
+
const config = workspace.state;
|
|
99
|
+
if (!config || config.provider !== 'ssm') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!workspace.name) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const client = createSSMClient(config as SSMStateConfig);
|
|
108
|
+
const parameterName = getSecretsParameterName(workspace.name, stage);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = await client.send(
|
|
112
|
+
new GetParameterCommand({
|
|
113
|
+
Name: parameterName,
|
|
114
|
+
WithDecryption: true,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!response.Parameter?.Value) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return JSON.parse(response.Parameter.Value) as StageSecrets;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof ParameterNotFound) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if SSM is configured for the workspace.
|
|
133
|
+
*/
|
|
134
|
+
export function isSSMConfigured(workspace: NormalizedWorkspace): boolean {
|
|
135
|
+
return !!workspace.state && workspace.state.provider === 'ssm';
|
|
136
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { generateSecurePassword } from '../secrets/generator.js';
|
|
4
|
+
import type { StageSecrets } from '../secrets/types.js';
|
|
5
|
+
import type { NormalizedWorkspace } from '../workspace/types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a secure random password for database users.
|
|
9
|
+
* Uses a combination of timestamp and random bytes for uniqueness.
|
|
10
|
+
*/
|
|
11
|
+
export function generateDbPassword(): string {
|
|
12
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate database URL for an app.
|
|
17
|
+
* All apps connect to the same database, but use different users/schemas.
|
|
18
|
+
*/
|
|
19
|
+
export function generateDbUrl(
|
|
20
|
+
appName: string,
|
|
21
|
+
password: string,
|
|
22
|
+
projectName: string,
|
|
23
|
+
host = 'localhost',
|
|
24
|
+
port = 5432,
|
|
25
|
+
): string {
|
|
26
|
+
const userName = appName.replace(/-/g, '_');
|
|
27
|
+
const dbName = `${projectName.replace(/-/g, '_')}_dev`;
|
|
28
|
+
return `postgresql://${userName}:${password}@${host}:${port}/${dbName}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate fullstack-aware custom secrets for a workspace.
|
|
33
|
+
*
|
|
34
|
+
* Generates:
|
|
35
|
+
* - Common secrets: NODE_ENV, PORT, LOG_LEVEL, JWT_SECRET
|
|
36
|
+
* - Per-app database passwords and URLs for backend apps with db service
|
|
37
|
+
* - Better-auth secrets for apps using the better-auth framework
|
|
38
|
+
*/
|
|
39
|
+
export function generateFullstackCustomSecrets(
|
|
40
|
+
workspace: NormalizedWorkspace,
|
|
41
|
+
): Record<string, string> {
|
|
42
|
+
const hasDb = !!workspace.services.db;
|
|
43
|
+
const customs: Record<string, string> = {
|
|
44
|
+
NODE_ENV: 'development',
|
|
45
|
+
PORT: '3000',
|
|
46
|
+
LOG_LEVEL: 'debug',
|
|
47
|
+
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (!hasDb) {
|
|
51
|
+
return customs;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Collect all frontend ports for trusted origins
|
|
55
|
+
const frontendPorts: number[] = [];
|
|
56
|
+
|
|
57
|
+
for (const [appName, appConfig] of Object.entries(workspace.apps)) {
|
|
58
|
+
if (appConfig.type === 'frontend') {
|
|
59
|
+
frontendPorts.push(appConfig.port);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Backend apps with database: generate per-app DB passwords and URLs
|
|
64
|
+
const password = generateDbPassword();
|
|
65
|
+
const upperName = appName.toUpperCase();
|
|
66
|
+
|
|
67
|
+
customs[`${upperName}_DATABASE_URL`] = generateDbUrl(
|
|
68
|
+
appName,
|
|
69
|
+
password,
|
|
70
|
+
workspace.name,
|
|
71
|
+
);
|
|
72
|
+
customs[`${upperName}_DB_PASSWORD`] = password;
|
|
73
|
+
|
|
74
|
+
// Better-auth framework secrets
|
|
75
|
+
if (appConfig.framework === 'better-auth') {
|
|
76
|
+
customs.AUTH_PORT = String(appConfig.port);
|
|
77
|
+
customs.AUTH_URL = `http://localhost:${appConfig.port}`;
|
|
78
|
+
customs.BETTER_AUTH_SECRET = `better-auth-${Date.now()}-${generateSecurePassword(16)}`;
|
|
79
|
+
customs.BETTER_AUTH_URL = `http://localhost:${appConfig.port}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generate trusted origins for better-auth (all app ports)
|
|
84
|
+
if (customs.BETTER_AUTH_SECRET) {
|
|
85
|
+
const allPorts = Object.values(workspace.apps).map((a) => a.port);
|
|
86
|
+
customs.BETTER_AUTH_TRUSTED_ORIGINS = allPorts
|
|
87
|
+
.map((p) => `http://localhost:${p}`)
|
|
88
|
+
.join(',');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return customs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract *_DB_PASSWORD keys from secrets and write docker/.env.
|
|
96
|
+
*
|
|
97
|
+
* The docker/.env file contains database passwords that the PostgreSQL
|
|
98
|
+
* init script reads to create per-app database users.
|
|
99
|
+
*/
|
|
100
|
+
export async function writeDockerEnvFromSecrets(
|
|
101
|
+
secrets: StageSecrets,
|
|
102
|
+
workspaceRoot: string,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const dbPasswordEntries = Object.entries(secrets.custom).filter(([key]) =>
|
|
105
|
+
key.endsWith('_DB_PASSWORD'),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (dbPasswordEntries.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const envContent = `# Auto-generated docker environment file
|
|
113
|
+
# Contains database passwords for docker-compose postgres init
|
|
114
|
+
# This file is gitignored - do not commit to version control
|
|
115
|
+
${dbPasswordEntries.map(([key, value]) => `${key}=${value}`).join('\n')}
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const envPath = join(workspaceRoot, 'docker', '.env');
|
|
119
|
+
await mkdir(dirname(envPath), { recursive: true });
|
|
120
|
+
await writeFile(envPath, envContent);
|
|
121
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import { loadWorkspaceConfig } from '../config.js';
|
|
5
|
+
import { startWorkspaceServices } from '../dev/index.js';
|
|
6
|
+
import { createStageSecrets } from '../secrets/generator.js';
|
|
7
|
+
import {
|
|
8
|
+
readStageSecrets,
|
|
9
|
+
secretsExist,
|
|
10
|
+
writeStageSecrets,
|
|
11
|
+
} from '../secrets/storage.js';
|
|
12
|
+
import { isSSMConfigured, pullSecrets, pushSecrets } from '../secrets/sync.js';
|
|
13
|
+
import type { ComposeServiceName } from '../types.js';
|
|
14
|
+
import type { LoadedConfig, NormalizedWorkspace } from '../workspace/types.js';
|
|
15
|
+
import {
|
|
16
|
+
generateFullstackCustomSecrets,
|
|
17
|
+
writeDockerEnvFromSecrets,
|
|
18
|
+
} from './fullstack-secrets.js';
|
|
19
|
+
|
|
20
|
+
const logger = console;
|
|
21
|
+
|
|
22
|
+
export interface SetupOptions {
|
|
23
|
+
stage?: string;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
skipDocker?: boolean;
|
|
26
|
+
yes?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Setup development environment.
|
|
31
|
+
*
|
|
32
|
+
* Orchestrates:
|
|
33
|
+
* 1. Load workspace config
|
|
34
|
+
* 2. Resolve secrets (local → SSM → generate fresh)
|
|
35
|
+
* 3. Write docker/.env from secrets
|
|
36
|
+
* 4. Start Docker services
|
|
37
|
+
*/
|
|
38
|
+
export async function setupCommand(options: SetupOptions = {}): Promise<void> {
|
|
39
|
+
const stage = options.stage ?? 'development';
|
|
40
|
+
|
|
41
|
+
logger.log('\n🔧 Setting up development environment...\n');
|
|
42
|
+
|
|
43
|
+
// 1. Load workspace config
|
|
44
|
+
let loadedConfig: LoadedConfig;
|
|
45
|
+
try {
|
|
46
|
+
loadedConfig = await loadWorkspaceConfig();
|
|
47
|
+
} catch {
|
|
48
|
+
logger.error(
|
|
49
|
+
'❌ No gkm.config.ts found. Run this command from a workspace root.',
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { workspace } = loadedConfig;
|
|
55
|
+
const isMultiApp = Object.keys(workspace.apps).length > 1;
|
|
56
|
+
|
|
57
|
+
logger.log(`📦 Workspace: ${workspace.name}`);
|
|
58
|
+
logger.log(`📱 Apps: ${Object.keys(workspace.apps).join(', ')}`);
|
|
59
|
+
logger.log(`🔑 Stage: ${stage}\n`);
|
|
60
|
+
|
|
61
|
+
// 2. Resolve secrets
|
|
62
|
+
const secrets = await resolveSecrets(stage, workspace, options);
|
|
63
|
+
|
|
64
|
+
if (!secrets) {
|
|
65
|
+
logger.error('❌ Failed to resolve secrets. Exiting.');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Write docker/.env from secrets (always regenerated as derived file)
|
|
70
|
+
if (isMultiApp && workspace.services.db) {
|
|
71
|
+
await writeDockerEnvFromSecrets(secrets, workspace.root);
|
|
72
|
+
logger.log('📄 Generated docker/.env with database passwords');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Start Docker services
|
|
76
|
+
if (!options.skipDocker) {
|
|
77
|
+
const composeFile = join(workspace.root, 'docker-compose.yml');
|
|
78
|
+
if (existsSync(composeFile)) {
|
|
79
|
+
logger.log('');
|
|
80
|
+
await startWorkspaceServices(workspace);
|
|
81
|
+
} else {
|
|
82
|
+
logger.log('⚠️ No docker-compose.yml found. Skipping Docker services.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Print summary
|
|
87
|
+
printSummary(workspace, stage);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve secrets with priority:
|
|
92
|
+
* 1. Local secrets exist → use them (preserves manual additions)
|
|
93
|
+
* 2. SSM configured and has secrets → pull and use
|
|
94
|
+
* 3. Neither → generate fresh secrets
|
|
95
|
+
*
|
|
96
|
+
* --force skips checks 1 and 2 and always regenerates.
|
|
97
|
+
*/
|
|
98
|
+
async function resolveSecrets(
|
|
99
|
+
stage: string,
|
|
100
|
+
workspace: NormalizedWorkspace,
|
|
101
|
+
options: SetupOptions,
|
|
102
|
+
) {
|
|
103
|
+
// Force regeneration
|
|
104
|
+
if (options.force) {
|
|
105
|
+
logger.log('🔐 Generating fresh secrets (--force)...');
|
|
106
|
+
return generateFreshSecrets(stage, workspace, options);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check local secrets first
|
|
110
|
+
if (secretsExist(stage, workspace.root)) {
|
|
111
|
+
logger.log('🔐 Using existing local secrets');
|
|
112
|
+
const secrets = await readStageSecrets(stage, workspace.root);
|
|
113
|
+
if (secrets) {
|
|
114
|
+
return secrets;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Try SSM pull if configured
|
|
119
|
+
if (isSSMConfigured(workspace)) {
|
|
120
|
+
logger.log('☁️ Checking for remote secrets in SSM...');
|
|
121
|
+
try {
|
|
122
|
+
const remoteSecrets = await pullSecrets(stage, workspace);
|
|
123
|
+
if (remoteSecrets) {
|
|
124
|
+
logger.log('✅ Pulled secrets from SSM');
|
|
125
|
+
await writeStageSecrets(remoteSecrets, workspace.root);
|
|
126
|
+
return remoteSecrets;
|
|
127
|
+
}
|
|
128
|
+
logger.log(' No remote secrets found');
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.warn(`⚠️ Failed to pull from SSM: ${(error as Error).message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Generate fresh secrets
|
|
135
|
+
logger.log('🔐 Generating fresh development secrets...');
|
|
136
|
+
return generateFreshSecrets(stage, workspace, options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate fresh secrets for the workspace.
|
|
141
|
+
*/
|
|
142
|
+
async function generateFreshSecrets(
|
|
143
|
+
stage: string,
|
|
144
|
+
workspace: NormalizedWorkspace,
|
|
145
|
+
options: SetupOptions,
|
|
146
|
+
) {
|
|
147
|
+
// Determine services from workspace config
|
|
148
|
+
const serviceNames: ComposeServiceName[] = [];
|
|
149
|
+
if (workspace.services.db) serviceNames.push('postgres');
|
|
150
|
+
if (workspace.services.cache) serviceNames.push('redis');
|
|
151
|
+
|
|
152
|
+
// Create base secrets with service credentials
|
|
153
|
+
const secrets = createStageSecrets(stage, serviceNames);
|
|
154
|
+
|
|
155
|
+
// Generate fullstack-aware custom secrets
|
|
156
|
+
const isMultiApp = Object.keys(workspace.apps).length > 1;
|
|
157
|
+
if (isMultiApp) {
|
|
158
|
+
const customSecrets = generateFullstackCustomSecrets(workspace);
|
|
159
|
+
secrets.custom = customSecrets;
|
|
160
|
+
} else {
|
|
161
|
+
secrets.custom = {
|
|
162
|
+
NODE_ENV: 'development',
|
|
163
|
+
PORT: '3000',
|
|
164
|
+
LOG_LEVEL: 'debug',
|
|
165
|
+
JWT_SECRET: `dev-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Write secrets
|
|
170
|
+
await writeStageSecrets(secrets, workspace.root);
|
|
171
|
+
logger.log(` Secrets written to .gkm/secrets/${stage}.json`);
|
|
172
|
+
|
|
173
|
+
// Offer to push to SSM if configured
|
|
174
|
+
if (isSSMConfigured(workspace) && !options.yes) {
|
|
175
|
+
const { shouldPush } = await prompts({
|
|
176
|
+
type: 'confirm',
|
|
177
|
+
name: 'shouldPush',
|
|
178
|
+
message: 'Push secrets to SSM for team sharing?',
|
|
179
|
+
initial: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (shouldPush) {
|
|
183
|
+
try {
|
|
184
|
+
await pushSecrets(stage, workspace);
|
|
185
|
+
logger.log('☁️ Secrets pushed to SSM');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.warn(`⚠️ Failed to push to SSM: ${(error as Error).message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return secrets;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Print setup summary with next steps.
|
|
197
|
+
*/
|
|
198
|
+
function printSummary(workspace: NormalizedWorkspace, stage: string): void {
|
|
199
|
+
logger.log(`\n${'─'.repeat(50)}`);
|
|
200
|
+
logger.log('\n✅ Development environment ready!\n');
|
|
201
|
+
|
|
202
|
+
logger.log('📋 Apps:');
|
|
203
|
+
for (const [name, app] of Object.entries(workspace.apps)) {
|
|
204
|
+
const icon = app.type === 'frontend' ? '🌐' : '🔧';
|
|
205
|
+
logger.log(` ${icon} ${name} → http://localhost:${app.port}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
logger.log('\n🚀 Next steps:');
|
|
209
|
+
logger.log(' gkm dev # Start all apps');
|
|
210
|
+
logger.log(` gkm secrets:show --stage ${stage} # View secrets`);
|
|
211
|
+
logger.log('');
|
|
212
|
+
}
|
|
@@ -80,7 +80,7 @@ describe('web (frontend) app context', () => {
|
|
|
80
80
|
|
|
81
81
|
it('should not have DATABASE_URL (frontend does not access database)', () => {
|
|
82
82
|
// Frontend gets secrets mapped without an app-specific DATABASE_URL
|
|
83
|
-
const
|
|
83
|
+
const _secrets = mapSecretsForApp(createFullstackSecrets(), 'web');
|
|
84
84
|
|
|
85
85
|
// No WEB_DATABASE_URL exists, so DATABASE_URL stays as default (api's)
|
|
86
86
|
// In practice, frontend apps don't use DATABASE_URL at all
|