@geekmidas/cli 1.10.30 → 1.10.32

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 (67) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/gkm.mjs +1 -1
  3. package/dist/{config-jsRYHOHU.mjs → config-B62g483e.mjs} +2 -2
  4. package/dist/{config-jsRYHOHU.mjs.map → config-B62g483e.mjs.map} +1 -1
  5. package/dist/{config-D3ORuiUs.cjs → config-Cuo8vFsp.cjs} +2 -2
  6. package/dist/{config-D3ORuiUs.cjs.map → config-Cuo8vFsp.cjs.map} +1 -1
  7. package/dist/config.cjs +2 -2
  8. package/dist/config.d.cts +1 -1
  9. package/dist/config.d.mts +1 -1
  10. package/dist/config.mjs +2 -2
  11. package/dist/deploy/sniffer-worker.cjs +2 -0
  12. package/dist/deploy/sniffer-worker.cjs.map +1 -1
  13. package/dist/deploy/sniffer-worker.mjs +2 -0
  14. package/dist/deploy/sniffer-worker.mjs.map +1 -1
  15. package/dist/{index-BkibYzso.d.cts → index-C3t5VL4R.d.cts} +2 -32
  16. package/dist/index-C3t5VL4R.d.cts.map +1 -0
  17. package/dist/{index-CY-ieuRp.d.mts → index-Dt_dZ7K4.d.mts} +2 -32
  18. package/dist/index-Dt_dZ7K4.d.mts.map +1 -0
  19. package/dist/index.cjs +65 -26
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.mjs +65 -26
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{openapi-ClA8lIhO.cjs → openapi-BQft1jm8.cjs} +5 -22
  24. package/dist/openapi-BQft1jm8.cjs.map +1 -0
  25. package/dist/{openapi-CTARkkkn.mjs → openapi-DE8b-vin.mjs} +6 -23
  26. package/dist/openapi-DE8b-vin.mjs.map +1 -0
  27. package/dist/openapi.cjs +3 -3
  28. package/dist/openapi.mjs +3 -3
  29. package/dist/workspace/index.cjs +1 -1
  30. package/dist/workspace/index.d.cts +2 -2
  31. package/dist/workspace/index.d.mts +2 -2
  32. package/dist/workspace/index.mjs +1 -1
  33. package/dist/{workspace-D4z4A4cq.mjs → workspace-Bi4X7Yzy.mjs} +1 -6
  34. package/dist/{workspace-D4z4A4cq.mjs.map → workspace-Bi4X7Yzy.mjs.map} +1 -1
  35. package/dist/{workspace-4SP3Gx4Y.cjs → workspace-CGYykWfn.cjs} +1 -6
  36. package/dist/{workspace-4SP3Gx4Y.cjs.map → workspace-CGYykWfn.cjs.map} +1 -1
  37. package/package.json +4 -4
  38. package/src/__tests__/openapi.spec.ts +0 -315
  39. package/src/build/__tests__/manifests.spec.ts +66 -0
  40. package/src/build/index.ts +12 -1
  41. package/src/build/types.ts +5 -0
  42. package/src/deploy/__tests__/__fixtures__/env-parsers/optional-env-parser.ts +21 -0
  43. package/src/deploy/__tests__/index.spec.ts +1 -3
  44. package/src/deploy/__tests__/sniffer.spec.ts +123 -0
  45. package/src/deploy/sniffer-worker.ts +2 -1
  46. package/src/deploy/sniffer.ts +86 -12
  47. package/src/docker/compose.ts +2 -0
  48. package/src/generators/CronGenerator.ts +3 -1
  49. package/src/generators/EndpointGenerator.ts +6 -2
  50. package/src/generators/FunctionGenerator.ts +3 -1
  51. package/src/generators/SubscriberGenerator.ts +3 -1
  52. package/src/index.ts +8 -0
  53. package/src/init/generators/docker.ts +2 -2
  54. package/src/init/generators/monorepo.ts +15 -1
  55. package/src/init/index.ts +2 -4
  56. package/src/init/versions.ts +2 -2
  57. package/src/openapi.ts +1 -35
  58. package/src/types.ts +7 -0
  59. package/src/workspace/__tests__/client-generator.spec.ts +1 -485
  60. package/src/workspace/client-generator.ts +1 -145
  61. package/src/workspace/index.ts +0 -1
  62. package/src/workspace/schema.ts +0 -9
  63. package/src/workspace/types.ts +0 -29
  64. package/dist/index-BkibYzso.d.cts.map +0 -1
  65. package/dist/index-CY-ieuRp.d.mts.map +0 -1
  66. package/dist/openapi-CTARkkkn.mjs.map +0 -1
  67. package/dist/openapi-ClA8lIhO.cjs.map +0 -1
@@ -60,7 +60,17 @@ export type { SniffResult } from '@geekmidas/envkit/sniffer';
60
60
  */
61
61
  export interface SniffedEnvironment {
62
62
  appName: string;
63
+ /**
64
+ * All required environment variable names.
65
+ * When `SniffAppOptions.markOptional` is true, optional variables are
66
+ * suffixed with `?` (e.g. `PORT?`).
67
+ */
63
68
  requiredEnvVars: string[];
69
+ /**
70
+ * Subset of variables that are optional (accessed via `.optional()` or
71
+ * `.default()`). Always populated regardless of `markOptional`.
72
+ */
73
+ optionalEnvVars: string[];
64
74
  }
65
75
 
66
76
  /**
@@ -69,6 +79,12 @@ export interface SniffedEnvironment {
69
79
  export interface SniffAppOptions {
70
80
  /** Whether to log warnings for errors encountered during sniffing. Defaults to true. */
71
81
  logWarnings?: boolean;
82
+ /**
83
+ * When true, optional variables (those with `.optional()` or `.default()`)
84
+ * are suffixed with `?` in `requiredEnvVars` (e.g. `PORT?`).
85
+ * Defaults to false.
86
+ */
87
+ markOptional?: boolean;
72
88
  }
73
89
 
74
90
  /**
@@ -97,7 +113,7 @@ export async function sniffAppEnvironment(
97
113
  workspacePath: string,
98
114
  options: SniffAppOptions = {},
99
115
  ): Promise<SniffedEnvironment> {
100
- const { logWarnings = true } = options;
116
+ const { logWarnings = true, markOptional = false } = options;
101
117
 
102
118
  // 1. Frontend apps - handle dependencies and config sniffing
103
119
  if (app.type === 'frontend') {
@@ -110,6 +126,7 @@ export async function sniffAppEnvironment(
110
126
  // The file calls .parse() at module load, which triggers sniffer to capture vars
111
127
  if (app.config) {
112
128
  const sniffedVars: string[] = [];
129
+ const sniffedOptional: string[] = [];
113
130
 
114
131
  // Collect config paths to sniff
115
132
  const configPaths: string[] = [];
@@ -131,14 +148,20 @@ export async function sniffAppEnvironment(
131
148
  }
132
149
 
133
150
  sniffedVars.push(...result.envVars);
151
+ sniffedOptional.push(...result.optionalEnvVars);
134
152
  }
135
153
 
154
+ const optionalEnvVars = [...new Set(sniffedOptional)];
136
155
  // Combine: dependency vars + sniffed vars (deduplicated)
137
- const allVars = [...new Set([...depVars, ...sniffedVars])];
138
- return { appName, requiredEnvVars: allVars };
156
+ const allVars = applyMarkOptional(
157
+ [...new Set([...depVars, ...sniffedVars])],
158
+ optionalEnvVars,
159
+ markOptional,
160
+ );
161
+ return { appName, requiredEnvVars: allVars, optionalEnvVars };
139
162
  }
140
163
 
141
- return { appName, requiredEnvVars: depVars };
164
+ return { appName, requiredEnvVars: depVars, optionalEnvVars: [] };
142
165
  }
143
166
 
144
167
  // 2. Entry apps - import entry file in subprocess to trigger config.parse()
@@ -151,7 +174,15 @@ export async function sniffAppEnvironment(
151
174
  );
152
175
  }
153
176
 
154
- return { appName, requiredEnvVars: result.envVars };
177
+ return {
178
+ appName,
179
+ requiredEnvVars: applyMarkOptional(
180
+ result.envVars,
181
+ result.optionalEnvVars,
182
+ markOptional,
183
+ ),
184
+ optionalEnvVars: result.optionalEnvVars,
185
+ };
155
186
  }
156
187
 
157
188
  // 4. Route-based apps - load routes and call getEnvironment() on each construct
@@ -168,7 +199,15 @@ export async function sniffAppEnvironment(
168
199
  );
169
200
  }
170
201
 
171
- return { appName, requiredEnvVars: result.envVars };
202
+ return {
203
+ appName,
204
+ requiredEnvVars: applyMarkOptional(
205
+ result.envVars,
206
+ result.optionalEnvVars,
207
+ markOptional,
208
+ ),
209
+ optionalEnvVars: result.optionalEnvVars,
210
+ };
172
211
  }
173
212
 
174
213
  // 5. Apps with envParser but no routes - run sniffer to detect env var usage
@@ -189,11 +228,33 @@ export async function sniffAppEnvironment(
189
228
  }
190
229
  }
191
230
 
192
- return { appName, requiredEnvVars: result.envVars };
231
+ return {
232
+ appName,
233
+ requiredEnvVars: applyMarkOptional(
234
+ result.envVars,
235
+ result.optionalEnvVars,
236
+ markOptional,
237
+ ),
238
+ optionalEnvVars: result.optionalEnvVars,
239
+ };
193
240
  }
194
241
 
195
- // 5. No env detection method available
196
- return { appName, requiredEnvVars: [] };
242
+ // No env detection method available
243
+ return { appName, requiredEnvVars: [], optionalEnvVars: [] };
244
+ }
245
+
246
+ /**
247
+ * Apply the `?` suffix to optional variables in an env var list.
248
+ * Only modifies the list when `markOptional` is true and there are optional vars.
249
+ */
250
+ function applyMarkOptional(
251
+ envVars: string[],
252
+ optionalEnvVars: string[],
253
+ markOptional: boolean,
254
+ ): string[] {
255
+ if (!markOptional || optionalEnvVars.length === 0) return envVars;
256
+ const optionalSet = new Set(optionalEnvVars);
257
+ return envVars.map((v) => (optionalSet.has(v) ? `${v}?` : v));
197
258
  }
198
259
 
199
260
  /**
@@ -201,6 +262,7 @@ export async function sniffAppEnvironment(
201
262
  */
202
263
  interface EntrySniffResult {
203
264
  envVars: string[];
265
+ optionalEnvVars: string[];
204
266
  error?: Error;
205
267
  }
206
268
 
@@ -267,6 +329,7 @@ async function sniffEntryFile(
267
329
  const result = JSON.parse(jsonMatch[0]);
268
330
  resolvePromise({
269
331
  envVars: result.envVars || [],
332
+ optionalEnvVars: result.optionalEnvVars || [],
270
333
  error: result.error ? new Error(result.error) : undefined,
271
334
  });
272
335
  return;
@@ -278,6 +341,7 @@ async function sniffEntryFile(
278
341
  // If we couldn't parse the output, return empty with error info
279
342
  resolvePromise({
280
343
  envVars: [],
344
+ optionalEnvVars: [],
281
345
  error: new Error(
282
346
  `Failed to sniff entry file (exit code ${code}): ${stderr || stdout || 'No output'}`,
283
347
  ),
@@ -287,6 +351,7 @@ async function sniffEntryFile(
287
351
  child.on('error', (err) => {
288
352
  resolvePromise({
289
353
  envVars: [],
354
+ optionalEnvVars: [],
290
355
  error: err,
291
356
  });
292
357
  });
@@ -320,7 +385,11 @@ async function sniffRouteFiles(
320
385
  const routesArray = Array.isArray(routes) ? routes : [routes];
321
386
  const pattern = routesArray[0];
322
387
  if (!pattern) {
323
- return { envVars: [], error: new Error('No route patterns provided') };
388
+ return {
389
+ envVars: [],
390
+ optionalEnvVars: [],
391
+ error: new Error('No route patterns provided'),
392
+ };
324
393
  }
325
394
 
326
395
  return new Promise((resolvePromise) => {
@@ -364,6 +433,9 @@ async function sniffRouteFiles(
364
433
  const result = JSON.parse(jsonMatch[0]);
365
434
  resolvePromise({
366
435
  envVars: result.envVars || [],
436
+ // Routes worker uses getEnvironment() which doesn't track
437
+ // optionality yet — populated when service register() is sniffed
438
+ optionalEnvVars: result.optionalEnvVars || [],
367
439
  error: result.error ? new Error(result.error) : undefined,
368
440
  });
369
441
  return;
@@ -375,6 +447,7 @@ async function sniffRouteFiles(
375
447
  // If we couldn't parse the output, return empty with error info
376
448
  resolvePromise({
377
449
  envVars: [],
450
+ optionalEnvVars: [],
378
451
  error: new Error(
379
452
  `Failed to sniff route files (exit code ${code}): ${stderr || stdout || 'No output'}`,
380
453
  ),
@@ -384,6 +457,7 @@ async function sniffRouteFiles(
384
457
  child.on('error', (err) => {
385
458
  resolvePromise({
386
459
  envVars: [],
460
+ optionalEnvVars: [],
387
461
  error: err,
388
462
  });
389
463
  });
@@ -410,7 +484,7 @@ async function sniffEnvParser(
410
484
  // Parse the envParser path: './src/config/env#envParser' or './src/config/env'
411
485
  const [modulePath, exportName = 'default'] = envParserPath.split('#');
412
486
  if (!modulePath) {
413
- return { envVars: [], unhandledRejections: [] };
487
+ return { envVars: [], optionalEnvVars: [], unhandledRejections: [] };
414
488
  }
415
489
 
416
490
  // Resolve the full path to the module
@@ -428,7 +502,7 @@ async function sniffEnvParser(
428
502
  console.warn(
429
503
  `[sniffer] Failed to import SnifferEnvironmentParser: ${message}`,
430
504
  );
431
- return { envVars: [], unhandledRejections: [] };
505
+ return { envVars: [], optionalEnvVars: [], unhandledRejections: [] };
432
506
  }
433
507
 
434
508
  const sniffer = new SnifferEnvironmentParser();
@@ -280,6 +280,7 @@ services:
280
280
  restart: unless-stopped
281
281
  environment:
282
282
  MP_SMTP_AUTH: \${SMTP_USER:-${imageName}}:\${SMTP_PASS:-${imageName}}
283
+ MP_SMTP_AUTH_ALLOW_INSECURE: true
283
284
  ports:
284
285
  - "\${MAILPIT_PORT:-8025}:8025" # Web UI / API
285
286
  - "\${SMTP_PORT:-1025}:1025" # SMTP
@@ -505,6 +506,7 @@ services:
505
506
  restart: unless-stopped
506
507
  environment:
507
508
  MP_SMTP_AUTH: \${SMTP_USER:-${workspace.name}}:\${SMTP_PASS:-${workspace.name}}
509
+ MP_SMTP_AUTH_ALLOW_INSECURE: true
508
510
  ports:
509
511
  - "\${MAILPIT_PORT:-8025}:8025" # Web UI / API
510
512
  - "\${SMTP_PORT:-1025}:1025" # SMTP
@@ -51,7 +51,9 @@ export class CronGenerator extends ConstructGenerator<
51
51
  schedule: construct.schedule || 'rate(1 hour)',
52
52
  timeout: construct.timeout,
53
53
  memorySize: construct.memorySize,
54
- environment: await construct.getEnvironment(),
54
+ environment: await construct.getEnvironment({
55
+ markOptional: context.markOptional,
56
+ }),
55
57
  });
56
58
 
57
59
  logger.log(`Generated cron handler: ${key}`);
@@ -130,7 +130,9 @@ export class EndpointGenerator extends ConstructGenerator<
130
130
  ),
131
131
  timeout: construct.timeout,
132
132
  memorySize: construct.memorySize,
133
- environment: await construct.getEnvironment(),
133
+ environment: await construct.getEnvironment({
134
+ markOptional: context.markOptional,
135
+ }),
134
136
  authorizer: construct.authorizer?.name ?? 'none',
135
137
  };
136
138
 
@@ -160,7 +162,9 @@ export class EndpointGenerator extends ConstructGenerator<
160
162
  ),
161
163
  timeout: construct.timeout,
162
164
  memorySize: construct.memorySize,
163
- environment: await construct.getEnvironment(),
165
+ environment: await construct.getEnvironment({
166
+ markOptional: context.markOptional,
167
+ }),
164
168
  authorizer: construct.authorizer?.name ?? 'none',
165
169
  };
166
170
 
@@ -69,7 +69,9 @@ export class FunctionGenerator extends ConstructGenerator<
69
69
  ),
70
70
  timeout: construct.timeout,
71
71
  memorySize: construct.memorySize,
72
- environment: await construct.getEnvironment(),
72
+ environment: await construct.getEnvironment({
73
+ markOptional: context.markOptional,
74
+ }),
73
75
  });
74
76
 
75
77
  logger.log(`Generated function handler: ${key}`);
@@ -70,7 +70,9 @@ export class SubscriberGenerator extends ConstructGenerator<
70
70
  subscribedEvents: construct.subscribedEvents || [],
71
71
  timeout: construct.timeout,
72
72
  memorySize: construct.memorySize,
73
- environment: await construct.getEnvironment(),
73
+ environment: await construct.getEnvironment({
74
+ markOptional: context.markOptional,
75
+ }),
74
76
  });
75
77
 
76
78
  logger.log(`Generated subscriber handler: ${key}`);
package/src/index.ts CHANGED
@@ -107,6 +107,10 @@ program
107
107
  .option('--production', 'Build for production (no dev tools, bundled output)')
108
108
  .option('--skip-bundle', 'Skip bundling step in production build')
109
109
  .option('--stage <stage>', 'Inject encrypted secrets for deployment stage')
110
+ .option(
111
+ '--mark-optional',
112
+ 'Suffix optional env vars with ? in manifest envVars field (e.g. PORT?)',
113
+ )
110
114
  .action(
111
115
  async (options: {
112
116
  provider?: string;
@@ -115,6 +119,7 @@ program
115
119
  production?: boolean;
116
120
  skipBundle?: boolean;
117
121
  stage?: string;
122
+ markOptional?: boolean;
118
123
  }) => {
119
124
  try {
120
125
  const globalOptions = program.opts();
@@ -133,6 +138,7 @@ program
133
138
  production: options.production || false,
134
139
  skipBundle: options.skipBundle || false,
135
140
  stage: options.stage,
141
+ markOptional: options.markOptional || false,
136
142
  });
137
143
  }
138
144
  // Handle legacy providers option
@@ -146,6 +152,7 @@ program
146
152
  production: options.production || false,
147
153
  skipBundle: options.skipBundle || false,
148
154
  stage: options.stage,
155
+ markOptional: options.markOptional || false,
149
156
  });
150
157
  }
151
158
  // Default to config-driven build
@@ -155,6 +162,7 @@ program
155
162
  production: options.production || false,
156
163
  skipBundle: options.skipBundle || false,
157
164
  stage: options.stage,
165
+ markOptional: options.markOptional || false,
158
166
  });
159
167
  }
160
168
  } catch (error) {
@@ -259,7 +259,7 @@ ${volumes.join('\n')}
259
259
  */
260
260
  function generateDockerEnv(
261
261
  apps: DatabaseAppConfig[],
262
- eventsBackend?: EventsBackend,
262
+ _eventsBackend?: EventsBackend,
263
263
  ): string {
264
264
  const envVars = apps.map((app) => {
265
265
  const envVar = `${app.name.toUpperCase()}_DB_PASSWORD`;
@@ -285,7 +285,7 @@ ${envVars.join('\n')}
285
285
  */
286
286
  function generatePostgresInitScript(
287
287
  apps: DatabaseAppConfig[],
288
- eventsBackend?: EventsBackend,
288
+ _eventsBackend?: EventsBackend,
289
289
  ): string {
290
290
  const userCreations = apps.map((app) => {
291
291
  const userName = app.name.replace(/-/g, '_');
@@ -389,7 +389,13 @@ export default defineWorkspace({
389
389
  },`;
390
390
 
391
391
  // Add services if any are selected
392
- if (services.db || services.cache || services.mail) {
392
+ if (
393
+ services.db ||
394
+ services.cache ||
395
+ services.mail ||
396
+ services.storage ||
397
+ services.events
398
+ ) {
393
399
  config += `
394
400
  services: {`;
395
401
  if (services.db) {
@@ -403,6 +409,14 @@ export default defineWorkspace({
403
409
  if (services.mail) {
404
410
  config += `
405
411
  mail: true,`;
412
+ }
413
+ if (services.storage) {
414
+ config += `
415
+ storage: true,`;
416
+ }
417
+ if (services.events) {
418
+ config += `
419
+ events: '${services.events}',`;
406
420
  }
407
421
  config += `
408
422
  },`;
package/src/init/index.ts CHANGED
@@ -192,11 +192,9 @@ export async function initCommand(
192
192
  ? ['db', 'cache', 'mail', 'storage']
193
193
  : answers.services || [];
194
194
 
195
- // Determine events backend (default to pgboss for fullstack with --yes)
195
+ // Determine events backend (default to pgboss for all templates with --yes)
196
196
  const eventsBackend: EventsBackend | undefined = options.yes
197
- ? isFullstack
198
- ? 'pgboss'
199
- : undefined
197
+ ? 'pgboss'
200
198
  : answers.eventsBackend;
201
199
 
202
200
  const services: ServicesSelection = {
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
32
32
  '@geekmidas/cache': '~1.1.0',
33
33
  '@geekmidas/client': '~4.0.4',
34
34
  '@geekmidas/cloud': '~1.0.0',
35
- '@geekmidas/constructs': '~3.0.6',
35
+ '@geekmidas/constructs': '~3.0.7',
36
36
  '@geekmidas/db': '~1.0.1',
37
37
  '@geekmidas/emailkit': '~1.0.0',
38
- '@geekmidas/envkit': '~1.0.4',
38
+ '@geekmidas/envkit': '~1.0.5',
39
39
  '@geekmidas/errors': '~1.0.0',
40
40
  '@geekmidas/events': '~1.1.0',
41
41
  '@geekmidas/logger': '~1.0.0',
package/src/openapi.ts CHANGED
@@ -115,7 +115,7 @@ export async function openapiCommand(
115
115
  logger.log(`Found ${result.endpointCount} endpoints`);
116
116
  }
117
117
  } else {
118
- // Workspace config - generate for each backend app and copy to frontend clients
118
+ // Workspace config - generate for each backend app
119
119
  const { workspace } = loadedConfig;
120
120
  const workspaceRoot = options.cwd || process.cwd();
121
121
 
@@ -132,11 +132,6 @@ export async function openapiCommand(
132
132
  return;
133
133
  }
134
134
 
135
- // Find frontend apps with client config
136
- const frontendApps = Object.entries(workspace.apps).filter(
137
- ([_, app]) => app.type === 'frontend' && app.client?.output,
138
- );
139
-
140
135
  // Generate OpenAPI for each backend app
141
136
  for (const [appName, app] of backendApps) {
142
137
  if (app.type !== 'backend' || !app.routes) continue;
@@ -164,35 +159,6 @@ export async function openapiCommand(
164
159
  logger.log(
165
160
  `📄 [${appName}] Generated OpenAPI (${result.endpointCount} endpoints)`,
166
161
  );
167
-
168
- // Copy to frontend apps that depend on this backend
169
- for (const [frontendName, frontendApp] of frontendApps) {
170
- if (frontendApp.type !== 'frontend') continue;
171
-
172
- const dependsOnBackend =
173
- !frontendApp.dependencies ||
174
- frontendApp.dependencies.includes(appName);
175
-
176
- if (dependsOnBackend && frontendApp.client?.output) {
177
- const frontendPath = join(workspaceRoot, frontendApp.path);
178
- const clientOutputPath = join(
179
- frontendPath,
180
- frontendApp.client.output,
181
- 'openapi.ts',
182
- );
183
-
184
- await mkdir(dirname(clientOutputPath), { recursive: true });
185
-
186
- // Read the generated content and write to frontend
187
- const { readFile } = await import('node:fs/promises');
188
- const content = await readFile(result.outputPath, 'utf-8');
189
- await writeFile(clientOutputPath, content);
190
-
191
- logger.log(
192
- ` → [${frontendName}] ${frontendApp.client.output}/openapi.ts`,
193
- );
194
- }
195
- }
196
162
  }
197
163
  }
198
164
  }
package/src/types.ts CHANGED
@@ -332,6 +332,13 @@ export interface BuildOptions {
332
332
  skipBundle?: boolean;
333
333
  /** Stage for secrets injection (e.g., 'production', 'staging') */
334
334
  stage?: string;
335
+ /**
336
+ * When true, optional environment variables (those with `.optional()` or
337
+ * `.default()` in the envParser) are suffixed with `?` within each
338
+ * construct's `environment` array in the manifest (e.g. `PORT?`).
339
+ * Defaults to false.
340
+ */
341
+ markOptional?: boolean;
335
342
  }
336
343
 
337
344
  /** Result from build command when secrets are injected */