@vertile-ai/iac 0.0.1 → 0.1.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.
Files changed (68) hide show
  1. package/README.md +73 -37
  2. package/docs/README.md +3 -2
  3. package/docs/index.html +2 -2
  4. package/docs/manifest.md +38 -5
  5. package/examples/bun-hono-api/README.md +9 -0
  6. package/examples/bun-hono-api/infrastructure/iac/iac.do.json +87 -0
  7. package/examples/bun-hono-api/infrastructure/iac/iac.json +107 -0
  8. package/examples/bun-hono-api/package.json +15 -0
  9. package/examples/bun-hono-api/src/index.ts +7 -0
  10. package/examples/go-api/README.md +8 -0
  11. package/examples/go-api/cmd/server/main.go +16 -0
  12. package/examples/go-api/go.mod +3 -0
  13. package/examples/go-api/infrastructure/iac/iac.do.json +90 -0
  14. package/examples/go-api/infrastructure/iac/iac.json +106 -0
  15. package/examples/next-monorepo/README.md +22 -0
  16. package/examples/next-monorepo/apps/admin/package.json +7 -0
  17. package/examples/next-monorepo/apps/web/package.json +7 -0
  18. package/examples/next-monorepo/infrastructure/iac/iac.aws.json +172 -0
  19. package/examples/next-monorepo/infrastructure/iac/iac.do.json +129 -0
  20. package/examples/{dynomic → next-monorepo}/infrastructure/iac/iac.json +102 -23
  21. package/examples/next-monorepo/infrastructure/iac/iac.vercel.json +109 -0
  22. package/examples/{dynomic → next-monorepo}/package.json +2 -2
  23. package/examples/node-api/README.md +8 -0
  24. package/examples/node-api/infrastructure/iac/iac.aws.json +132 -0
  25. package/examples/node-api/infrastructure/iac/iac.json +111 -0
  26. package/examples/node-api/package.json +12 -0
  27. package/examples/node-api/src/server.js +8 -0
  28. package/examples/python-fastapi-api/README.md +8 -0
  29. package/examples/python-fastapi-api/app/main.py +8 -0
  30. package/examples/python-fastapi-api/infrastructure/iac/iac.aws.json +129 -0
  31. package/examples/python-fastapi-api/infrastructure/iac/iac.json +113 -0
  32. package/examples/python-fastapi-api/pyproject.toml +10 -0
  33. package/examples/react-spa/README.md +8 -0
  34. package/examples/react-spa/index.html +2 -0
  35. package/examples/react-spa/infrastructure/iac/iac.aws.json +115 -0
  36. package/examples/react-spa/infrastructure/iac/iac.json +111 -0
  37. package/examples/react-spa/infrastructure/iac/iac.vercel.json +76 -0
  38. package/examples/react-spa/package.json +19 -0
  39. package/examples/react-spa/src/main.jsx +8 -0
  40. package/examples/sveltekit-web/README.md +9 -0
  41. package/examples/sveltekit-web/infrastructure/iac/iac.json +76 -0
  42. package/examples/sveltekit-web/infrastructure/iac/iac.vercel.json +74 -0
  43. package/examples/sveltekit-web/package.json +19 -0
  44. package/examples/sveltekit-web/src/routes/+page.svelte +3 -0
  45. package/examples/sveltekit-web/svelte.config.js +7 -0
  46. package/package.json +1 -1
  47. package/schema/iac.schema.json +83 -2
  48. package/src/apply.mjs +3 -4
  49. package/src/cli.mjs +1 -0
  50. package/src/core/context.mjs +4 -2
  51. package/src/core/deployments.mjs +39 -0
  52. package/src/core/env-files.mjs +38 -0
  53. package/src/core/env-source.mjs +5 -0
  54. package/src/core/hcl.mjs +2 -1
  55. package/src/core/manifest.mjs +15 -1
  56. package/src/core/render.mjs +19 -8
  57. package/src/core/vercel-manifests.mjs +5 -11
  58. package/src/plan.mjs +3 -4
  59. package/src/providers/aws/index.mjs +43 -24
  60. package/src/provision-env.mjs +72 -24
  61. package/src/render.mjs +3 -4
  62. package/src/shared.mjs +0 -4
  63. package/src/sync-env.mjs +33 -38
  64. package/examples/dynomic/README.md +0 -22
  65. package/examples/dynomic/apps/admin/package.json +0 -7
  66. package/examples/dynomic/apps/web/package.json +0 -7
  67. /package/examples/{dynomic → next-monorepo}/apps/admin/vercel.json +0 -0
  68. /package/examples/{dynomic → next-monorepo}/apps/web/vercel.json +0 -0
package/src/sync-env.mjs CHANGED
@@ -4,6 +4,8 @@ import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import process from 'node:process'
6
6
  import { resolvePlatformContext } from './core/context.mjs'
7
+ import { envSourceDir } from './core/env-source.mjs'
8
+ import { environmentFiles, environmentOutputFile } from './core/env-files.mjs'
7
9
  import { readManifest } from './core/manifest.mjs'
8
10
  import { readOption } from './shared.mjs'
9
11
 
@@ -26,30 +28,36 @@ function splitList(value) {
26
28
  .filter(Boolean)
27
29
  }
28
30
 
29
- function selectedVariantNames(argv) {
31
+ function configuredVariants(manifest) {
32
+ const variants = { ...defaultVariants }
33
+ const configured = manifest.env?.environments || {}
34
+ for (const [name, config] of Object.entries(configured)) {
35
+ const strict = config && typeof config === 'object' && !Array.isArray(config)
36
+ ? config.strict ?? true
37
+ : true
38
+ variants[name] = {
39
+ output: environmentOutputFile(manifest, name),
40
+ sources: environmentFiles(manifest, name),
41
+ strict,
42
+ }
43
+ }
44
+ return variants
45
+ }
46
+
47
+ function selectedVariantNames(argv, variants) {
30
48
  const value = readOption(argv, '--variants')
31
49
  if (!value) return ['local', 'production', 'staging', 'test']
32
50
 
33
51
  const names = splitList(value)
34
- const invalid = names.filter((name) => !defaultVariants[name])
52
+ const invalid = names.filter((name) => !variants[name])
35
53
  if (invalid.length > 0) {
36
54
  throw new Error(
37
- `Invalid --variants values: ${invalid.join(', ')}. Supported: ${Object.keys(defaultVariants).join(',')}`,
55
+ `Invalid --variants values: ${invalid.join(', ')}. Supported: ${Object.keys(variants).join(',')}`,
38
56
  )
39
57
  }
40
58
  return names
41
59
  }
42
60
 
43
- function envSourceDir(manifest) {
44
- return (
45
- manifest.env.sourceDir ||
46
- manifest.env.dir ||
47
- manifest.env.infraDir ||
48
- manifest.infraDir ||
49
- 'infrastructure'
50
- )
51
- }
52
-
53
61
  function parseEnvLine(line) {
54
62
  if (!line) return null
55
63
  const trimmed = line.trim()
@@ -89,23 +97,24 @@ function entriesToLines(entries) {
89
97
 
90
98
  function resolveLayer({ rootDir, baseDir, variant }) {
91
99
  const examplePath = path.join(baseDir, '.env.example')
92
- const sourcePath = path.join(baseDir, variant.sources[0])
100
+ const sourcePaths = variant.sources.map((source) => path.join(baseDir, source))
101
+ const existingSourcePaths = sourcePaths.filter((sourcePath) => fs.existsSync(sourcePath))
93
102
 
94
103
  if (variant.strict) {
95
- if (!fs.existsSync(sourcePath)) {
104
+ if (existingSourcePaths.length === 0) {
96
105
  throw new Error(
97
- `Missing required infrastructure env source file: ${path.relative(rootDir, sourcePath)}`,
106
+ `Missing required env source file: ${sourcePaths.map((sourcePath) => path.relative(rootDir, sourcePath)).join(' or ')}`,
98
107
  )
99
108
  }
100
109
  return {
101
- entries: readEnvFile(sourcePath),
102
- sourceLabel: path.relative(rootDir, sourcePath),
110
+ entries: mergeLayers(existingSourcePaths.map(readEnvFile)),
111
+ sourceLabel: existingSourcePaths.map((sourcePath) => path.relative(rootDir, sourcePath)).join(' + '),
103
112
  }
104
113
  }
105
114
 
106
- if (!fs.existsSync(examplePath) && !fs.existsSync(sourcePath)) {
115
+ if (!fs.existsSync(examplePath) && existingSourcePaths.length === 0) {
107
116
  throw new Error(
108
- `Missing required infrastructure env source files: ${path.relative(rootDir, sourcePath)} or ${path.relative(rootDir, examplePath)}`,
117
+ `Missing required env source files: ${sourcePaths.map((sourcePath) => path.relative(rootDir, sourcePath)).join(' or ')} or ${path.relative(rootDir, examplePath)}`,
109
118
  )
110
119
  }
111
120
 
@@ -115,7 +124,7 @@ function resolveLayer({ rootDir, baseDir, variant }) {
115
124
  layers.push(readEnvFile(examplePath))
116
125
  labels.push(path.relative(rootDir, examplePath))
117
126
  }
118
- if (fs.existsSync(sourcePath)) {
127
+ for (const sourcePath of existingSourcePaths) {
119
128
  layers.push(readEnvFile(sourcePath))
120
129
  labels.push(path.relative(rootDir, sourcePath))
121
130
  }
@@ -144,19 +153,6 @@ function projectSharedLayer(sharedLayer, app, sharedPrefixes = []) {
144
153
  .filter(({ key }) => key)
145
154
  }
146
155
 
147
- function assertNoSharedOverrides(sharedLayer, packageLayer, appKey) {
148
- const sharedKeys = new Set(sharedLayer.map(({ key }) => key))
149
- const overlaps = packageLayer
150
- .map(({ key }) => key)
151
- .filter((key, index, keys) => sharedKeys.has(key) && keys.indexOf(key) === index)
152
-
153
- if (overlaps.length === 0) return
154
-
155
- throw new Error(
156
- `Detected shared env key overrides for app "${appKey}": ${overlaps.join(', ')}`,
157
- )
158
- }
159
-
160
156
  function linesToEnvMap(lines) {
161
157
  const values = new Map()
162
158
  for (const line of lines) {
@@ -219,9 +215,10 @@ async function main() {
219
215
  const context = resolvePlatformContext(argv)
220
216
  const manifest = readManifest(context.manifestPath)
221
217
  const dryRun = hasFlag(argv, '--dry-run')
222
- const variants = selectedVariantNames(argv).map((name) => ({
218
+ const availableVariants = configuredVariants(manifest)
219
+ const variants = selectedVariantNames(argv, availableVariants).map((name) => ({
223
220
  name,
224
- ...defaultVariants[name],
221
+ ...availableVariants[name],
225
222
  }))
226
223
 
227
224
  if (shouldSkipSync(manifest)) {
@@ -248,8 +245,6 @@ async function main() {
248
245
  })
249
246
 
250
247
  const sharedEntries = projectSharedLayer(shared.entries, app, sharedPrefixes)
251
- assertNoSharedOverrides(sharedEntries, scoped.entries, app.key)
252
-
253
248
  const merged = mergeLayers([sharedEntries, scoped.entries])
254
249
  const mergedLines = entriesToLines(merged)
255
250
  const outputPath = path.join(appOutputDir(context.repoRoot, app), variant.output)
@@ -1,22 +0,0 @@
1
- # Dynomic
2
-
3
- Dynomic is a fixture project used to exercise Vertile AI IaC from the shape a
4
- product repo would keep. It intentionally uses the full current manifest surface:
5
- multiple Vercel apps, domains, preview/production env provisioning, local env
6
- sync, portable storage/database/queue/compute concepts, provider overrides, and
7
- provider-specific escape hatch resources.
8
-
9
- It intentionally contains no real secrets. The env files use placeholder values
10
- so package consumers can inspect and run dry-runs without extra setup.
11
-
12
- ## Commands
13
-
14
- ```bash
15
- vertile-iac sync-env --repo-root examples/dynomic --variants=staging,production
16
- vertile-iac env --repo-root examples/dynomic --targets=preview,production
17
- vertile-iac projects --repo-root examples/dynomic --projects=web
18
- vertile-iac domains --repo-root examples/dynomic --projects=web
19
- vertile-iac render --repo-root examples/dynomic --target=all --env=production
20
- ```
21
-
22
- Apply commands require `VERCEL_TOKEN` and a real Vercel team/project.
@@ -1,7 +0,0 @@
1
- {
2
- "name": "dynomic-admin",
3
- "private": true,
4
- "scripts": {
5
- "build": "echo \"dynomic admin fixture build\""
6
- }
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "name": "dynomic-web",
3
- "private": true,
4
- "scripts": {
5
- "build": "echo \"dynomic fixture build\""
6
- }
7
- }