t44 0.2.0-rc.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.
Potentially problematic release.
This version of t44 might be problematic. Click here for more details.
- package/LICENSE.md +203 -0
- package/README.md +154 -0
- package/bin/activate +36 -0
- package/bin/activate.ts +30 -0
- package/bin/postinstall.sh +19 -0
- package/bin/shell +27 -0
- package/bin/t44 +27 -0
- package/caps/HomeRegistry.v0.ts +298 -0
- package/caps/OpenApiSchema.v0.ts +192 -0
- package/caps/ProjectDeployment.v0.ts +363 -0
- package/caps/ProjectDevelopment.v0.ts +246 -0
- package/caps/ProjectPublishing.v0.ts +307 -0
- package/caps/ProjectRack.v0.ts +128 -0
- package/caps/WorkspaceCli.v0.ts +391 -0
- package/caps/WorkspaceConfig.v0.ts +626 -0
- package/caps/WorkspaceConfig.yaml +53 -0
- package/caps/WorkspaceConnection.v0.ts +240 -0
- package/caps/WorkspaceEntityConfig.v0.ts +64 -0
- package/caps/WorkspaceEntityFact.v0.ts +193 -0
- package/caps/WorkspaceInfo.v0.ts +554 -0
- package/caps/WorkspaceInit.v0.ts +30 -0
- package/caps/WorkspaceKey.v0.ts +186 -0
- package/caps/WorkspaceProjects.v0.ts +455 -0
- package/caps/WorkspacePrompt.v0.ts +396 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.v0.ts +104 -0
- package/caps/WorkspaceShell.yaml +65 -0
- package/caps/WorkspaceShellCli.v0.ts +109 -0
- package/caps/WorkspaceTest.v0.ts +167 -0
- package/caps/providers/LICENSE.md +8 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.v0.ts +328 -0
- package/caps/providers/bunny.net/api-pull.v0.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.v0.ts +161 -0
- package/caps/providers/bunny.net/api-storage.v0.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.v0.ts +245 -0
- package/caps/providers/bunny.net/api.v0.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.v0.ts +207 -0
- package/caps/providers/dynadot.com/api-domains.v0.test.ts +147 -0
- package/caps/providers/dynadot.com/api-domains.v0.ts +137 -0
- package/caps/providers/dynadot.com/api.v0.ts +88 -0
- package/caps/providers/git-scm.com/ProjectPublishing.v0.ts +231 -0
- package/caps/providers/github.com/ProjectPublishing.v0.ts +75 -0
- package/caps/providers/github.com/api.v0.ts +90 -0
- package/caps/providers/npmjs.com/ProjectPublishing.v0.ts +741 -0
- package/caps/providers/vercel.com/ProjectDeployment.v0.ts +339 -0
- package/caps/providers/vercel.com/api.v0.test.ts +67 -0
- package/caps/providers/vercel.com/api.v0.ts +132 -0
- package/caps/providers/vercel.com/bun.lock +194 -0
- package/caps/providers/vercel.com/package.json +10 -0
- package/caps/providers/vercel.com/project.v0.test.ts +108 -0
- package/caps/providers/vercel.com/project.v0.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +189 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/openapi.ts +132 -0
- package/lib/ucan.ts +137 -0
- package/package.json +41 -0
- package/structs/HomeRegistryConfig.v0.ts +27 -0
- package/structs/ProjectDeploymentConfig.v0.ts +27 -0
- package/structs/ProjectDeploymentFact.v0.ts +110 -0
- package/structs/ProjectPublishingFact.v0.ts +69 -0
- package/structs/ProjectRackConfig.v0.ts +27 -0
- package/structs/WorkspaceCliConfig.v0.ts +27 -0
- package/structs/WorkspaceConfig.v0.ts +27 -0
- package/structs/WorkspaceKeyConfig.v0.ts +27 -0
- package/structs/WorkspaceMappings.v0.ts +27 -0
- package/structs/WorkspaceProjectsConfig.v0.ts +27 -0
- package/structs/WorkspaceRepositories.v0.ts +27 -0
- package/structs/WorkspaceShellConfig.v0.ts +45 -0
- package/structs/providers/LICENSE.md +8 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/ProjectDeploymentFact.v0.ts +41 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.v0.ts +42 -0
- package/structs/providers/dynadot.com/DomainFact.v0.ts +146 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0.ts +41 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.v0.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.v0.ts +52 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.v0.ts +42 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.v0.ts +48 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.v0.ts +38 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.v0.ts +48 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +5 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import { mkdir, writeFile } from 'fs/promises'
|
|
5
|
+
|
|
6
|
+
export async function capsule({
|
|
7
|
+
encapsulate,
|
|
8
|
+
CapsulePropertyTypes,
|
|
9
|
+
makeImportStack
|
|
10
|
+
}: {
|
|
11
|
+
encapsulate: any
|
|
12
|
+
CapsulePropertyTypes: any
|
|
13
|
+
makeImportStack: any
|
|
14
|
+
}) {
|
|
15
|
+
// High level API that deals with everything concerning deployment of projects.
|
|
16
|
+
// NOTE: The API signatures do NOT match the vercel SDK and this is on purpose.
|
|
17
|
+
// The goal is to move towards a standard 'deployment' API that can be used across providers.
|
|
18
|
+
return encapsulate({
|
|
19
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
20
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
21
|
+
'#t44/structs/providers/vercel.com/ProjectDeploymentFact.v0': {
|
|
22
|
+
as: '$ProjectDeploymentFact'
|
|
23
|
+
},
|
|
24
|
+
'#t44/structs/ProjectDeploymentFact.v0': {
|
|
25
|
+
as: '$StatusFact'
|
|
26
|
+
},
|
|
27
|
+
'#': {
|
|
28
|
+
project: {
|
|
29
|
+
type: CapsulePropertyTypes.Mapping,
|
|
30
|
+
value: './project.v0'
|
|
31
|
+
},
|
|
32
|
+
deploy: {
|
|
33
|
+
type: CapsulePropertyTypes.Function,
|
|
34
|
+
value: async function (this: any, { projectionDir, alias, config }: { projectionDir: string, alias: string, config: any }) {
|
|
35
|
+
|
|
36
|
+
const projectName = config.provider.config.ProjectSettings.name
|
|
37
|
+
const projectSettings = {
|
|
38
|
+
...config.provider.config.ProjectSettings || {},
|
|
39
|
+
name: undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Ensure project '${projectName}' is created on Vercel ...`)
|
|
43
|
+
|
|
44
|
+
const details = await this.project.ensureCreated({
|
|
45
|
+
name: projectName,
|
|
46
|
+
settings: projectSettings
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
console.log(`Project ID: ${details.id}`)
|
|
50
|
+
|
|
51
|
+
// Set environment variables if configured
|
|
52
|
+
if (config.provider.config.ENV) {
|
|
53
|
+
console.log(`Managing environment variables ...`)
|
|
54
|
+
|
|
55
|
+
// Get existing environment variables
|
|
56
|
+
const team = await this.project.vercel.getDefaultTeam()
|
|
57
|
+
const existingVars = await (await this.project.vercel.vercel).projects.filterProjectEnvs({
|
|
58
|
+
idOrName: details.id,
|
|
59
|
+
slug: team
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const existingVarMap = new Map<string, string>(
|
|
63
|
+
existingVars.envs?.map((env: any) => [env.key as string, env.id as string]) || []
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const configuredKeys = new Set(Object.keys(config.provider.config.ENV))
|
|
67
|
+
|
|
68
|
+
// Delete variables that are no longer defined
|
|
69
|
+
for (const [key, envId] of existingVarMap.entries()) {
|
|
70
|
+
if (!configuredKeys.has(key)) {
|
|
71
|
+
await (await this.project.vercel.vercel).projects.removeProjectEnv({
|
|
72
|
+
idOrName: details.id,
|
|
73
|
+
slug: team,
|
|
74
|
+
id: envId
|
|
75
|
+
})
|
|
76
|
+
console.log(`Deleted environment variable: ${key}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create or update environment variables
|
|
81
|
+
for (const [key, value] of Object.entries(config.provider.config.ENV)) {
|
|
82
|
+
|
|
83
|
+
// If value is a function (jit expression), execute it
|
|
84
|
+
const resolvedValue = typeof value === 'function'
|
|
85
|
+
? await value()
|
|
86
|
+
: value as string
|
|
87
|
+
|
|
88
|
+
if (!existingVarMap.has(key)) {
|
|
89
|
+
// Create new variable
|
|
90
|
+
await (await this.project.vercel.vercel).projects.createProjectEnv({
|
|
91
|
+
idOrName: details.id,
|
|
92
|
+
slug: team,
|
|
93
|
+
requestBody: {
|
|
94
|
+
key,
|
|
95
|
+
value: resolvedValue,
|
|
96
|
+
type: 'encrypted',
|
|
97
|
+
target: ['production', 'preview', 'development']
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
console.log(`Created environment variable: ${key}`, resolvedValue)
|
|
101
|
+
} else {
|
|
102
|
+
// Update existing variable
|
|
103
|
+
const envId = existingVarMap.get(key)!
|
|
104
|
+
await (await this.project.vercel.vercel).projects.editProjectEnv({
|
|
105
|
+
idOrName: details.id,
|
|
106
|
+
slug: team,
|
|
107
|
+
id: envId,
|
|
108
|
+
requestBody: {
|
|
109
|
+
value: resolvedValue,
|
|
110
|
+
type: 'encrypted',
|
|
111
|
+
target: ['production', 'preview', 'development']
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
console.log(`Updated environment variable: ${key}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`Environment variables configured.`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const projectSourceDir = join(config.sourceDir)
|
|
122
|
+
const projectProjectionDir = join(projectionDir, 'projects', projectName)
|
|
123
|
+
|
|
124
|
+
await $`rm -Rf "${projectProjectionDir}" && mkdir -p "${projectProjectionDir}" && rsync -a "${projectSourceDir}/" "${projectProjectionDir}/"`
|
|
125
|
+
|
|
126
|
+
const vercelDir = join(projectProjectionDir, '.vercel')
|
|
127
|
+
await mkdir(vercelDir, { recursive: true })
|
|
128
|
+
|
|
129
|
+
const defaultTeam = await this.project.vercel.getDefaultTeam()
|
|
130
|
+
const projectJson = {
|
|
131
|
+
projectId: details.id,
|
|
132
|
+
orgId: await this.project.vercel.orgIdForName({
|
|
133
|
+
name: defaultTeam
|
|
134
|
+
}),
|
|
135
|
+
projectName
|
|
136
|
+
}
|
|
137
|
+
await writeFile(
|
|
138
|
+
join(vercelDir, 'project.json'),
|
|
139
|
+
JSON.stringify(projectJson, null, 4)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const vercelJsonConfig = config.provider.config['/vercel.json'] || {}
|
|
143
|
+
const vercelJson = {
|
|
144
|
+
framework: vercelJsonConfig.framework,
|
|
145
|
+
installCommand: vercelJsonConfig.installCommand || 'bun install',
|
|
146
|
+
buildCommand: vercelJsonConfig.buildCommand || 'bun run build',
|
|
147
|
+
outputDirectory: vercelJsonConfig.outputDirectory
|
|
148
|
+
}
|
|
149
|
+
await writeFile(
|
|
150
|
+
join(projectProjectionDir, 'vercel.json'),
|
|
151
|
+
JSON.stringify(vercelJson, null, 4)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
console.log(`Deploying to vercel ...`)
|
|
155
|
+
|
|
156
|
+
await $`vercel link --yes --project ${projectJson.projectName}`.cwd(projectProjectionDir)
|
|
157
|
+
await $`vercel deploy --force --target=preview`.cwd(projectProjectionDir)
|
|
158
|
+
// TODO: Add a workspace ID: '--meta WORKSPACE_ID=<id>'
|
|
159
|
+
|
|
160
|
+
console.log(`Deployment to vercel done.`)
|
|
161
|
+
|
|
162
|
+
// Fetch and store project details (automatically saved via $ProjectDeploymentFact)
|
|
163
|
+
await this.project.get({
|
|
164
|
+
name: details.name
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Write deployment status with updatedAt
|
|
168
|
+
const statusResult = {
|
|
169
|
+
projectName,
|
|
170
|
+
provider: 'vercel.com',
|
|
171
|
+
status: 'READY',
|
|
172
|
+
'#t44/structs/ProjectDeploymentConfig.v0': {
|
|
173
|
+
updatedAt: new Date().toISOString()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', statusResult)
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
status: {
|
|
180
|
+
type: CapsulePropertyTypes.Function,
|
|
181
|
+
value: async function (this: any, { config, now, passive }: { config: any; now?: boolean; passive?: boolean }) {
|
|
182
|
+
const projectName = config.provider.config.ProjectSettings.name
|
|
183
|
+
|
|
184
|
+
if (!projectName) {
|
|
185
|
+
return {
|
|
186
|
+
projectName: projectName || 'unknown',
|
|
187
|
+
provider: 'vercel.com',
|
|
188
|
+
error: 'No project name configured',
|
|
189
|
+
rawDefinitionFilepaths: []
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Raw fact filepaths that this status depends on
|
|
194
|
+
const rawFilepaths = [
|
|
195
|
+
this.$ProjectDeploymentFact.getRelativeFilepath('projects', projectName)
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
// Try to get cached status if not forcing refresh
|
|
199
|
+
if (!now) {
|
|
200
|
+
const cached = await this.$StatusFact.get('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', rawFilepaths)
|
|
201
|
+
if (cached) {
|
|
202
|
+
return cached.data
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// In passive mode, don't call the provider if no cache exists
|
|
207
|
+
if (passive) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const projectDetails = await this.project.get({
|
|
212
|
+
name: projectName
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if (!projectDetails) {
|
|
216
|
+
const errorResult = {
|
|
217
|
+
projectName,
|
|
218
|
+
provider: 'vercel.com',
|
|
219
|
+
error: 'Project not found',
|
|
220
|
+
rawDefinitionFilepaths: rawFilepaths
|
|
221
|
+
}
|
|
222
|
+
await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', errorResult)
|
|
223
|
+
return errorResult
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const statusTeam = await this.project.vercel.getDefaultTeam()
|
|
227
|
+
const deploymentsResponse = await (await this.project.vercel.vercel).deployments.getDeployments({
|
|
228
|
+
projectId: projectDetails.id,
|
|
229
|
+
teamId: await this.project.vercel.orgIdForName({
|
|
230
|
+
name: statusTeam
|
|
231
|
+
}),
|
|
232
|
+
limit: 1
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const latestDeployment = deploymentsResponse.deployments?.[0]
|
|
236
|
+
|
|
237
|
+
const statusMap: Record<string, string> = {
|
|
238
|
+
'READY': 'READY',
|
|
239
|
+
'BUILDING': 'BUILDING',
|
|
240
|
+
'ERROR': 'ERROR',
|
|
241
|
+
'CANCELED': 'ERROR',
|
|
242
|
+
'QUEUED': 'BUILDING'
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Preserve updatedAt from existing cached status
|
|
246
|
+
const existingStatus = await this.$StatusFact.get('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus')
|
|
247
|
+
const existingMeta = existingStatus?.data?.['#t44/structs/ProjectDeploymentConfig.v0']
|
|
248
|
+
|
|
249
|
+
const result: Record<string, any> = {
|
|
250
|
+
projectName: projectDetails.name,
|
|
251
|
+
provider: 'vercel.com',
|
|
252
|
+
status: statusMap[latestDeployment?.readyState] || 'UNKNOWN',
|
|
253
|
+
publicUrl: latestDeployment?.url ? `https://${latestDeployment.url}` : undefined,
|
|
254
|
+
createdAt: latestDeployment?.createdAt,
|
|
255
|
+
updatedAt: latestDeployment?.aliasAssigned,
|
|
256
|
+
providerProjectId: projectDetails.id,
|
|
257
|
+
providerPortalUrl: latestDeployment?.inspectorUrl,
|
|
258
|
+
rawDefinitionFilepaths: rawFilepaths
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (existingMeta?.updatedAt) {
|
|
262
|
+
result['#t44/structs/ProjectDeploymentConfig.v0'] = {
|
|
263
|
+
updatedAt: existingMeta.updatedAt
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', result)
|
|
268
|
+
|
|
269
|
+
return result
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
deprovision: {
|
|
273
|
+
type: CapsulePropertyTypes.Function,
|
|
274
|
+
value: async function (this: any, { config }: { config: any }) {
|
|
275
|
+
|
|
276
|
+
const projectName = config.provider.config.ProjectSettings.name
|
|
277
|
+
|
|
278
|
+
console.log(`Deprovisioning project '${projectName}' from Vercel ...`)
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Get project details to verify it exists
|
|
282
|
+
const details = await this.project.get({
|
|
283
|
+
name: projectName
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (!details) {
|
|
287
|
+
console.log(`Project '${projectName}' not found on Vercel. Nothing to deprovision.`)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log(`Found project ID: ${details.id}`)
|
|
292
|
+
|
|
293
|
+
// Delete the project
|
|
294
|
+
const deprovisionTeam = await this.project.vercel.getDefaultTeam()
|
|
295
|
+
await (await this.project.vercel.vercel).projects.deleteProject({
|
|
296
|
+
idOrName: details.id,
|
|
297
|
+
slug: deprovisionTeam
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
console.log(`Successfully deleted project '${projectName}' from Vercel.`)
|
|
301
|
+
|
|
302
|
+
// Delete fact files
|
|
303
|
+
console.log(`Deleting fact files ...`)
|
|
304
|
+
try {
|
|
305
|
+
await this.$ProjectDeploymentFact.delete('projects', projectName)
|
|
306
|
+
await this.$StatusFact.delete('ProjectDeploymentStatus', projectName)
|
|
307
|
+
console.log(`Fact files deleted`)
|
|
308
|
+
} catch (error: any) {
|
|
309
|
+
console.log(`Error deleting fact files: ${error.message}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
} catch (error: any) {
|
|
313
|
+
if (error.message?.includes('not found') || error.status === 404) {
|
|
314
|
+
console.log(`Project '${projectName}' not found on Vercel. Nothing to deprovision.`)
|
|
315
|
+
|
|
316
|
+
// Still delete fact files even if project not found
|
|
317
|
+
console.log(`Deleting fact files ...`)
|
|
318
|
+
try {
|
|
319
|
+
await this.$ProjectDeploymentFact.delete('projects', projectName)
|
|
320
|
+
await this.$StatusFact.delete('ProjectDeploymentStatus', projectName)
|
|
321
|
+
console.log(`Fact files deleted`)
|
|
322
|
+
} catch (factError: any) {
|
|
323
|
+
console.log(`Error deleting fact files: ${factError.message}`)
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
throw error
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}, {
|
|
334
|
+
importMeta: import.meta,
|
|
335
|
+
importStack: makeImportStack(),
|
|
336
|
+
capsuleName: capsule['#'],
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
capsule['#'] = 't44/caps/providers/vercel.com/ProjectDeployment.v0'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env bun test
|
|
2
|
+
|
|
3
|
+
export const testConfig = {
|
|
4
|
+
group: 'vendor',
|
|
5
|
+
runOnAll: false,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
import * as bunTest from 'bun:test'
|
|
9
|
+
import { run } from '../../../workspace-rt'
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
test: { describe, it, expect },
|
|
13
|
+
vercel
|
|
14
|
+
} = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
|
|
15
|
+
const spine = await encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
18
|
+
'#': {
|
|
19
|
+
test: {
|
|
20
|
+
type: CapsulePropertyTypes.Mapping,
|
|
21
|
+
value: 't44/caps/WorkspaceTest.v0',
|
|
22
|
+
options: {
|
|
23
|
+
'#': {
|
|
24
|
+
bunTest,
|
|
25
|
+
env: {
|
|
26
|
+
VERCEL_TOKEN: { factReference: 't44/structs/providers/vercel.com/WorkspaceConnectionConfig.v0:apiToken' }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
vercel: {
|
|
32
|
+
type: CapsulePropertyTypes.Mapping,
|
|
33
|
+
value: './api.v0'
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}, {
|
|
38
|
+
importMeta: import.meta,
|
|
39
|
+
importStack: makeImportStack(),
|
|
40
|
+
capsuleName: 't44/caps/providers/vercel.com/api.v0.test'
|
|
41
|
+
})
|
|
42
|
+
return { spine }
|
|
43
|
+
}, async ({ spine, apis }: any) => {
|
|
44
|
+
return apis[spine.capsuleSourceLineRef]
|
|
45
|
+
}, {
|
|
46
|
+
importMeta: import.meta
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('Vercel SDK', function () {
|
|
50
|
+
|
|
51
|
+
it('getTeams()', async function () {
|
|
52
|
+
|
|
53
|
+
const result = await vercel.getTeams()
|
|
54
|
+
|
|
55
|
+
expect(result).toBeObject()
|
|
56
|
+
expect(result.teams).toBeArray()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('getProjects()', async function () {
|
|
60
|
+
|
|
61
|
+
const result = await vercel.getProjects()
|
|
62
|
+
|
|
63
|
+
expect(result).toBeObject()
|
|
64
|
+
expect(result.projects).toBeArray()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
|
|
2
|
+
// Global promise to track ongoing team selection
|
|
3
|
+
let activeTeamSelection: Promise<string> | null = null
|
|
4
|
+
|
|
5
|
+
export async function capsule({
|
|
6
|
+
encapsulate,
|
|
7
|
+
CapsulePropertyTypes,
|
|
8
|
+
makeImportStack
|
|
9
|
+
}: {
|
|
10
|
+
encapsulate: any
|
|
11
|
+
CapsulePropertyTypes: any
|
|
12
|
+
makeImportStack: any
|
|
13
|
+
}) {
|
|
14
|
+
// Low level API that maps the vercel sdk API.
|
|
15
|
+
return encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
18
|
+
'#t44/structs/providers/vercel.com/WorkspaceConnectionConfig.v0': {
|
|
19
|
+
as: '$ConnectionConfig'
|
|
20
|
+
},
|
|
21
|
+
'#': {
|
|
22
|
+
// @see https://docs.vercel.com/docs/rest-api/reference/endpoints
|
|
23
|
+
vercel: {
|
|
24
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
25
|
+
value: async function (this: any) {
|
|
26
|
+
|
|
27
|
+
const { Vercel } = await import('@vercel/sdk');
|
|
28
|
+
|
|
29
|
+
const apiToken = await this.$ConnectionConfig.getConfigValue('apiToken')
|
|
30
|
+
|
|
31
|
+
const vercel = new Vercel({
|
|
32
|
+
bearerToken: apiToken
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return vercel
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
getDefaultTeam: {
|
|
39
|
+
type: CapsulePropertyTypes.Function,
|
|
40
|
+
value: async function (this: any): Promise<string> {
|
|
41
|
+
// Check if already configured in stored config
|
|
42
|
+
const storedConfig = await this.$ConnectionConfig.getStoredConfig() || {}
|
|
43
|
+
if (storedConfig.team) {
|
|
44
|
+
return storedConfig.team
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if team selection is already in progress
|
|
48
|
+
if (activeTeamSelection) {
|
|
49
|
+
return activeTeamSelection
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Start team selection process
|
|
53
|
+
activeTeamSelection = (async () => {
|
|
54
|
+
try {
|
|
55
|
+
// Fetch teams from API
|
|
56
|
+
const chalk = (await import('chalk')).default
|
|
57
|
+
console.log(chalk.gray(' Fetching your Vercel teams...\n'))
|
|
58
|
+
|
|
59
|
+
const teamsResponse = await this.getTeams()
|
|
60
|
+
const teams = teamsResponse.teams || []
|
|
61
|
+
|
|
62
|
+
if (teams.length === 0) {
|
|
63
|
+
throw new Error('No teams found in your Vercel account')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Prepare choices for selection
|
|
67
|
+
const choices = teams.map((team: any) => ({
|
|
68
|
+
name: `${team.name} (${team.slug})`,
|
|
69
|
+
value: team.slug
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
// Prompt user to select team
|
|
73
|
+
const inquirer = await import('inquirer')
|
|
74
|
+
const { selectedTeam } = await inquirer.default.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: 'list',
|
|
77
|
+
name: 'selectedTeam',
|
|
78
|
+
message: 'Default Team',
|
|
79
|
+
choices
|
|
80
|
+
}
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
// Store the selected team
|
|
84
|
+
const updatedConfig = await this.$ConnectionConfig.getStoredConfig() || {}
|
|
85
|
+
updatedConfig.team = selectedTeam
|
|
86
|
+
await this.$ConnectionConfig.setStoredConfig(updatedConfig)
|
|
87
|
+
|
|
88
|
+
console.log(chalk.green(`\n ✓ Default Team saved to connection config\n`))
|
|
89
|
+
|
|
90
|
+
return selectedTeam
|
|
91
|
+
} finally {
|
|
92
|
+
activeTeamSelection = null
|
|
93
|
+
}
|
|
94
|
+
})()
|
|
95
|
+
|
|
96
|
+
return activeTeamSelection
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
getTeams: {
|
|
100
|
+
type: CapsulePropertyTypes.Function,
|
|
101
|
+
value: async function (this: any) {
|
|
102
|
+
return (await this.vercel).teams.getTeams({})
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
getProjects: {
|
|
106
|
+
type: CapsulePropertyTypes.Function,
|
|
107
|
+
value: async function (this: any) {
|
|
108
|
+
return (await this.vercel).projects.getProjects({})
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
orgIdForName: {
|
|
112
|
+
type: CapsulePropertyTypes.Function,
|
|
113
|
+
value: async function (this: any, { name }: { name: string }) {
|
|
114
|
+
const teamsResponse = await (await this.vercel).teams.getTeams({})
|
|
115
|
+
const team = teamsResponse.teams?.find((t: any) => t.slug === name || t.name === name)
|
|
116
|
+
|
|
117
|
+
if (!team) {
|
|
118
|
+
throw new Error(`Team '${name}' not found`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return team.id
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, {
|
|
127
|
+
importMeta: import.meta,
|
|
128
|
+
importStack: makeImportStack(),
|
|
129
|
+
capsuleName: capsule['#'],
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
capsule['#'] = 't44/caps/providers/vercel.com/api.v0'
|