@akash-chowdhury-24/deployhub 2.0.15 → 2.0.16

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.
@@ -1,6 +1,77 @@
1
1
  import chalk from 'chalk';
2
- import axios from 'axios';
3
2
  import { loadConfig, loadEnv } from '../core/config.js';
3
+ import { resolveEnvTargets } from '../core/environments.js';
4
+ import {
5
+ anyEnvHasResolvableHealthCheckUrl,
6
+ runHealthChecksForEnvs,
7
+ formatHealthCheckAllSummary,
8
+ } from '../utils/health-check.js';
9
+
10
+ /**
11
+ * Standalone verify — same per-env URL resolution and summary as the deploy pipeline stage.
12
+ *
13
+ * @param {import('../core/config.js').DeployHubConfig} config
14
+ * @param {string|undefined} envFlag — omitted = defaultEnvironment; `"all"` = every enabled
15
+ * @param {{ httpGet?: (url: string, timeoutMs: number) => Promise<{ status: number }> }} [options]
16
+ * @returns {Promise<{ ok: boolean, targets: string[], results: object[], failures: object[], summary: string, message: string }>}
17
+ */
18
+ export async function runVerify(config, envFlag, options = {}) {
19
+ const { targets, skippedDisabled } = resolveEnvTargets(config, envFlag);
20
+
21
+ if (targets.length === 0) {
22
+ return {
23
+ ok: false,
24
+ targets,
25
+ results: [],
26
+ failures: [],
27
+ summary: '',
28
+ message: 'No enabled environments to verify.',
29
+ skippedDisabled,
30
+ };
31
+ }
32
+
33
+ if (!anyEnvHasResolvableHealthCheckUrl(config, targets)) {
34
+ const label =
35
+ targets.length === 1
36
+ ? `environment "${targets[0]}"`
37
+ : 'the selected environment(s)';
38
+ return {
39
+ ok: false,
40
+ targets,
41
+ results: [],
42
+ failures: [],
43
+ summary: '',
44
+ message: `No health check URL configured for ${label}. Set healthCheck.url or environments.<name>.config.healthCheckUrl.`,
45
+ skippedDisabled,
46
+ };
47
+ }
48
+
49
+ const { results, failures } = await runHealthChecksForEnvs(config, targets, options);
50
+ const multi = targets.length > 1 || failures.length > 0;
51
+ const summary = multi ? formatHealthCheckAllSummary(results, failures) : '';
52
+
53
+ let message = '';
54
+ if (failures.length === 0 && results.length === 1 && !multi) {
55
+ const r = results[0];
56
+ message = `Health check passed (${r.envName}): HTTP ${r.status} (${r.elapsed}ms)`;
57
+ } else if (failures.length === 0 && results.length === 0) {
58
+ message = `No health check URL configured for the selected environment(s).`;
59
+ }
60
+
61
+ return {
62
+ ok: failures.length === 0 && results.length > 0,
63
+ targets,
64
+ results,
65
+ failures,
66
+ summary,
67
+ message:
68
+ message ||
69
+ (failures.length > 0
70
+ ? failures[0].error
71
+ : `All ${results.length} environment(s) passed health checks.`),
72
+ skippedDisabled,
73
+ };
74
+ }
4
75
 
5
76
  /**
6
77
  * @param {import('commander').Command} program
@@ -8,48 +79,41 @@ import { loadConfig, loadEnv } from '../core/config.js';
8
79
  export function registerVerifyCommand(program) {
9
80
  program
10
81
  .command('verify')
11
- .description('Run health check on configured endpoint')
12
- .action(async () => {
82
+ .description('Run health check on configured endpoint (default environment, or --env)')
83
+ .option(
84
+ '--env <name>',
85
+ 'Environment to verify (name, or "all" for every enabled environment)'
86
+ )
87
+ .action(async (opts) => {
13
88
  loadEnv();
14
89
  const config = await loadConfig();
15
- const url = config.healthCheck?.url;
16
90
 
17
- if (!url) {
18
- console.error(chalk.red('No health check URL configured in deployhub.config.json'));
91
+ let outcome;
92
+ try {
93
+ outcome = await runVerify(config, opts.env);
94
+ } catch (err) {
95
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
19
96
  process.exit(1);
20
97
  }
21
98
 
22
- const timeout = (config.healthCheck.timeout || 30) * 1000;
23
- const start = Date.now();
99
+ for (const name of outcome.skippedDisabled || []) {
100
+ console.log(chalk.gray(`Skipping disabled environment "${name}"`));
101
+ }
102
+
103
+ if (outcome.summary) {
104
+ console.log('');
105
+ console.log(outcome.summary);
106
+ } else if (outcome.ok && outcome.message) {
107
+ console.log(chalk.green(`✓ ${outcome.message}`));
108
+ }
24
109
 
25
- try {
26
- const response = await axios.get(url, {
27
- timeout,
28
- validateStatus: () => true,
29
- });
30
- const elapsed = Date.now() - start;
31
- const ok = response.status >= 200 && response.status < 400;
32
-
33
- if (ok) {
34
- console.log(
35
- chalk.green(`✓ Health check passed: HTTP ${response.status} (${elapsed}ms)`)
36
- );
37
- } else {
38
- console.log(
39
- chalk.red(`✗ Health check failed: HTTP ${response.status} (${elapsed}ms)`)
40
- );
41
- process.exit(1);
110
+ if (!outcome.ok) {
111
+ if (!outcome.summary && outcome.message) {
112
+ console.error(chalk.red(`✗ ${outcome.message}`));
42
113
  }
43
- } catch (err) {
44
- const elapsed = Date.now() - start;
45
- console.error(
46
- chalk.red(
47
- `✗ Health check failed: ${err instanceof Error ? err.message : String(err)} (${elapsed}ms)`
48
- )
49
- );
50
114
  process.exit(1);
51
115
  }
52
116
  });
53
117
  }
54
118
 
55
- export default { registerVerifyCommand };
119
+ export default { registerVerifyCommand, runVerify };
@@ -2,6 +2,20 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import { z } from 'zod';
4
4
  import dotenv from 'dotenv';
5
+ import { createLogger } from '../logger/index.js';
6
+ import { ensureLegacyHistoryCopiedToDefaultEnv } from '../storage/index.js';
7
+ import {
8
+ isNewEnvironmentShape,
9
+ getEnvMethod,
10
+ getEnvSettings,
11
+ isEnvEnabled,
12
+ getEnvTrigger,
13
+ resolveDefaultEnvironmentName,
14
+ getEnabledEnvironmentNames,
15
+ buildEnvironmentEntry,
16
+ resolveEnvTargets,
17
+ mergeMethodSettingsIntoEnv,
18
+ } from './environments.js';
5
19
 
6
20
  const SideConfigSchema = z.object({
7
21
  framework: z.string(),
@@ -12,36 +26,52 @@ const SideConfigSchema = z.object({
12
26
  port: z.number().optional(),
13
27
  });
14
28
 
29
+ /** Method-specific non-secret settings (host, paths, namespace, image name, etc.). */
30
+ const MethodConfigSchema = z
31
+ .object({
32
+ deploymentType: z.enum(['server']).optional(),
33
+ frontendDeploymentType: z.enum(['server']).optional(),
34
+ backendDeploymentType: z.enum(['server']).optional(),
35
+ host: z.string().optional(),
36
+ user: z.string().optional(),
37
+ path: z.string().optional(),
38
+ deployPath: z.string().optional(),
39
+ keyPath: z.string().optional(),
40
+ sshPort: z.number().optional(),
41
+ ec2InstanceId: z.string().optional(),
42
+ awsRegion: z.string().optional(),
43
+ azureSubscriptionId: z.string().optional(),
44
+ azureResourceGroup: z.string().optional(),
45
+ azureVmName: z.string().optional(),
46
+ gcpProjectId: z.string().optional(),
47
+ gcpZone: z.string().optional(),
48
+ gcpInstanceName: z.string().optional(),
49
+ kubeconfig: z.string().optional(),
50
+ kubeContext: z.string().optional(),
51
+ kubeNamespace: z.string().optional(),
52
+ dockerImageName: z.string().optional(),
53
+ dockerRegistryUrl: z.string().optional(),
54
+ dockerHost: z.string().optional(),
55
+ healthCheckUrl: z.string().optional(),
56
+ appName: z.string().optional(),
57
+ framework: z.string().optional(),
58
+ port: z.number().optional(),
59
+ frontendDeployPath: z.string().optional(),
60
+ backendDeployPath: z.string().optional(),
61
+ })
62
+ .passthrough();
63
+
64
+ const DEPLOY_METHODS = ['ssh', 'docker', 'ec2', 'azure-vm', 'gcp-vm', 'kubernetes'];
65
+
66
+ /**
67
+ * Per-environment entry. Storage (providers list / credentials) stays project-wide
68
+ * in this pass — per-env storage overrides are a future extension.
69
+ */
15
70
  const EnvironmentSchema = z.object({
16
- type: z.string().optional(),
17
- deploymentType: z.enum(['server']).optional(),
18
- frontendDeploymentType: z.enum(['server']).optional(),
19
- backendDeploymentType: z.enum(['server']).optional(),
20
- host: z.string().optional(),
21
- user: z.string().optional(),
22
- path: z.string().optional(),
23
- deployPath: z.string().optional(),
24
- keyPath: z.string().optional(),
25
- sshPort: z.number().optional(),
26
- ec2InstanceId: z.string().optional(),
27
- awsRegion: z.string().optional(),
28
- azureSubscriptionId: z.string().optional(),
29
- azureResourceGroup: z.string().optional(),
30
- azureVmName: z.string().optional(),
31
- gcpProjectId: z.string().optional(),
32
- gcpZone: z.string().optional(),
33
- gcpInstanceName: z.string().optional(),
34
- kubeconfig: z.string().optional(),
35
- kubeContext: z.string().optional(),
36
- kubeNamespace: z.string().optional(),
37
- dockerImageName: z.string().optional(),
38
- dockerRegistryUrl: z.string().optional(),
39
- dockerHost: z.string().optional(),
40
- appName: z.string().optional(),
41
- framework: z.string().optional(),
42
- port: z.number().optional(),
43
- frontendDeployPath: z.string().optional(),
44
- backendDeployPath: z.string().optional(),
71
+ enabled: z.boolean().default(true),
72
+ method: z.string(),
73
+ trigger: z.enum(['push', 'manual']).default('manual'),
74
+ config: MethodConfigSchema.default({}),
45
75
  });
46
76
 
47
77
  const ConfigSchema = z.object({
@@ -60,8 +90,21 @@ const ConfigSchema = z.object({
60
90
  docker: z.boolean().default(false),
61
91
  artifact: z.boolean().default(true),
62
92
  storage: z.array(z.string()).default(['local']),
63
- deploy: z.array(z.string()).default([]),
93
+ /** @deprecated Prefer environments.*.enabled + defaultEnvironment; kept optional for in-memory BC. */
94
+ deploy: z.array(z.string()).optional().default([]),
64
95
  environments: z.record(EnvironmentSchema).default({}),
96
+ defaultEnvironment: z.string().optional(),
97
+ /**
98
+ * Environment that keeps unprefixed CI secrets (SSH_HOST, not DEVELOPMENT_SSH_HOST).
99
+ * Set when the first environment is created; never silently changed when more envs are added.
100
+ */
101
+ unprefixedSecretEnvironment: z.string().optional(),
102
+ /**
103
+ * Set when a project is created via init or when a legacy flat config is silently
104
+ * migrated. Tells ensureLegacyHistoryCopiedToDefaultEnv not to copy the build catalog
105
+ * into the default env (multi-env-native projects never had pre-multi-env deploy history).
106
+ */
107
+ legacyHistoryMigrated: z.boolean().optional(),
65
108
  healthCheck: z
66
109
  .object({
67
110
  url: z.string().default(''),
@@ -93,9 +136,214 @@ const ConfigSchema = z.object({
93
136
  });
94
137
 
95
138
  /** @typedef {z.infer<typeof ConfigSchema>} DeployHubConfig */
139
+ /** @typedef {z.infer<typeof EnvironmentSchema>} EnvironmentEntry */
140
+ /** @typedef {z.infer<typeof MethodConfigSchema>} MethodConfig */
96
141
 
97
142
  const CONFIG_FILENAME = 'deployhub.config.json';
98
143
 
144
+ /** Top-level keys that indicated a pre-environments flat deploy config. */
145
+ const FLAT_DEPLOY_KEYS = [
146
+ 'type',
147
+ 'method',
148
+ 'host',
149
+ 'user',
150
+ 'path',
151
+ 'deployPath',
152
+ 'keyPath',
153
+ 'sshPort',
154
+ 'ec2InstanceId',
155
+ 'awsRegion',
156
+ 'azureSubscriptionId',
157
+ 'azureResourceGroup',
158
+ 'azureVmName',
159
+ 'gcpProjectId',
160
+ 'gcpZone',
161
+ 'gcpInstanceName',
162
+ 'kubeconfig',
163
+ 'kubeContext',
164
+ 'kubeNamespace',
165
+ 'dockerImageName',
166
+ 'dockerRegistryUrl',
167
+ 'dockerHost',
168
+ 'appName',
169
+ 'frontendDeployPath',
170
+ 'backendDeployPath',
171
+ 'deploymentType',
172
+ ];
173
+
174
+ /**
175
+ * @param {unknown} entry
176
+ * @returns {boolean}
177
+ */
178
+ // isNewEnvironmentShape imported from ./environments.js
179
+
180
+ /**
181
+ * @param {Record<string, unknown>} entry
182
+ * @returns {Record<string, unknown>}
183
+ */
184
+ function extractMethodConfig(entry) {
185
+ /** @type {Record<string, unknown>} */
186
+ const config = { ...(entry.config && typeof entry.config === 'object' ? entry.config : {}) };
187
+ for (const [key, value] of Object.entries(entry)) {
188
+ if (
189
+ key === 'enabled' ||
190
+ key === 'method' ||
191
+ key === 'trigger' ||
192
+ key === 'config' ||
193
+ key === 'type'
194
+ ) {
195
+ continue;
196
+ }
197
+ if (value !== undefined) {
198
+ config[key] = value;
199
+ }
200
+ }
201
+ return config;
202
+ }
203
+
204
+ /**
205
+ * @param {Record<string, unknown>} raw
206
+ * @returns {boolean}
207
+ */
208
+ export function needsConfigMigration(raw) {
209
+ if (!raw || typeof raw !== 'object') return false;
210
+
211
+ const envs = raw.environments;
212
+ if (!envs || typeof envs !== 'object' || Array.isArray(envs)) {
213
+ return FLAT_DEPLOY_KEYS.some((k) => raw[k] !== undefined);
214
+ }
215
+
216
+ const entries = Object.values(envs);
217
+ if (entries.some((e) => e && typeof e === 'object' && !isNewEnvironmentShape(e))) {
218
+ return true;
219
+ }
220
+
221
+ // Legacy deploy[] without defaultEnvironment (even when env shapes are already new)
222
+ if (Array.isArray(raw.deploy) && raw.deploy.length > 0 && !raw.defaultEnvironment) {
223
+ return true;
224
+ }
225
+
226
+ if (entries.length > 0 && !raw.defaultEnvironment) {
227
+ return true;
228
+ }
229
+
230
+ return false;
231
+ }
232
+
233
+ /**
234
+ * Migrate legacy flat / type-at-top-level environment configs to
235
+ * `{ enabled, method, trigger, config }` + `defaultEnvironment`.
236
+ * Idempotent: already-migrated configs are returned unchanged.
237
+ *
238
+ * @param {Record<string, unknown>} raw
239
+ * @returns {{ config: Record<string, unknown>, migrated: boolean, reason: string|null }}
240
+ */
241
+ export function migrateConfigToEnvironments(raw) {
242
+ if (!needsConfigMigration(raw)) {
243
+ return { config: raw, migrated: false, reason: null };
244
+ }
245
+
246
+ /** @type {Record<string, unknown>} */
247
+ const next = { ...raw };
248
+ const legacyDeploy = Array.isArray(raw.deploy) ? raw.deploy.map(String) : [];
249
+
250
+ /** @type {Record<string, Record<string, unknown>>} */
251
+ const newEnvironments = {};
252
+
253
+ const rawEnvs =
254
+ raw.environments && typeof raw.environments === 'object' && !Array.isArray(raw.environments)
255
+ ? /** @type {Record<string, Record<string, unknown>>} */ (raw.environments)
256
+ : null;
257
+
258
+ if (rawEnvs && Object.keys(rawEnvs).length > 0) {
259
+ for (const [name, entry] of Object.entries(rawEnvs)) {
260
+ if (!entry || typeof entry !== 'object') continue;
261
+ if (isNewEnvironmentShape(entry)) {
262
+ newEnvironments[name] = {
263
+ enabled: entry.enabled !== false,
264
+ method: String(entry.method),
265
+ trigger: entry.trigger === 'push' ? 'push' : 'manual',
266
+ config: extractMethodConfig(entry),
267
+ };
268
+ continue;
269
+ }
270
+
271
+ const method = String(entry.method || entry.type || 'ssh');
272
+ const enabled =
273
+ legacyDeploy.length === 0 ? true : legacyDeploy.includes(name);
274
+ newEnvironments[name] = {
275
+ enabled,
276
+ method,
277
+ trigger: entry.trigger === 'push' ? 'push' : 'manual',
278
+ config: extractMethodConfig(entry),
279
+ };
280
+ }
281
+ } else {
282
+ // Truly flat single-environment config (method/type + settings at top level).
283
+ const method = String(raw.method || raw.type || 'ssh');
284
+ /** @type {Record<string, unknown>} */
285
+ const flatConfig = {};
286
+ for (const key of FLAT_DEPLOY_KEYS) {
287
+ if (key === 'type' || key === 'method') continue;
288
+ if (raw[key] !== undefined) flatConfig[key] = raw[key];
289
+ }
290
+ newEnvironments.default = {
291
+ enabled: true,
292
+ method,
293
+ trigger: 'manual',
294
+ config: flatConfig,
295
+ };
296
+ for (const key of FLAT_DEPLOY_KEYS) {
297
+ delete next[key];
298
+ }
299
+ }
300
+
301
+ let defaultEnvironment =
302
+ typeof raw.defaultEnvironment === 'string' && raw.defaultEnvironment
303
+ ? raw.defaultEnvironment
304
+ : null;
305
+
306
+ if (!defaultEnvironment || !newEnvironments[defaultEnvironment]) {
307
+ if (legacyDeploy[0] && newEnvironments[legacyDeploy[0]]) {
308
+ defaultEnvironment = legacyDeploy[0];
309
+ } else if (newEnvironments.default) {
310
+ defaultEnvironment = 'default';
311
+ } else if (newEnvironments.production) {
312
+ defaultEnvironment = 'production';
313
+ } else {
314
+ const enabledNames = Object.entries(newEnvironments)
315
+ .filter(([, e]) => e.enabled !== false)
316
+ .map(([n]) => n);
317
+ defaultEnvironment =
318
+ enabledNames[0] || Object.keys(newEnvironments)[0] || 'default';
319
+ }
320
+ }
321
+
322
+ if (!newEnvironments[defaultEnvironment] && Object.keys(newEnvironments).length === 0) {
323
+ newEnvironments[defaultEnvironment] = {
324
+ enabled: true,
325
+ method: 'ssh',
326
+ trigger: 'manual',
327
+ config: {},
328
+ };
329
+ }
330
+
331
+ next.environments = newEnvironments;
332
+ next.defaultEnvironment = defaultEnvironment;
333
+ if (!next.unprefixedSecretEnvironment) {
334
+ next.unprefixedSecretEnvironment = defaultEnvironment;
335
+ }
336
+ // deploy[] is superseded by enabled + defaultEnvironment
337
+ delete next.deploy;
338
+
339
+ const reason =
340
+ !rawEnvs || Object.keys(rawEnvs).length === 0
341
+ ? 'flat'
342
+ : 'legacy-environments';
343
+
344
+ return { config: next, migrated: true, reason };
345
+ }
346
+
99
347
  /**
100
348
  * @param {string} [cwd]
101
349
  * @returns {string}
@@ -104,6 +352,20 @@ export function getConfigPath(cwd = process.cwd()) {
104
352
  return path.join(cwd, CONFIG_FILENAME);
105
353
  }
106
354
 
355
+ /**
356
+ * @param {DeployHubConfig} config
357
+ */
358
+ export function validateConfigConsistency(config) {
359
+ const envs = config.environments || {};
360
+ const def = config.defaultEnvironment;
361
+ if (typeof def === 'string' && def && !envs[def]) {
362
+ throw new Error(
363
+ `defaultEnvironment "${def}" is not defined in environments. ` +
364
+ 'Fix deployhub.config.json (typo?) or run deployhub env list.'
365
+ );
366
+ }
367
+ }
368
+
107
369
  /**
108
370
  * @param {string} [cwd]
109
371
  * @returns {Promise<DeployHubConfig>}
@@ -116,7 +378,43 @@ export async function loadConfig(cwd = process.cwd()) {
116
378
  );
117
379
  }
118
380
  const raw = await fs.readJson(configPath);
119
- const parsed = ConfigSchema.parse(raw);
381
+ const { config: migratedRaw, migrated, reason } = migrateConfigToEnvironments(raw);
382
+
383
+ if (migrated) {
384
+ const log = createLogger('config');
385
+ if (reason === 'flat') {
386
+ log.warn('Old single-environment config detected.');
387
+ log.warn(' Migrating to environments.default using your existing settings.');
388
+ log.warn(
389
+ ' Nothing changes in behavior — this just adds room for more environments later.'
390
+ );
391
+ } else {
392
+ log.warn('Legacy environment config format detected.');
393
+ log.warn(
394
+ ' Migrating to enabled/method/trigger/config shape (defaultEnvironment set).'
395
+ );
396
+ log.warn(
397
+ ' Nothing changes in behavior — this just adds room for more environments later.'
398
+ );
399
+ }
400
+ await fs.writeJson(configPath, migratedRaw, { spaces: 2 });
401
+
402
+ const storage = /** @type {string[]|undefined} */ (migratedRaw.storage);
403
+ if (
404
+ Array.isArray(storage) &&
405
+ storage.length > 0 &&
406
+ migratedRaw.project &&
407
+ migratedRaw.defaultEnvironment
408
+ ) {
409
+ await ensureLegacyHistoryCopiedToDefaultEnv(
410
+ storage,
411
+ String(migratedRaw.project),
412
+ String(migratedRaw.defaultEnvironment)
413
+ );
414
+ }
415
+ }
416
+
417
+ const parsed = ConfigSchema.parse(migratedRaw);
120
418
 
121
419
  if (!parsed.framework && parsed.projectType === 'frontend') {
122
420
  parsed.framework = 'node';
@@ -131,6 +429,27 @@ export async function loadConfig(cwd = process.cwd()) {
131
429
  parsed.version = '0.0.0';
132
430
  }
133
431
 
432
+ if (!parsed.defaultEnvironment) {
433
+ parsed.defaultEnvironment = resolveDefaultEnvironmentName(parsed) || undefined;
434
+ }
435
+
436
+ // Grandfather unprefixed CI secrets for the original environment (in-memory;
437
+ // persisted on next saveConfig / env add / migration rewrite).
438
+ if (!parsed.unprefixedSecretEnvironment) {
439
+ const envNames = Object.keys(parsed.environments || {});
440
+ if (envNames.length >= 1) {
441
+ parsed.unprefixedSecretEnvironment =
442
+ parsed.defaultEnvironment && parsed.environments[parsed.defaultEnvironment]
443
+ ? parsed.defaultEnvironment
444
+ : envNames[0];
445
+ }
446
+ }
447
+
448
+ // Keep deploy[] in sync for code paths that still read it (pipeline, doctor, workflows).
449
+ parsed.deploy = getEnabledEnvironmentNames(parsed);
450
+
451
+ validateConfigConsistency(parsed);
452
+
134
453
  return parsed;
135
454
  }
136
455
 
@@ -140,7 +459,11 @@ export async function loadConfig(cwd = process.cwd()) {
140
459
  */
141
460
  export async function saveConfig(config, cwd = process.cwd()) {
142
461
  const configPath = getConfigPath(cwd);
143
- await fs.writeJson(configPath, config, { spaces: 2 });
462
+ /** @type {Record<string, unknown>} */
463
+ const toWrite = { ...config };
464
+ // Persist new shape only — deploy[] is derived at load time.
465
+ delete toWrite.deploy;
466
+ await fs.writeJson(configPath, toWrite, { spaces: 2 });
144
467
  }
145
468
 
146
469
  /**
@@ -171,4 +494,19 @@ export async function appendEnv(vars, cwd = process.cwd()) {
171
494
  await fs.appendFile(envPath, `${separator}${lines.join('\n')}\n`);
172
495
  }
173
496
 
174
- export { ConfigSchema };
497
+ export {
498
+ ConfigSchema,
499
+ EnvironmentSchema,
500
+ MethodConfigSchema,
501
+ DEPLOY_METHODS,
502
+ isNewEnvironmentShape,
503
+ getEnvMethod,
504
+ getEnvSettings,
505
+ isEnvEnabled,
506
+ getEnvTrigger,
507
+ resolveDefaultEnvironmentName,
508
+ getEnabledEnvironmentNames,
509
+ buildEnvironmentEntry,
510
+ resolveEnvTargets,
511
+ mergeMethodSettingsIntoEnv,
512
+ };