@vertile-ai/iac 0.0.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/README.md +255 -0
- package/docs/README.md +40 -0
- package/docs/index.html +276 -0
- package/docs/manifest.md +130 -0
- package/docs/positioning.md +65 -0
- package/docs/roadmap.md +77 -0
- package/examples/dynomic/README.md +22 -0
- package/examples/dynomic/apps/admin/package.json +7 -0
- package/examples/dynomic/apps/admin/vercel.json +4 -0
- package/examples/dynomic/apps/web/package.json +7 -0
- package/examples/dynomic/apps/web/vercel.json +4 -0
- package/examples/dynomic/infrastructure/iac/iac.json +191 -0
- package/examples/dynomic/package.json +17 -0
- package/package.json +51 -0
- package/schema/iac.schema.json +266 -0
- package/src/apply.mjs +38 -0
- package/src/cli.mjs +74 -0
- package/src/core/args.mjs +36 -0
- package/src/core/concepts.mjs +26 -0
- package/src/core/context.mjs +39 -0
- package/src/core/hcl.mjs +110 -0
- package/src/core/manifest.mjs +110 -0
- package/src/core/render.mjs +38 -0
- package/src/core/terraform.mjs +42 -0
- package/src/core/vercel-manifests.mjs +179 -0
- package/src/plan.mjs +32 -0
- package/src/providers/aws/index.mjs +153 -0
- package/src/providers/digitalocean/index.mjs +85 -0
- package/src/providers/vercel/index.mjs +60 -0
- package/src/provision-env.mjs +798 -0
- package/src/reconcile-project-domains.mjs +549 -0
- package/src/reconcile-project-settings.mjs +385 -0
- package/src/render.mjs +27 -0
- package/src/shared.mjs +116 -0
- package/src/sync-env.mjs +297 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import {
|
|
6
|
+
readVercelEnvManifest,
|
|
7
|
+
readVercelProjectSettingsManifest,
|
|
8
|
+
} from './core/vercel-manifests.mjs'
|
|
9
|
+
import { resolveIacContext } from './shared.mjs'
|
|
10
|
+
|
|
11
|
+
const iacContext = resolveIacContext(process.argv.slice(2), {
|
|
12
|
+
autoCreateKeys: 'landing,web-client,web-server,auth,preview,payment',
|
|
13
|
+
autoCreatePrefixes: 'template-',
|
|
14
|
+
})
|
|
15
|
+
const tokenFilePath = iacContext.tokenFilePath
|
|
16
|
+
const apiBase = 'https://api.vercel.com'
|
|
17
|
+
const shouldAutoCreateProject = iacContext.shouldAutoCreateProject
|
|
18
|
+
|
|
19
|
+
function readPositiveIntegerEnv(key, fallback) {
|
|
20
|
+
const value = Number(process.env[key])
|
|
21
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaultThrottleMs = readPositiveIntegerEnv('VERCEL_API_THROTTLE_MS', 250)
|
|
25
|
+
const maxRequestAttempts = Math.max(
|
|
26
|
+
1,
|
|
27
|
+
readPositiveIntegerEnv('VERCEL_API_MAX_ATTEMPTS', 4),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = {
|
|
32
|
+
apply: false,
|
|
33
|
+
projects: [],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const arg of argv) {
|
|
37
|
+
if (arg === '--apply') {
|
|
38
|
+
args.apply = true
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (arg.startsWith('--projects=')) {
|
|
43
|
+
args.projects = arg
|
|
44
|
+
.slice('--projects='.length)
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((value) => value.trim())
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return args
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readTokenFromFile(filePath) {
|
|
56
|
+
if (!fs.existsSync(filePath)) return ''
|
|
57
|
+
|
|
58
|
+
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/)
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const trimmed = line.trim()
|
|
61
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
62
|
+
|
|
63
|
+
if (trimmed.startsWith('VERCEL_TOKEN=')) {
|
|
64
|
+
return trimmed.slice('VERCEL_TOKEN='.length).trim()
|
|
65
|
+
}
|
|
66
|
+
if (trimmed.startsWith('VERCEL_API_KEY=')) {
|
|
67
|
+
return trimmed.slice('VERCEL_API_KEY='.length).trim()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return trimmed
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toQuery(params) {
|
|
77
|
+
const query = new URLSearchParams()
|
|
78
|
+
for (const [key, value] of Object.entries(params)) {
|
|
79
|
+
if (value === undefined || value === null || value === '') continue
|
|
80
|
+
query.set(key, String(value))
|
|
81
|
+
}
|
|
82
|
+
const encoded = query.toString()
|
|
83
|
+
return encoded ? `?${encoded}` : ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sleep(ms) {
|
|
87
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readRetryAfterMs(response, attempt) {
|
|
91
|
+
const retryAfter = response.headers.get('retry-after')
|
|
92
|
+
if (retryAfter) {
|
|
93
|
+
const retryAfterSeconds = Number(retryAfter)
|
|
94
|
+
if (Number.isFinite(retryAfterSeconds)) {
|
|
95
|
+
return Math.max(0, retryAfterSeconds * 1000)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const retryAfterDate = Date.parse(retryAfter)
|
|
99
|
+
if (Number.isFinite(retryAfterDate)) {
|
|
100
|
+
return Math.max(0, retryAfterDate - Date.now())
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reset = Number(response.headers.get('x-ratelimit-reset'))
|
|
105
|
+
if (Number.isFinite(reset) && reset > 0) {
|
|
106
|
+
return Math.max(0, reset * 1000 - Date.now())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Math.min(30000, 1000 * 2 ** attempt)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function requestJSON({ token, method, pathname, query, body }) {
|
|
113
|
+
const url = `${apiBase}${pathname}${toQuery(query || {})}`
|
|
114
|
+
|
|
115
|
+
for (let attempt = 0; attempt < maxRequestAttempts; attempt += 1) {
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
method,
|
|
118
|
+
headers: {
|
|
119
|
+
Authorization: `Bearer ${token}`,
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
Accept: 'application/json',
|
|
122
|
+
},
|
|
123
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
if (defaultThrottleMs > 0) await sleep(defaultThrottleMs)
|
|
128
|
+
if (response.status === 204) return {}
|
|
129
|
+
return response.json()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const errorText = await response.text()
|
|
133
|
+
if (response.status === 429 && attempt < maxRequestAttempts - 1) {
|
|
134
|
+
const delayMs = readRetryAfterMs(response, attempt)
|
|
135
|
+
console.warn(
|
|
136
|
+
`[rate-limit] Vercel API ${method} ${pathname} returned 429; retrying in ${Math.ceil(delayMs / 1000)}s`,
|
|
137
|
+
)
|
|
138
|
+
await sleep(delayMs)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Vercel API ${method} ${pathname} failed (${response.status}): ${errorText}`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`Vercel API ${method} ${pathname} failed after retries`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function resolveTeamId({ token, teamSlug }) {
|
|
151
|
+
const teams = await requestJSON({
|
|
152
|
+
token,
|
|
153
|
+
method: 'GET',
|
|
154
|
+
pathname: '/v1/teams',
|
|
155
|
+
})
|
|
156
|
+
const rows = Array.isArray(teams.teams) ? teams.teams : []
|
|
157
|
+
const match = rows.find((team) => team?.slug === teamSlug)
|
|
158
|
+
if (!match?.id) {
|
|
159
|
+
throw new Error(`Unable to resolve team ID for slug "${teamSlug}"`)
|
|
160
|
+
}
|
|
161
|
+
return match.id
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function listTeamProjects({ token, teamId }) {
|
|
165
|
+
const projects = await requestJSON({
|
|
166
|
+
token,
|
|
167
|
+
method: 'GET',
|
|
168
|
+
pathname: '/v9/projects',
|
|
169
|
+
query: { teamId, limit: 100 },
|
|
170
|
+
})
|
|
171
|
+
return Array.isArray(projects.projects) ? projects.projects : []
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function createTeamProject({ token, teamId, name }) {
|
|
175
|
+
const project = await requestJSON({
|
|
176
|
+
token,
|
|
177
|
+
method: 'POST',
|
|
178
|
+
pathname: '/v10/projects',
|
|
179
|
+
query: { teamId },
|
|
180
|
+
body: { name },
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const id =
|
|
184
|
+
typeof project?.id === 'string'
|
|
185
|
+
? project.id.trim()
|
|
186
|
+
: typeof project?.project?.id === 'string'
|
|
187
|
+
? project.project.id.trim()
|
|
188
|
+
: ''
|
|
189
|
+
|
|
190
|
+
if (!id) {
|
|
191
|
+
throw new Error(`Created Vercel project "${name}" but no project id was returned`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return id
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function diffSettings(current, desired) {
|
|
198
|
+
const keys = [
|
|
199
|
+
'rootDirectory',
|
|
200
|
+
'nodeVersion',
|
|
201
|
+
'enableAffectedProjectsDeployments',
|
|
202
|
+
]
|
|
203
|
+
const patch = {}
|
|
204
|
+
const diffs = []
|
|
205
|
+
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
const currentValue = current[key] ?? null
|
|
208
|
+
const desiredValue = desired[key] ?? null
|
|
209
|
+
if (currentValue !== desiredValue) {
|
|
210
|
+
patch[key] = desiredValue
|
|
211
|
+
diffs.push({ key, current: currentValue, desired: desiredValue })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { patch, diffs }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function main() {
|
|
219
|
+
const args = parseArgs(process.argv.slice(2))
|
|
220
|
+
const dryRun = !args.apply
|
|
221
|
+
|
|
222
|
+
const token =
|
|
223
|
+
process.env.VERCEL_TOKEN ||
|
|
224
|
+
process.env.VERCEL_API_KEY ||
|
|
225
|
+
readTokenFromFile(tokenFilePath)
|
|
226
|
+
|
|
227
|
+
if (!dryRun && !token) {
|
|
228
|
+
throw new Error('Missing VERCEL_TOKEN (or VERCEL_API_KEY) for apply mode')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const envManifest = readVercelEnvManifest(iacContext)
|
|
232
|
+
const projectSettingsManifest = readVercelProjectSettingsManifest(iacContext)
|
|
233
|
+
|
|
234
|
+
const configuredProjects = Array.isArray(envManifest.projects)
|
|
235
|
+
? envManifest.projects
|
|
236
|
+
: []
|
|
237
|
+
const projectSettings = Array.isArray(projectSettingsManifest.projects)
|
|
238
|
+
? projectSettingsManifest.projects
|
|
239
|
+
: []
|
|
240
|
+
const teamSlug = envManifest.teamSlug
|
|
241
|
+
if (!teamSlug || typeof teamSlug !== 'string') {
|
|
242
|
+
throw new Error('Missing or invalid teamSlug in the env manifest')
|
|
243
|
+
}
|
|
244
|
+
const teamId = token ? await resolveTeamId({ token, teamSlug }) : ''
|
|
245
|
+
|
|
246
|
+
const defaultSettings = projectSettingsManifest.defaults || {}
|
|
247
|
+
|
|
248
|
+
const projectByKey = new Map(
|
|
249
|
+
configuredProjects
|
|
250
|
+
.filter(
|
|
251
|
+
(project) =>
|
|
252
|
+
typeof project.key === 'string' &&
|
|
253
|
+
typeof project.id === 'string' &&
|
|
254
|
+
project.id.trim().length > 0,
|
|
255
|
+
)
|
|
256
|
+
.map((project) => [project.key, project]),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if (token && teamId) {
|
|
260
|
+
const remoteProjects = await listTeamProjects({ token, teamId })
|
|
261
|
+
const remoteIdByName = new Map(
|
|
262
|
+
remoteProjects
|
|
263
|
+
.filter(
|
|
264
|
+
(project) =>
|
|
265
|
+
typeof project.name === 'string' &&
|
|
266
|
+
project.name.trim().length > 0 &&
|
|
267
|
+
typeof project.id === 'string' &&
|
|
268
|
+
project.id.trim().length > 0
|
|
269
|
+
)
|
|
270
|
+
.map((project) => [project.name.trim(), project.id.trim()])
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for (const project of configuredProjects) {
|
|
274
|
+
if (
|
|
275
|
+
typeof project?.key !== 'string' ||
|
|
276
|
+
typeof project?.name !== 'string' ||
|
|
277
|
+
(typeof project?.id === 'string' && project.id.trim().length > 0)
|
|
278
|
+
) {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const projectName = project.name.trim()
|
|
283
|
+
let resolvedId = remoteIdByName.get(projectName)
|
|
284
|
+
|
|
285
|
+
if (!resolvedId && shouldAutoCreateProject(project.key)) {
|
|
286
|
+
if (dryRun) {
|
|
287
|
+
console.log(
|
|
288
|
+
`[plan] ${project.key}: would create Vercel project "${projectName}" because env-manifest id is missing`,
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resolvedId = await createTeamProject({
|
|
294
|
+
token,
|
|
295
|
+
teamId,
|
|
296
|
+
name: projectName,
|
|
297
|
+
})
|
|
298
|
+
remoteIdByName.set(projectName, resolvedId)
|
|
299
|
+
console.log(
|
|
300
|
+
`[created] ${project.key}: created Vercel project "${projectName}" -> ${resolvedId}`,
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!resolvedId) continue
|
|
305
|
+
|
|
306
|
+
projectByKey.set(project.key, { ...project, id: resolvedId })
|
|
307
|
+
console.log(
|
|
308
|
+
`[resolved] ${project.key}: using Vercel project "${project.name}" -> ${resolvedId}`
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const requested = args.projects.length ? new Set(args.projects) : null
|
|
314
|
+
|
|
315
|
+
for (const entry of projectSettings) {
|
|
316
|
+
if (requested && !requested.has(entry.key)) continue
|
|
317
|
+
|
|
318
|
+
const configured = projectByKey.get(entry.key)
|
|
319
|
+
if (!configured) {
|
|
320
|
+
console.log(`[skip] ${entry.key}: missing Vercel project ID in env-manifest`)
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const desired = {
|
|
325
|
+
rootDirectory: entry.rootDirectory ?? defaultSettings.rootDirectory ?? null,
|
|
326
|
+
nodeVersion: entry.nodeVersion ?? defaultSettings.nodeVersion ?? null,
|
|
327
|
+
enableAffectedProjectsDeployments:
|
|
328
|
+
entry.enableAffectedProjectsDeployments ??
|
|
329
|
+
defaultSettings.enableAffectedProjectsDeployments ??
|
|
330
|
+
null,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!token) {
|
|
334
|
+
console.log(
|
|
335
|
+
`[dry-run/offline] ${entry.key}: cannot fetch remote settings without token`,
|
|
336
|
+
)
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const project = await requestJSON({
|
|
341
|
+
token,
|
|
342
|
+
method: 'GET',
|
|
343
|
+
pathname: `/v9/projects/${encodeURIComponent(configured.id)}`,
|
|
344
|
+
query: { teamId },
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
const current = {
|
|
348
|
+
rootDirectory: project.rootDirectory ?? null,
|
|
349
|
+
nodeVersion: project.nodeVersion ?? null,
|
|
350
|
+
enableAffectedProjectsDeployments:
|
|
351
|
+
project.enableAffectedProjectsDeployments ?? null,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const { patch, diffs } = diffSettings(current, desired)
|
|
355
|
+
|
|
356
|
+
if (diffs.length === 0) {
|
|
357
|
+
console.log(`[ok] ${entry.key}: project settings already in sync`)
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
console.log(`[diff] ${entry.key}`)
|
|
362
|
+
for (const change of diffs) {
|
|
363
|
+
console.log(
|
|
364
|
+
` - ${change.key}: ${JSON.stringify(change.current)} -> ${JSON.stringify(change.desired)}`,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (dryRun) continue
|
|
369
|
+
|
|
370
|
+
await requestJSON({
|
|
371
|
+
token,
|
|
372
|
+
method: 'PATCH',
|
|
373
|
+
pathname: `/v9/projects/${encodeURIComponent(configured.id)}`,
|
|
374
|
+
query: { teamId },
|
|
375
|
+
body: patch,
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
console.log(`[applied] ${entry.key}: updated remote project settings`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
main().catch((error) => {
|
|
383
|
+
console.error('Error:', error instanceof Error ? error.message : String(error))
|
|
384
|
+
process.exit(1)
|
|
385
|
+
})
|
package/src/render.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { parseTargetOption, readOption } from './core/args.mjs'
|
|
5
|
+
import { resolvePlatformContext } from './core/context.mjs'
|
|
6
|
+
import { assertEnvironment, readManifest } from './core/manifest.mjs'
|
|
7
|
+
import { writeTargets } from './core/render.mjs'
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
const argv = process.argv.slice(2)
|
|
11
|
+
const context = resolvePlatformContext(argv)
|
|
12
|
+
const manifest = readManifest(context.manifestPath)
|
|
13
|
+
const environment = readOption(argv, '--env') || 'production'
|
|
14
|
+
const targets = parseTargetOption(argv)
|
|
15
|
+
|
|
16
|
+
assertEnvironment(manifest, environment)
|
|
17
|
+
|
|
18
|
+
const rendered = await writeTargets({ context, manifest, environment, targets })
|
|
19
|
+
for (const item of rendered) {
|
|
20
|
+
console.log(`Rendered ${item.workspace}`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main().catch((error) => {
|
|
25
|
+
console.error(error.message)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
})
|
package/src/shared.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function findProjectRoot(startDir) {
|
|
5
|
+
let current = startDir
|
|
6
|
+
const { root } = path.parse(current)
|
|
7
|
+
|
|
8
|
+
while (true) {
|
|
9
|
+
const hasPackageJson = fs.existsSync(path.join(current, 'package.json'))
|
|
10
|
+
const hasInfrastructure = fs.existsSync(path.join(current, 'infrastructure'))
|
|
11
|
+
if (hasPackageJson && hasInfrastructure) return current
|
|
12
|
+
if (current === root) break
|
|
13
|
+
current = path.dirname(current)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Could not find project root from ${startDir}. Pass --repo-root or run inside a project with package.json and infrastructure/.`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readOption(argv, name) {
|
|
22
|
+
const prefix = `${name}=`
|
|
23
|
+
const inline = argv.find((arg) => arg.startsWith(prefix))
|
|
24
|
+
if (inline) return inline.slice(prefix.length)
|
|
25
|
+
|
|
26
|
+
const index = argv.indexOf(name)
|
|
27
|
+
if (index !== -1) return argv[index + 1] || ''
|
|
28
|
+
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitList(value) {
|
|
33
|
+
return value
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((item) => item.trim())
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveFrom(rootDir, value) {
|
|
40
|
+
if (!value) return ''
|
|
41
|
+
return path.isAbsolute(value) ? value : path.join(rootDir, value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveIacContext(argv, defaults = {}) {
|
|
45
|
+
const repoRootArg = readOption(argv, '--repo-root')
|
|
46
|
+
const repoRoot = repoRootArg
|
|
47
|
+
? path.resolve(repoRootArg)
|
|
48
|
+
: findProjectRoot(process.cwd())
|
|
49
|
+
|
|
50
|
+
const iacDir = resolveFrom(
|
|
51
|
+
repoRoot,
|
|
52
|
+
readOption(argv, '--iac-dir') || defaults.iacDir || 'infrastructure/iac',
|
|
53
|
+
)
|
|
54
|
+
const manifestArg = readOption(argv, '--manifest')
|
|
55
|
+
const projectSettingsArg = readOption(argv, '--project-settings')
|
|
56
|
+
const projectDomainsArg = readOption(argv, '--project-domains')
|
|
57
|
+
const iacManifestArg = readOption(argv, '--iac-manifest')
|
|
58
|
+
const infraDir =
|
|
59
|
+
readOption(argv, '--infra-dir') || defaults.infraDir || ''
|
|
60
|
+
|
|
61
|
+
const autoCreateKeys = new Set([
|
|
62
|
+
...splitList(defaults.autoCreateKeys || ''),
|
|
63
|
+
...splitList(readOption(argv, '--auto-create-keys')),
|
|
64
|
+
])
|
|
65
|
+
const autoCreatePrefixes = [
|
|
66
|
+
...splitList(defaults.autoCreatePrefixes || ''),
|
|
67
|
+
...splitList(readOption(argv, '--auto-create-prefixes')),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
repoRoot,
|
|
72
|
+
iacDir,
|
|
73
|
+
infraDir,
|
|
74
|
+
manifestPath: resolveFrom(
|
|
75
|
+
repoRoot,
|
|
76
|
+
manifestArg || path.relative(repoRoot, path.join(iacDir, 'env-manifest.json')),
|
|
77
|
+
),
|
|
78
|
+
projectSettingsPath: resolveFrom(
|
|
79
|
+
repoRoot,
|
|
80
|
+
projectSettingsArg || path.relative(repoRoot, path.join(iacDir, 'project-settings.json')),
|
|
81
|
+
),
|
|
82
|
+
projectDomainsPath: resolveFrom(
|
|
83
|
+
repoRoot,
|
|
84
|
+
projectDomainsArg || path.relative(repoRoot, path.join(iacDir, 'project-domains.json')),
|
|
85
|
+
),
|
|
86
|
+
iacManifestPath: resolveFrom(
|
|
87
|
+
repoRoot,
|
|
88
|
+
iacManifestArg || path.relative(repoRoot, path.join(iacDir, 'iac.json')),
|
|
89
|
+
),
|
|
90
|
+
tokenFilePath: resolveFrom(
|
|
91
|
+
repoRoot,
|
|
92
|
+
readOption(argv, '--token-file') || defaults.tokenFile || '.vercel.token',
|
|
93
|
+
),
|
|
94
|
+
explicitManifestPath: Boolean(manifestArg),
|
|
95
|
+
explicitProjectSettingsPath: Boolean(projectSettingsArg),
|
|
96
|
+
explicitProjectDomainsPath: Boolean(projectDomainsArg),
|
|
97
|
+
explicitIacManifestPath: Boolean(iacManifestArg),
|
|
98
|
+
shouldAutoCreateProject(key) {
|
|
99
|
+
return autoCreateKeys.has(key) || autoCreatePrefixes.some((prefix) => key.startsWith(prefix))
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function sharedOptionsHelp() {
|
|
105
|
+
return `Shared options:
|
|
106
|
+
--repo-root <path> Project root containing infrastructure/.
|
|
107
|
+
--iac-dir <path> Directory containing project IaC manifests. Defaults to infrastructure/iac.
|
|
108
|
+
--manifest <path> Env manifest path. Defaults to <iac-dir>/env-manifest.json.
|
|
109
|
+
--project-settings <path> Project settings manifest path.
|
|
110
|
+
--project-domains <path> Project domains manifest path.
|
|
111
|
+
--iac-manifest <path> Unified IaC manifest path. Defaults to <iac-dir>/iac.json.
|
|
112
|
+
--infra-dir <path> Override manifest infraDir.
|
|
113
|
+
--token-file <path> Token file. Defaults to <repo-root>/.vercel.token.
|
|
114
|
+
--auto-create-keys <a,b> Project keys allowed to be created in apply mode.
|
|
115
|
+
--auto-create-prefixes <a,b> Project key prefixes allowed to be created in apply mode.`
|
|
116
|
+
}
|