t44 0.4.0-rc.10
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/.dco-signatures +9 -0
- package/.github/workflows/dco.yaml +12 -0
- package/.github/workflows/gordian-open-integrity.yaml +13 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +203 -0
- package/README.md +185 -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/ConfigSchemaStruct.ts +55 -0
- package/caps/Home.ts +57 -0
- package/caps/HomeRegistry.ts +319 -0
- package/caps/HomeRegistryFile.ts +144 -0
- package/caps/JsonSchemas.ts +220 -0
- package/caps/OpenApiSchema.ts +67 -0
- package/caps/PackageDescriptor.ts +88 -0
- package/caps/ProjectCatalogs.ts +153 -0
- package/caps/ProjectDeployment.ts +363 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +522 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +322 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/WorkspaceCli.ts +442 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +71 -0
- package/caps/WorkspaceConfigFile.ts +807 -0
- package/caps/WorkspaceConnection.ts +256 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +595 -0
- package/caps/WorkspaceInit.ts +30 -0
- package/caps/WorkspaceKey.ts +338 -0
- package/caps/WorkspaceModel.ts +373 -0
- package/caps/WorkspaceProjects.ts +636 -0
- package/caps/WorkspacePrompt.ts +406 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.ts +104 -0
- package/caps/WorkspaceShell.yaml +64 -0
- package/caps/WorkspaceShellCli.ts +109 -0
- package/caps/WorkspaceTest.ts +167 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
- package/caps/providers/bunny.net/api-pull.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.ts +164 -0
- package/caps/providers/bunny.net/api-storage.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.ts +248 -0
- package/caps/providers/bunny.net/api.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
- package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
- package/caps/providers/dynadot.com/api-domains.ts +169 -0
- package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
- package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
- package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
- package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
- package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
- package/caps/providers/github.com/ProjectPublishing.ts +133 -0
- package/caps/providers/github.com/api.ts +130 -0
- package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
- package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
- package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
- package/caps/providers/vercel.com/api.test.ts +67 -0
- package/caps/providers/vercel.com/api.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.test.ts +108 -0
- package/caps/providers/vercel.com/project.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/examples/01-Lifecycle/main.test.ts +228 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +369 -0
- package/lib/schema-console-renderer.ts +181 -0
- package/lib/schema-resolver.ts +349 -0
- package/lib/ucan.ts +137 -0
- package/package.json +102 -0
- package/standalone-rt.ts +121 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +60 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectRack.ts +51 -0
- package/structs/ProjectRackConfig.ts +56 -0
- package/structs/RepositoryOriginDescriptor.ts +51 -0
- package/structs/RootKeyConfig.ts +64 -0
- package/structs/SigningKeyConfig.ts +64 -0
- package/structs/Workspace.ts +56 -0
- package/structs/WorkspaceCatalogs.ts +56 -0
- package/structs/WorkspaceCliConfig.ts +53 -0
- package/structs/WorkspaceConfig.ts +64 -0
- package/structs/WorkspaceConfigFile.ts +50 -0
- package/structs/WorkspaceConfigFileMeta.ts +70 -0
- package/structs/WorkspaceKey.ts +55 -0
- package/structs/WorkspaceKeyConfig.ts +56 -0
- package/structs/WorkspaceMappingsConfig.ts +56 -0
- package/structs/WorkspaceProject.ts +104 -0
- package/structs/WorkspaceProjectsConfig.ts +67 -0
- package/structs/WorkspacePublishingConfig.ts +65 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
- package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/dynadot.com/DomainFact.ts +46 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +3 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
|
|
4
|
+
export async function capsule({
|
|
5
|
+
encapsulate,
|
|
6
|
+
CapsulePropertyTypes,
|
|
7
|
+
makeImportStack
|
|
8
|
+
}: {
|
|
9
|
+
encapsulate: any
|
|
10
|
+
CapsulePropertyTypes: any
|
|
11
|
+
makeImportStack: any
|
|
12
|
+
}) {
|
|
13
|
+
return encapsulate({
|
|
14
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
15
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
16
|
+
'#t44/structs/ProjectDeploymentConfig': {
|
|
17
|
+
as: '$ProjectDeploymentConfig',
|
|
18
|
+
},
|
|
19
|
+
'#t44/structs/WorkspaceConfig': {
|
|
20
|
+
as: '$WorkspaceConfig'
|
|
21
|
+
},
|
|
22
|
+
'#': {
|
|
23
|
+
WorkspacePrompt: {
|
|
24
|
+
type: CapsulePropertyTypes.Mapping,
|
|
25
|
+
value: 't44/caps/WorkspacePrompt'
|
|
26
|
+
},
|
|
27
|
+
Vercel: {
|
|
28
|
+
type: CapsulePropertyTypes.Mapping,
|
|
29
|
+
value: 't44/caps/providers/vercel.com/ProjectDeployment'
|
|
30
|
+
},
|
|
31
|
+
Bunny: {
|
|
32
|
+
type: CapsulePropertyTypes.Mapping,
|
|
33
|
+
value: 't44/caps/providers/bunny.net/ProjectDeployment'
|
|
34
|
+
},
|
|
35
|
+
Dynadot: {
|
|
36
|
+
type: CapsulePropertyTypes.Mapping,
|
|
37
|
+
value: 't44/caps/providers/dynadot.com/ProjectDeployment'
|
|
38
|
+
},
|
|
39
|
+
WorkspaceProjects: {
|
|
40
|
+
type: CapsulePropertyTypes.Mapping,
|
|
41
|
+
value: 't44/caps/WorkspaceProjects'
|
|
42
|
+
},
|
|
43
|
+
run: {
|
|
44
|
+
type: CapsulePropertyTypes.Function,
|
|
45
|
+
value: async function (this: any, { args }: any): Promise<void> {
|
|
46
|
+
|
|
47
|
+
let { projectSelector, deprovision, yes } = args
|
|
48
|
+
|
|
49
|
+
const deploymentConfig = await this.$ProjectDeploymentConfig.config
|
|
50
|
+
|
|
51
|
+
if (!deploymentConfig?.deployments) {
|
|
52
|
+
throw new Error('No deployments configuration found')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let matchingDeployments: Record<string, any> = {}
|
|
56
|
+
|
|
57
|
+
if (!projectSelector) {
|
|
58
|
+
// Show interactive project selection
|
|
59
|
+
const chalk = (await import('chalk')).default
|
|
60
|
+
const allProjects = Object.keys(deploymentConfig.deployments)
|
|
61
|
+
|
|
62
|
+
if (allProjects.length === 0) {
|
|
63
|
+
throw new Error('No deployments configured')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Display heading
|
|
67
|
+
const actionText = deprovision ? 'deprovision' : 'deploy'
|
|
68
|
+
console.log(chalk.cyan(`\nPick a project to ${actionText}. You will be asked for necessary credentials as needed.\n`))
|
|
69
|
+
|
|
70
|
+
// Build choices with deployment status
|
|
71
|
+
const choices: Array<{ name: string; value: string }> = []
|
|
72
|
+
|
|
73
|
+
for (const projectName of allProjects) {
|
|
74
|
+
const projectAliases = deploymentConfig.deployments[projectName]
|
|
75
|
+
const aliasNames = Object.keys(projectAliases)
|
|
76
|
+
const firstAlias = aliasNames[0]
|
|
77
|
+
const aliasConfig = projectAliases[firstAlias]
|
|
78
|
+
|
|
79
|
+
// Support both 'provider' (single) and 'providers' (array) patterns
|
|
80
|
+
const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
|
|
81
|
+
|
|
82
|
+
// Extract provider name from the first provider's capsule path
|
|
83
|
+
const firstCapsulePath = providers[0]?.capsule || ''
|
|
84
|
+
const providerMatch = firstCapsulePath.match(/providers\/([^/]+)\//)
|
|
85
|
+
const providerName = providerMatch ? providerMatch[1] : 'unknown'
|
|
86
|
+
|
|
87
|
+
// Check deployment status across all providers that support it
|
|
88
|
+
let statusText = ''
|
|
89
|
+
let isDeployed = false
|
|
90
|
+
try {
|
|
91
|
+
let status: any
|
|
92
|
+
for (const providerConfig of providers) {
|
|
93
|
+
const capsulePath = providerConfig.capsule || ''
|
|
94
|
+
const config = { ...aliasConfig, provider: providerConfig }
|
|
95
|
+
if (capsulePath.includes('vercel.com')) {
|
|
96
|
+
status = await this.Vercel.status({ config, passive: true })
|
|
97
|
+
} else if (capsulePath.includes('bunny.net')) {
|
|
98
|
+
status = await this.Bunny.status({ config, passive: true })
|
|
99
|
+
}
|
|
100
|
+
// Use the first provider that returns a valid status
|
|
101
|
+
if (status && !status.error) break
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!status || status?.error) {
|
|
105
|
+
statusText = chalk.gray('not deployed')
|
|
106
|
+
} else if (status?.status === 'READY') {
|
|
107
|
+
isDeployed = true
|
|
108
|
+
// Calculate deployment duration
|
|
109
|
+
let durationText = ''
|
|
110
|
+
if (status.createdAt || status.updatedAt) {
|
|
111
|
+
const deployedDate = new Date(status.updatedAt || status.createdAt)
|
|
112
|
+
const now = new Date()
|
|
113
|
+
const diffMs = now.getTime() - deployedDate.getTime()
|
|
114
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
115
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
116
|
+
const diffMinutes = Math.floor(diffMs / (1000 * 60))
|
|
117
|
+
|
|
118
|
+
if (diffDays > 0) {
|
|
119
|
+
durationText = chalk.gray(` (${diffDays}d ago)`)
|
|
120
|
+
} else if (diffHours > 0) {
|
|
121
|
+
durationText = chalk.gray(` (${diffHours}h ago)`)
|
|
122
|
+
} else if (diffMinutes > 0) {
|
|
123
|
+
durationText = chalk.gray(` (${diffMinutes}m ago)`)
|
|
124
|
+
} else {
|
|
125
|
+
durationText = chalk.gray(' (just now)')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
statusText = chalk.green('deployed') + durationText
|
|
129
|
+
} else if (status?.status) {
|
|
130
|
+
statusText = chalk.gray(status.status.toLowerCase())
|
|
131
|
+
} else {
|
|
132
|
+
statusText = chalk.gray('not deployed')
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
statusText = chalk.gray('not deployed')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// When deprovisioning, only show deployed projects
|
|
139
|
+
if (deprovision && !isDeployed) continue
|
|
140
|
+
|
|
141
|
+
const providerText = chalk.cyan(`[${providerName}]`)
|
|
142
|
+
const aliasText = chalk.gray(`[${aliasNames.join(', ')}]`)
|
|
143
|
+
const projectText = chalk.white(projectName)
|
|
144
|
+
|
|
145
|
+
choices.push({
|
|
146
|
+
name: `${projectText} ${providerText} ${aliasText} ${statusText}`,
|
|
147
|
+
value: projectName
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (choices.length === 0) {
|
|
152
|
+
console.log(chalk.gray('No deployed projects found.\n'))
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const selectedProject = await this.WorkspacePrompt.select({
|
|
158
|
+
message: `Select a project:`,
|
|
159
|
+
choices
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Set the selected project as workspaceProject for further processing
|
|
163
|
+
matchingDeployments[selectedProject] = deploymentConfig.deployments[selectedProject]
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
|
|
166
|
+
console.log(chalk.red('\nABORTED\n'))
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
throw error
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
matchingDeployments = await this.WorkspaceProjects.resolveMatchingDeployments({
|
|
173
|
+
workspaceProject: projectSelector,
|
|
174
|
+
deployments: deploymentConfig.deployments
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Deploy or deprovision each matching project
|
|
179
|
+
for (const [projectName, deploymentConfig] of Object.entries(matchingDeployments)) {
|
|
180
|
+
if (deprovision) {
|
|
181
|
+
console.log(`\n=> Deprovisioning project '${projectName}' ...\n`)
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`\n=> Deploying project '${projectName}' ...\n`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const orderedAliases = orderAliasesByDependencies(deploymentConfig)
|
|
187
|
+
|
|
188
|
+
// For deprovision, confirm once at the project level
|
|
189
|
+
if (deprovision && !yes) {
|
|
190
|
+
const chalk = (await import('chalk')).default
|
|
191
|
+
const aliasNames = orderedAliases.join(', ')
|
|
192
|
+
console.log(chalk.red(`\n⚠️ WARNING: You are about to DELETE all deployments for project '${projectName}':\n`))
|
|
193
|
+
console.log(chalk.red(` Aliases: ${aliasNames}`))
|
|
194
|
+
console.log(chalk.red(`\n ⚠️ THIS ACTION IS IRREVERSIBLE ⚠️\n`))
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const confirmation = await this.WorkspacePrompt.input({
|
|
198
|
+
message: chalk.red(`To confirm deletion, type the project name exactly: "${projectName}"`),
|
|
199
|
+
defaultValue: ''
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
if (confirmation !== projectName) {
|
|
203
|
+
console.log(chalk.red('\n⚠️ ABORTED\n'))
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log(chalk.red(`\n✓ Confirmation received. Proceeding with deprovisioning...\n`))
|
|
208
|
+
} catch (error: any) {
|
|
209
|
+
if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
|
|
210
|
+
console.log(chalk.red('\n\nABORTED\n'))
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
throw error
|
|
214
|
+
}
|
|
215
|
+
} else if (deprovision && yes) {
|
|
216
|
+
const chalk = (await import('chalk')).default
|
|
217
|
+
console.log(chalk.red(`\n✓ Auto-confirmed with --yes flag. Proceeding with deprovisioning...\n`))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// For deprovision, reverse the order to handle dependencies correctly
|
|
221
|
+
const aliasesToProcess = deprovision ? orderedAliases.reverse() : orderedAliases
|
|
222
|
+
|
|
223
|
+
for (const alias of aliasesToProcess) {
|
|
224
|
+
if (deprovision) {
|
|
225
|
+
console.log(`\n=> Deprovisioning provider project alias '${alias}' for workspace project '${projectName}' ...\n`)
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`\n=> Running deployment of provider project alias '${alias}' for workspace project '${projectName}' ...\n`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const aliasConfig = deploymentConfig[alias]
|
|
231
|
+
|
|
232
|
+
// Support both 'provider' (single) and 'providers' (array) patterns
|
|
233
|
+
const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
|
|
234
|
+
|
|
235
|
+
for (const providerConfig of providers) {
|
|
236
|
+
const capsulePath = providerConfig.capsule
|
|
237
|
+
|
|
238
|
+
// Build config object that matches expected structure
|
|
239
|
+
const config = {
|
|
240
|
+
...aliasConfig,
|
|
241
|
+
provider: providerConfig
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (capsulePath === 't44/caps/providers/vercel.com/ProjectDeployment') {
|
|
245
|
+
|
|
246
|
+
if (deprovision) {
|
|
247
|
+
// Check if project exists before attempting to deprovision
|
|
248
|
+
const status = await this.Vercel.status({ config })
|
|
249
|
+
|
|
250
|
+
if (status.error) {
|
|
251
|
+
console.log(`Project not found on provider. Skipping deprovisioning.`)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await this.Vercel.deprovision({ config })
|
|
256
|
+
} else {
|
|
257
|
+
await this.Vercel.deploy({
|
|
258
|
+
alias,
|
|
259
|
+
config,
|
|
260
|
+
projectionDir: join(
|
|
261
|
+
(await this.$WorkspaceConfig.config).rootDir,
|
|
262
|
+
'.~o/workspace.foundation/o/vercel.com'
|
|
263
|
+
)
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
} else if (capsulePath === 't44/caps/providers/bunny.net/ProjectDeployment') {
|
|
268
|
+
|
|
269
|
+
if (deprovision) {
|
|
270
|
+
await this.Bunny.deprovision({ config })
|
|
271
|
+
} else {
|
|
272
|
+
await this.Bunny.deploy({
|
|
273
|
+
alias,
|
|
274
|
+
config,
|
|
275
|
+
projectionDir: join(
|
|
276
|
+
(await this.$WorkspaceConfig.config).rootDir,
|
|
277
|
+
'.~o/workspace.foundation/o/bunny.net'
|
|
278
|
+
),
|
|
279
|
+
workspaceProjectName: projectName
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
} else if (capsulePath === 't44/caps/providers/dynadot.com/ProjectDeployment') {
|
|
284
|
+
|
|
285
|
+
if (deprovision) {
|
|
286
|
+
await this.Dynadot.deprovision({ config })
|
|
287
|
+
} else {
|
|
288
|
+
await this.Dynadot.deploy({
|
|
289
|
+
alias,
|
|
290
|
+
config,
|
|
291
|
+
projectionDir: join(
|
|
292
|
+
(await this.$WorkspaceConfig.config).rootDir,
|
|
293
|
+
'.~o/workspace.foundation/o/dynadot.com'
|
|
294
|
+
),
|
|
295
|
+
workspaceProjectName: projectName
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
} else {
|
|
300
|
+
throw new Error(`Unsupported capsule '${capsulePath}'!`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (deprovision) {
|
|
305
|
+
console.log(`\n<= Deprovisioning of provider project alias '${alias}' for workspace project '${projectName}' done.\n`)
|
|
306
|
+
} else {
|
|
307
|
+
console.log(`\n<= Deployment of provider project alias '${alias}' for workspace project '${projectName}' done.\n`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (deprovision) {
|
|
312
|
+
console.log(`\n<= Project '${projectName}' deprovisioning complete.\n`)
|
|
313
|
+
} else {
|
|
314
|
+
console.log(`\n<= Project '${projectName}' deployment complete.\n`)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}, {
|
|
322
|
+
importMeta: import.meta,
|
|
323
|
+
importStack: makeImportStack(),
|
|
324
|
+
capsuleName: capsule['#'],
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
capsule['#'] = 't44/caps/ProjectDeployment'
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
function orderAliasesByDependencies(deploymentConfig: Record<string, any>): string[] {
|
|
331
|
+
const aliases = Object.keys(deploymentConfig)
|
|
332
|
+
const ordered: string[] = []
|
|
333
|
+
const visited = new Set<string>()
|
|
334
|
+
const visiting = new Set<string>()
|
|
335
|
+
|
|
336
|
+
function visit(alias: string): void {
|
|
337
|
+
if (visited.has(alias)) return
|
|
338
|
+
|
|
339
|
+
if (visiting.has(alias)) {
|
|
340
|
+
throw new Error(`Circular dependency detected involving alias: ${alias}`)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
visiting.add(alias)
|
|
344
|
+
|
|
345
|
+
const depends = deploymentConfig[alias].depends || []
|
|
346
|
+
for (const dep of depends) {
|
|
347
|
+
if (!deploymentConfig[dep]) {
|
|
348
|
+
throw new Error(`Dependency '${dep}' not found for alias '${alias}'`)
|
|
349
|
+
}
|
|
350
|
+
visit(dep)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
visiting.delete(alias)
|
|
354
|
+
visited.add(alias)
|
|
355
|
+
ordered.push(alias)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const alias of aliases) {
|
|
359
|
+
visit(alias)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return ordered
|
|
363
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
|
|
2
|
+
import { join, resolve, relative } from 'path'
|
|
3
|
+
import { readdir, readFile, access } from 'fs/promises'
|
|
4
|
+
import { constants } from 'fs'
|
|
5
|
+
import { $ } from 'bun'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
export async function capsule({
|
|
9
|
+
encapsulate,
|
|
10
|
+
CapsulePropertyTypes,
|
|
11
|
+
makeImportStack
|
|
12
|
+
}: {
|
|
13
|
+
encapsulate: any
|
|
14
|
+
CapsulePropertyTypes: any
|
|
15
|
+
makeImportStack: any
|
|
16
|
+
}) {
|
|
17
|
+
return encapsulate({
|
|
18
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
19
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
20
|
+
'#': {
|
|
21
|
+
WorkspaceConfig: {
|
|
22
|
+
type: CapsulePropertyTypes.Mapping,
|
|
23
|
+
value: 't44/caps/WorkspaceConfig'
|
|
24
|
+
},
|
|
25
|
+
WorkspaceProjects: {
|
|
26
|
+
type: CapsulePropertyTypes.Mapping,
|
|
27
|
+
value: 't44/caps/WorkspaceProjects'
|
|
28
|
+
},
|
|
29
|
+
WorkspacePrompt: {
|
|
30
|
+
type: CapsulePropertyTypes.Mapping,
|
|
31
|
+
value: 't44/caps/WorkspacePrompt'
|
|
32
|
+
},
|
|
33
|
+
run: {
|
|
34
|
+
type: CapsulePropertyTypes.Function,
|
|
35
|
+
value: async function (this: any, { args }: any): Promise<void> {
|
|
36
|
+
|
|
37
|
+
const { projectSelector } = args
|
|
38
|
+
|
|
39
|
+
const projects = await this.WorkspaceProjects.list
|
|
40
|
+
|
|
41
|
+
// Discover all dev scripts across projects and their packages
|
|
42
|
+
const devTargets: Array<{
|
|
43
|
+
label: string
|
|
44
|
+
type: 'project' | 'package'
|
|
45
|
+
projectName: string
|
|
46
|
+
packageName?: string
|
|
47
|
+
dir: string
|
|
48
|
+
script: string
|
|
49
|
+
}> = []
|
|
50
|
+
|
|
51
|
+
for (const [projectName, projectInfo] of Object.entries(projects)) {
|
|
52
|
+
const sourceDir = (projectInfo as any).sourceDir
|
|
53
|
+
if (!sourceDir) continue
|
|
54
|
+
|
|
55
|
+
// Check project root for dev script
|
|
56
|
+
const projectPkgPath = join(sourceDir, 'package.json')
|
|
57
|
+
try {
|
|
58
|
+
await access(projectPkgPath, constants.F_OK)
|
|
59
|
+
const pkgContent = await readFile(projectPkgPath, 'utf-8')
|
|
60
|
+
const pkg = JSON.parse(pkgContent)
|
|
61
|
+
if (pkg.scripts?.dev) {
|
|
62
|
+
devTargets.push({
|
|
63
|
+
label: projectName,
|
|
64
|
+
type: 'project',
|
|
65
|
+
projectName,
|
|
66
|
+
dir: sourceDir,
|
|
67
|
+
script: pkg.scripts.dev
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
} catch { }
|
|
71
|
+
|
|
72
|
+
// Check packages/* for dev scripts
|
|
73
|
+
const packagesDir = join(sourceDir, 'packages')
|
|
74
|
+
try {
|
|
75
|
+
await access(packagesDir, constants.F_OK)
|
|
76
|
+
const packageEntries = await readdir(packagesDir, { withFileTypes: true })
|
|
77
|
+
for (const entry of packageEntries) {
|
|
78
|
+
if (!entry.isDirectory()) continue
|
|
79
|
+
const pkgDir = join(packagesDir, entry.name)
|
|
80
|
+
const pkgJsonPath = join(pkgDir, 'package.json')
|
|
81
|
+
try {
|
|
82
|
+
await access(pkgJsonPath, constants.F_OK)
|
|
83
|
+
const pkgContent = await readFile(pkgJsonPath, 'utf-8')
|
|
84
|
+
const pkg = JSON.parse(pkgContent)
|
|
85
|
+
if (pkg.scripts?.dev) {
|
|
86
|
+
devTargets.push({
|
|
87
|
+
label: `${projectName}/packages/${entry.name}`,
|
|
88
|
+
type: 'package',
|
|
89
|
+
projectName,
|
|
90
|
+
packageName: entry.name,
|
|
91
|
+
dir: pkgDir,
|
|
92
|
+
script: pkg.scripts.dev
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
} catch { }
|
|
96
|
+
}
|
|
97
|
+
} catch { }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (devTargets.length === 0) {
|
|
101
|
+
console.log(chalk.yellow('\nNo projects or packages with a "dev" script found.\n'))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sort alphabetically by label
|
|
106
|
+
devTargets.sort((a, b) => a.label.localeCompare(b.label))
|
|
107
|
+
|
|
108
|
+
let selectedTarget: typeof devTargets[0]
|
|
109
|
+
|
|
110
|
+
if (projectSelector) {
|
|
111
|
+
// Match by project name, package path, package name, or resolved path
|
|
112
|
+
const resolvedSelector = resolve(process.cwd(), projectSelector)
|
|
113
|
+
|
|
114
|
+
const matches = devTargets.filter(t => {
|
|
115
|
+
// Name-based matching
|
|
116
|
+
if (t.label === projectSelector ||
|
|
117
|
+
t.label.startsWith(projectSelector) ||
|
|
118
|
+
t.projectName === projectSelector ||
|
|
119
|
+
t.packageName === projectSelector) {
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
// Path-based matching: resolved selector matches or contains the target dir
|
|
123
|
+
const resolvedDir = resolve(t.dir)
|
|
124
|
+
if (resolvedDir === resolvedSelector || resolvedDir.startsWith(resolvedSelector + '/')) {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
return false
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (matches.length === 0) {
|
|
131
|
+
console.log(chalk.red(`\nNo dev script found matching '${projectSelector}'.\n`))
|
|
132
|
+
console.log(chalk.gray('Available targets:'))
|
|
133
|
+
for (const t of devTargets) {
|
|
134
|
+
const typeTag = t.type === 'project'
|
|
135
|
+
? chalk.cyan('[project]')
|
|
136
|
+
: chalk.magenta('[package]')
|
|
137
|
+
console.log(chalk.gray(` - ${t.label} ${typeTag}`))
|
|
138
|
+
}
|
|
139
|
+
console.log('')
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (matches.length > 1) {
|
|
144
|
+
// Prefer exact match over substring matches
|
|
145
|
+
const exactMatch = matches.find(t =>
|
|
146
|
+
t.label === projectSelector ||
|
|
147
|
+
t.projectName === projectSelector ||
|
|
148
|
+
t.packageName === projectSelector
|
|
149
|
+
)
|
|
150
|
+
if (exactMatch) {
|
|
151
|
+
matches.length = 0
|
|
152
|
+
matches.push(exactMatch)
|
|
153
|
+
} else {
|
|
154
|
+
console.log(chalk.red(`\nMultiple dev targets match '${projectSelector}':\n`))
|
|
155
|
+
for (const m of matches) {
|
|
156
|
+
console.log(chalk.gray(` - ${m.label}`))
|
|
157
|
+
}
|
|
158
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
selectedTarget = matches[0]
|
|
164
|
+
} else {
|
|
165
|
+
// Interactive picker
|
|
166
|
+
console.log(chalk.cyan('\nSelect a dev server to run:\n'))
|
|
167
|
+
|
|
168
|
+
const choices: Array<{ name: string; value: number }> = []
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < devTargets.length; i++) {
|
|
171
|
+
const t = devTargets[i]
|
|
172
|
+
const typeTag = t.type === 'project'
|
|
173
|
+
? chalk.cyan('[project]')
|
|
174
|
+
: chalk.magenta('[package]')
|
|
175
|
+
const scriptPreview = chalk.gray(t.script)
|
|
176
|
+
|
|
177
|
+
choices.push({
|
|
178
|
+
name: `${chalk.white(t.label)} ${typeTag} ${scriptPreview}`,
|
|
179
|
+
value: i
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const selectedIndex = await this.WorkspacePrompt.select({
|
|
185
|
+
message: 'Select dev target:',
|
|
186
|
+
choices,
|
|
187
|
+
pageSize: 20
|
|
188
|
+
})
|
|
189
|
+
selectedTarget = devTargets[selectedIndex]
|
|
190
|
+
} catch (error: any) {
|
|
191
|
+
if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
|
|
192
|
+
console.log(chalk.red('\nABORTED\n'))
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
throw error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Run the dev script interactively
|
|
200
|
+
const typeTag = selectedTarget.type === 'project'
|
|
201
|
+
? chalk.cyan('[project]')
|
|
202
|
+
: chalk.magenta('[package]')
|
|
203
|
+
|
|
204
|
+
console.log(chalk.green(`\n=> Starting dev server for ${selectedTarget.label} ${typeTag}\n`))
|
|
205
|
+
console.log(chalk.gray(` Directory: ${selectedTarget.dir}`))
|
|
206
|
+
console.log(chalk.gray(` Script: ${selectedTarget.script}\n`))
|
|
207
|
+
|
|
208
|
+
// Check if node_modules exists; if not, check for dependencies and run bun install
|
|
209
|
+
const nodeModulesDir = join(selectedTarget.dir, 'node_modules')
|
|
210
|
+
let needsInstall = false
|
|
211
|
+
try {
|
|
212
|
+
await access(nodeModulesDir, constants.F_OK)
|
|
213
|
+
} catch {
|
|
214
|
+
// node_modules missing — check if package.json has dependencies
|
|
215
|
+
const pkgPath = join(selectedTarget.dir, 'package.json')
|
|
216
|
+
try {
|
|
217
|
+
const pkgContent = await readFile(pkgPath, 'utf-8')
|
|
218
|
+
const pkg = JSON.parse(pkgContent)
|
|
219
|
+
if ((pkg.dependencies && Object.keys(pkg.dependencies).length > 0) ||
|
|
220
|
+
(pkg.devDependencies && Object.keys(pkg.devDependencies).length > 0)) {
|
|
221
|
+
needsInstall = true
|
|
222
|
+
}
|
|
223
|
+
} catch { }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (needsInstall) {
|
|
227
|
+
console.log(chalk.yellow(` Installing dependencies ...\n`))
|
|
228
|
+
const installProc = Bun.spawn(['bun', 'install'], {
|
|
229
|
+
cwd: selectedTarget.dir,
|
|
230
|
+
stdin: 'inherit',
|
|
231
|
+
stdout: 'inherit',
|
|
232
|
+
stderr: 'inherit'
|
|
233
|
+
})
|
|
234
|
+
await installProc.exited
|
|
235
|
+
console.log('')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Use Bun.spawn for full interactive mode (stdin/stdout/stderr passthrough)
|
|
239
|
+
const proc = Bun.spawn(['bun', 'run', 'dev'], {
|
|
240
|
+
cwd: selectedTarget.dir,
|
|
241
|
+
stdin: 'inherit',
|
|
242
|
+
stdout: 'inherit',
|
|
243
|
+
stderr: 'inherit'
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
await proc.exited
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}, {
|
|
252
|
+
importMeta: import.meta,
|
|
253
|
+
importStack: makeImportStack(),
|
|
254
|
+
capsuleName: capsule['#'],
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
capsule['#'] = 't44/caps/ProjectDevelopment'
|