@stream44.studio/t44 0.4.0-rc.24
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/.github/workflows/test.yaml +31 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +21 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
- package/.repo-identifier +1 -0
- package/DCO.md +34 -0
- package/LICENSE.txt +186 -0
- package/README.md +189 -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 +426 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +654 -0
- package/caps/ProjectPulling.ts +234 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +332 -0
- package/caps/ProjectTest.ts +251 -0
- package/caps/ProjectTestLib.ts +257 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/TaskWorkflow.ts +192 -0
- package/caps/WorkspaceCli.ts +448 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +87 -0
- package/caps/WorkspaceConfigFile.ts +902 -0
- package/caps/WorkspaceConnection.ts +329 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +619 -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 +430 -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/patterns/README.md +2 -0
- package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
- package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/examples/01-Lifecycle/main.test.ts +223 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +381 -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 +91 -0
- package/standalone-rt.test.ts +150 -0
- package/standalone-rt.ts +140 -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/ProjectPublishingConfig.ts +78 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectPullingConfig.ts +52 -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/WorkspaceShellConfig.ts +83 -0
- package/structs/patterns/README.md +2 -0
- package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/tsconfig.json +33 -0
- package/workspace-rt.ts +152 -0
- package/workspace.yaml +3 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { resolve, relative } from 'path'
|
|
2
|
+
import chalk from 'chalk'
|
|
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
|
+
'#@stream44.studio/t44/structs/ProjectDeploymentConfig': {
|
|
17
|
+
as: '$config'
|
|
18
|
+
},
|
|
19
|
+
'#@stream44.studio/t44/structs/WorkspaceConfig': {
|
|
20
|
+
as: '$Config'
|
|
21
|
+
},
|
|
22
|
+
'#@stream44.studio/t44/structs/ProjectPublishingConfig': {
|
|
23
|
+
as: '$WorkspaceRepositories'
|
|
24
|
+
},
|
|
25
|
+
'#': {
|
|
26
|
+
WorkspaceProjects: {
|
|
27
|
+
type: CapsulePropertyTypes.Mapping,
|
|
28
|
+
value: '@stream44.studio/t44/caps/WorkspaceProjects'
|
|
29
|
+
},
|
|
30
|
+
WorkspaceConfig: {
|
|
31
|
+
type: CapsulePropertyTypes.Mapping,
|
|
32
|
+
value: '@stream44.studio/t44/caps/WorkspaceConfig'
|
|
33
|
+
},
|
|
34
|
+
Vercel: {
|
|
35
|
+
type: CapsulePropertyTypes.Mapping,
|
|
36
|
+
value: '@stream44.studio/t44-vercel.com/caps/ProjectDeployment'
|
|
37
|
+
},
|
|
38
|
+
Bunny: {
|
|
39
|
+
type: CapsulePropertyTypes.Mapping,
|
|
40
|
+
value: '@stream44.studio/t44-bunny.net/caps/StaticWebsite/ProjectDeployment'
|
|
41
|
+
},
|
|
42
|
+
Dynadot: {
|
|
43
|
+
type: CapsulePropertyTypes.Mapping,
|
|
44
|
+
value: '@stream44.studio/t44-dynadot.com/caps/ProjectDeployment'
|
|
45
|
+
},
|
|
46
|
+
ProjectCatalogs: {
|
|
47
|
+
type: CapsulePropertyTypes.Mapping,
|
|
48
|
+
value: '@stream44.studio/t44/caps/ProjectCatalogs'
|
|
49
|
+
},
|
|
50
|
+
run: {
|
|
51
|
+
type: CapsulePropertyTypes.Function,
|
|
52
|
+
value: async function (this: any, { args }: any): Promise<void> {
|
|
53
|
+
|
|
54
|
+
const workspaceConfig = await this.$Config.config
|
|
55
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
56
|
+
const configTree = await this.WorkspaceConfig.configTree
|
|
57
|
+
await this.WorkspaceProjects.gatherGitInfo({ now: args?.now })
|
|
58
|
+
const workspaceProjects = await this.WorkspaceProjects.list
|
|
59
|
+
|
|
60
|
+
console.log('\n' + chalk.bold('═══════════════════════════════════════════════════════════════'))
|
|
61
|
+
console.log(chalk.bold.cyan(' WORKSPACE INFORMATION'))
|
|
62
|
+
console.log(chalk.bold('═══════════════════════════════════════════════════════════════\n'))
|
|
63
|
+
|
|
64
|
+
console.log(chalk.gray('Current Directory:'), chalk.white(process.cwd()))
|
|
65
|
+
console.log(chalk.gray(' Workspace Root:'), chalk.white(workspaceRootDir))
|
|
66
|
+
console.log(chalk.gray(' Workspace Name:'), chalk.white(workspaceConfig?.name || 'N/A'))
|
|
67
|
+
console.log(chalk.gray(' Workspace ID:'), chalk.white(workspaceConfig?.identifier || 'N/A') + '\n')
|
|
68
|
+
|
|
69
|
+
// Display config tree
|
|
70
|
+
console.log(chalk.bold.magenta('CONFIGURATION FILES'))
|
|
71
|
+
console.log(chalk.gray('───────────────────────────────────────────────────────────────\n'))
|
|
72
|
+
|
|
73
|
+
const printTree = (treeNode: any, prefix: string = '', isLast: boolean = true) => {
|
|
74
|
+
// Determine what to display
|
|
75
|
+
let displayPath: string
|
|
76
|
+
let formattedPath: string
|
|
77
|
+
|
|
78
|
+
if (treeNode.extendsValue) {
|
|
79
|
+
displayPath = treeNode.extendsValue
|
|
80
|
+
|
|
81
|
+
// Check if it's a relative path (starts with '.')
|
|
82
|
+
if (displayPath.startsWith('.')) {
|
|
83
|
+
formattedPath = chalk.white(displayPath)
|
|
84
|
+
} else {
|
|
85
|
+
// It's an npm package - highlight the package name
|
|
86
|
+
let packageName: string
|
|
87
|
+
let restOfPath: string
|
|
88
|
+
|
|
89
|
+
if (displayPath.startsWith('@')) {
|
|
90
|
+
// Scoped package: @org/name/rest
|
|
91
|
+
const match = displayPath.match(/^(@[^/]+\/[^/]+)(\/.*)?$/)
|
|
92
|
+
if (match) {
|
|
93
|
+
packageName = match[1]
|
|
94
|
+
restOfPath = match[2] || ''
|
|
95
|
+
} else {
|
|
96
|
+
packageName = displayPath
|
|
97
|
+
restOfPath = ''
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Unscoped package: name/rest
|
|
101
|
+
const match = displayPath.match(/^([^/]+)(\/.*)?$/)
|
|
102
|
+
if (match) {
|
|
103
|
+
packageName = match[1]
|
|
104
|
+
restOfPath = match[2] || ''
|
|
105
|
+
} else {
|
|
106
|
+
packageName = displayPath
|
|
107
|
+
restOfPath = ''
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
formattedPath = chalk.cyan(packageName) + chalk.white(restOfPath)
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
// Root config - show relative path
|
|
115
|
+
displayPath = relative(workspaceRootDir, treeNode.path)
|
|
116
|
+
formattedPath = chalk.white(displayPath)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Compute relative path from workspace root for clickable terminal link
|
|
120
|
+
const relFilePath = relative(workspaceRootDir, treeNode.path)
|
|
121
|
+
|
|
122
|
+
const connector = isLast ? '└── ' : '├── '
|
|
123
|
+
console.log(chalk.gray(prefix + connector) + formattedPath + chalk.gray(' - ' + relFilePath))
|
|
124
|
+
|
|
125
|
+
if (treeNode.extends && treeNode.extends.length > 0) {
|
|
126
|
+
const childPrefix = prefix + (isLast ? ' ' : '│ ')
|
|
127
|
+
treeNode.extends.forEach((child: any, index: number) => {
|
|
128
|
+
printTree(child, childPrefix, index === treeNode.extends.length - 1)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
printTree(configTree)
|
|
134
|
+
console.log('')
|
|
135
|
+
|
|
136
|
+
// Display Projects
|
|
137
|
+
const projectNames = Object.keys(workspaceProjects)
|
|
138
|
+
|
|
139
|
+
if (projectNames.length > 0) {
|
|
140
|
+
console.log(chalk.bold.yellow('PROJECTS'))
|
|
141
|
+
console.log(chalk.gray('───────────────────────────────────────────────────────────────\n'))
|
|
142
|
+
|
|
143
|
+
for (const projectName of projectNames) {
|
|
144
|
+
const project = workspaceProjects[projectName]
|
|
145
|
+
|
|
146
|
+
if (project.missing) {
|
|
147
|
+
console.log(chalk.bold.white(` ${projectName}`) + chalk.red(' ✗ directory does not exist: ') + chalk.gray(project.sourceDir))
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hasDeployments = Object.keys(project.deployments).length > 0
|
|
152
|
+
const hasRepositories = Object.keys(project.repositories).length > 0
|
|
153
|
+
|
|
154
|
+
const gitOrigin = project.git && typeof project.git === 'object' && project.git.remotes?.origin
|
|
155
|
+
? chalk.gray(' - ' + project.git.remotes.origin)
|
|
156
|
+
: ''
|
|
157
|
+
console.log(chalk.bold.white(` ${projectName}`) + gitOrigin)
|
|
158
|
+
if (args?.full && project.identifier?.did) {
|
|
159
|
+
console.log(chalk.gray(' did: ') + chalk.white(project.identifier.did))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const repoEntries = Object.entries(project.repositories)
|
|
163
|
+
const deploymentEntries = Object.entries(project.deployments)
|
|
164
|
+
|
|
165
|
+
// Pre-resolve all deployment statuses so we can count lines for tree connectors
|
|
166
|
+
const resolvedDeployments: { deploymentName: string, tree: any[], statusResults: Map<string, any> }[] = []
|
|
167
|
+
|
|
168
|
+
if (hasDeployments) {
|
|
169
|
+
for (const [deploymentName, projectAliases] of deploymentEntries) {
|
|
170
|
+
const tree = buildDependencyTree(projectAliases as Record<string, any>)
|
|
171
|
+
const statusPromises = new Map<string, Promise<any>>()
|
|
172
|
+
|
|
173
|
+
const collectStatusCalls = (node: any) => {
|
|
174
|
+
const aliasConfig = node.config
|
|
175
|
+
const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
|
|
176
|
+
|
|
177
|
+
if (providers.length > 0) {
|
|
178
|
+
const providerStatusPromises: Promise<any>[] = []
|
|
179
|
+
|
|
180
|
+
for (const providerConfig of providers) {
|
|
181
|
+
const capsulePath = providerConfig.capsule
|
|
182
|
+
const config = { ...aliasConfig, provider: providerConfig }
|
|
183
|
+
|
|
184
|
+
const passive = !args?.now && !args?.full
|
|
185
|
+
|
|
186
|
+
if (capsulePath === '@stream44.studio/t44-vercel.com/caps/ProjectDeployment') {
|
|
187
|
+
providerStatusPromises.push(this.Vercel.status({
|
|
188
|
+
config,
|
|
189
|
+
now: args?.now,
|
|
190
|
+
passive,
|
|
191
|
+
deploymentName
|
|
192
|
+
}).catch((error: any) => ({
|
|
193
|
+
projectName: deploymentName,
|
|
194
|
+
provider: 'vercel.com',
|
|
195
|
+
error: error.message,
|
|
196
|
+
rawDefinitionFilepaths: []
|
|
197
|
+
})))
|
|
198
|
+
} else if (capsulePath === '@stream44.studio/t44-bunny.net/caps/StaticWebsite/ProjectDeployment') {
|
|
199
|
+
providerStatusPromises.push(this.Bunny.status({
|
|
200
|
+
config,
|
|
201
|
+
now: args?.now,
|
|
202
|
+
passive,
|
|
203
|
+
deploymentName
|
|
204
|
+
}).catch((error: any) => ({
|
|
205
|
+
projectName: deploymentName,
|
|
206
|
+
provider: 'bunny.net',
|
|
207
|
+
error: error.message,
|
|
208
|
+
rawDefinitionFilepaths: []
|
|
209
|
+
})))
|
|
210
|
+
} else if (capsulePath === '@stream44.studio/t44-dynadot.com/caps/ProjectDeployment') {
|
|
211
|
+
providerStatusPromises.push(this.Dynadot.status({
|
|
212
|
+
config,
|
|
213
|
+
now: args?.now,
|
|
214
|
+
passive,
|
|
215
|
+
deploymentName
|
|
216
|
+
}).catch((error: any) => ({
|
|
217
|
+
projectName: deploymentName,
|
|
218
|
+
provider: 'dynadot.com',
|
|
219
|
+
error: error.message,
|
|
220
|
+
rawDefinitionFilepaths: []
|
|
221
|
+
})))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (providerStatusPromises.length > 0) {
|
|
226
|
+
statusPromises.set(node.alias, Promise.all(providerStatusPromises))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (node.children.length > 0) {
|
|
231
|
+
for (const child of node.children) {
|
|
232
|
+
collectStatusCalls(child)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const node of tree) {
|
|
238
|
+
collectStatusCalls(node)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const statusResults = new Map<string, any>()
|
|
242
|
+
const skippedProviders = new Set<string>()
|
|
243
|
+
await Promise.all(
|
|
244
|
+
Array.from(statusPromises.entries()).map(async ([alias, promise]) => {
|
|
245
|
+
try {
|
|
246
|
+
const result = await promise
|
|
247
|
+
// Filter out null results (passive mode, no cached data)
|
|
248
|
+
const filtered = Array.isArray(result) ? result.filter((r: any) => r !== null) : result
|
|
249
|
+
if (Array.isArray(filtered) && filtered.length > 0) {
|
|
250
|
+
statusResults.set(alias, filtered)
|
|
251
|
+
} else if (!Array.isArray(filtered) && filtered !== null) {
|
|
252
|
+
statusResults.set(alias, filtered)
|
|
253
|
+
}
|
|
254
|
+
} catch (error: any) {
|
|
255
|
+
// Check for MISSING_CREDENTIALS error
|
|
256
|
+
if (error?.message?.startsWith('MISSING_CREDENTIALS:')) {
|
|
257
|
+
const parts = error.message.slice('MISSING_CREDENTIALS:'.length).split(':')
|
|
258
|
+
const provider = parts[0] || 'unknown'
|
|
259
|
+
skippedProviders.add(provider)
|
|
260
|
+
// Set a skip marker for this alias
|
|
261
|
+
statusResults.set(alias, [{ skipped: true, provider, reason: 'credentials not configured' }])
|
|
262
|
+
} else {
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
// Log skipped providers once per deployment
|
|
270
|
+
if (skippedProviders.size > 0) {
|
|
271
|
+
for (const provider of skippedProviders) {
|
|
272
|
+
console.log(chalk.yellow(`\n ⚠️ Skipping ${provider} status check: credentials not configured`))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
resolvedDeployments.push({ deploymentName, tree, statusResults })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Count total lines for tree connectors
|
|
281
|
+
let totalItems: number
|
|
282
|
+
if (args?.full) {
|
|
283
|
+
totalItems = repoEntries.length + resolvedDeployments.length
|
|
284
|
+
} else {
|
|
285
|
+
// In compact mode, each provider status is one line;
|
|
286
|
+
// aliases with no cached status still get one line
|
|
287
|
+
let compactDeploymentLines = 0
|
|
288
|
+
for (const { tree, statusResults } of resolvedDeployments) {
|
|
289
|
+
const countLines = (nodes: any[]): number => {
|
|
290
|
+
let count = 0
|
|
291
|
+
for (const node of nodes) {
|
|
292
|
+
const statuses = statusResults.get(node.alias) || []
|
|
293
|
+
count += statuses.length > 0 ? statuses.length : 1
|
|
294
|
+
if (node.children.length > 0) count += countLines(node.children)
|
|
295
|
+
}
|
|
296
|
+
return count
|
|
297
|
+
}
|
|
298
|
+
compactDeploymentLines += countLines(tree)
|
|
299
|
+
}
|
|
300
|
+
totalItems = repoEntries.length + compactDeploymentLines
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let itemIndex = 0
|
|
304
|
+
|
|
305
|
+
// Build reverse lookup: repoName -> catalog names
|
|
306
|
+
const catalogList = await this.ProjectCatalogs.list
|
|
307
|
+
const repoCatalogMap: Record<string, string[]> = {}
|
|
308
|
+
if (catalogList && typeof catalogList === 'object') {
|
|
309
|
+
for (const [catalogName, catalogConfig] of Object.entries(catalogList)) {
|
|
310
|
+
const repos = (catalogConfig as any)?.repositories
|
|
311
|
+
if (repos && typeof repos === 'object') {
|
|
312
|
+
for (const repoKey of Object.keys(repos)) {
|
|
313
|
+
if (!repoCatalogMap[repoKey]) repoCatalogMap[repoKey] = []
|
|
314
|
+
repoCatalogMap[repoKey].push(catalogName)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Display repositories for this project (first)
|
|
321
|
+
if (hasRepositories) {
|
|
322
|
+
for (const [repoName, repoConfig] of repoEntries) {
|
|
323
|
+
const typedConfig = repoConfig as any
|
|
324
|
+
const sourceDirPath = typedConfig.sourceDir ? resolve(typedConfig.sourceDir) : 'N/A'
|
|
325
|
+
let relPath = typedConfig.sourceDir ? relative(workspaceRootDir, sourceDirPath) : '.'
|
|
326
|
+
if (relPath === '') relPath = '.'
|
|
327
|
+
|
|
328
|
+
const providers = Array.isArray(typedConfig.providers)
|
|
329
|
+
? typedConfig.providers
|
|
330
|
+
: typedConfig.provider
|
|
331
|
+
? [typedConfig.provider]
|
|
332
|
+
: []
|
|
333
|
+
|
|
334
|
+
const vendors = providers
|
|
335
|
+
.map((p: any) => {
|
|
336
|
+
const capsule = p.capsule || ''
|
|
337
|
+
const vendorMatch = capsule.match(/\/caps\/patterns\/([^\/]+)\//)
|
|
338
|
+
return vendorMatch ? vendorMatch[1] : 'unknown'
|
|
339
|
+
})
|
|
340
|
+
.filter((v: string, i: number, arr: string[]) => arr.indexOf(v) === i)
|
|
341
|
+
|
|
342
|
+
const vendor = vendors.length > 0 ? vendors.join(' & ') : 'unknown'
|
|
343
|
+
|
|
344
|
+
const gitProvider = providers.find((p: any) =>
|
|
345
|
+
p.capsule && p.capsule.includes('git-scm.com')
|
|
346
|
+
)
|
|
347
|
+
const origin = gitProvider?.config?.RepositorySettings?.origin || 'N/A'
|
|
348
|
+
|
|
349
|
+
itemIndex++
|
|
350
|
+
const connector = itemIndex === totalItems ? '└── ' : '├── '
|
|
351
|
+
console.log(chalk.gray(' ' + connector) + chalk.blue('repository: ') + chalk.white(relPath) + chalk.gray(' | ') +
|
|
352
|
+
chalk.cyan(repoName) + chalk.gray(' → ') +
|
|
353
|
+
chalk.green(vendor) + chalk.gray(' | ') +
|
|
354
|
+
chalk.yellow(origin))
|
|
355
|
+
|
|
356
|
+
const catalogs = repoCatalogMap[repoName]
|
|
357
|
+
if (catalogs && catalogs.length > 0) {
|
|
358
|
+
const continueLine = itemIndex === totalItems ? ' ' : '│ '
|
|
359
|
+
for (let ci = 0; ci < catalogs.length; ci++) {
|
|
360
|
+
const catConnector = ci === catalogs.length - 1 ? '└── ' : '├── '
|
|
361
|
+
console.log(chalk.gray(' ' + continueLine + catConnector) + chalk.gray('catalog: ') + chalk.yellow(catalogs[ci]))
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Display deployments for this project (after repositories)
|
|
368
|
+
for (const { deploymentName, tree, statusResults } of resolvedDeployments) {
|
|
369
|
+
if (args?.full) {
|
|
370
|
+
// Full mode: verbose multi-line display
|
|
371
|
+
itemIndex++
|
|
372
|
+
const isLastItem = itemIndex === totalItems
|
|
373
|
+
const connector = isLastItem ? '└── ' : '├── '
|
|
374
|
+
const continueLine = isLastItem ? ' ' : '│ '
|
|
375
|
+
|
|
376
|
+
console.log(chalk.gray(' ' + connector) + chalk.yellow('deployment: ') + chalk.white(deploymentName))
|
|
377
|
+
|
|
378
|
+
const printNode = (node: any, indent: string, isLast: boolean) => {
|
|
379
|
+
const aliasConfig = node.config
|
|
380
|
+
const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
|
|
381
|
+
|
|
382
|
+
if (providers.length === 0) {
|
|
383
|
+
const nodeConnector = isLast ? '└── ' : '├── '
|
|
384
|
+
console.log(chalk.gray(indent + nodeConnector) + chalk.cyan(node.alias) + chalk.gray(': ') + chalk.red('No provider capsule configured'))
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const sourceDirPath = aliasConfig.sourceDir ? resolve(aliasConfig.sourceDir) : 'N/A'
|
|
389
|
+
const relPath = aliasConfig.sourceDir ? relative(workspaceRootDir, sourceDirPath) : 'N/A'
|
|
390
|
+
|
|
391
|
+
const nodeConnector = isLast ? '└── ' : '├── '
|
|
392
|
+
console.log(chalk.gray(indent + nodeConnector) + chalk.cyan(node.alias) + chalk.gray(' (') + chalk.white(relPath) + chalk.gray(')'))
|
|
393
|
+
|
|
394
|
+
const detailIndent = indent + (isLast ? ' ' : '│ ')
|
|
395
|
+
const statusArray = statusResults.get(node.alias) || []
|
|
396
|
+
|
|
397
|
+
if (statusArray.length === 0) {
|
|
398
|
+
console.log(chalk.gray(`${detailIndent}Status: `) + chalk.gray('not deployed'))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const printStatus = (status: any) => {
|
|
402
|
+
if (!status) {
|
|
403
|
+
console.log(chalk.yellow(`${detailIndent}Status method not available for this provider`))
|
|
404
|
+
} else if (status.skipped) {
|
|
405
|
+
// Credentials not configured - already logged at top level
|
|
406
|
+
return
|
|
407
|
+
} else if (status.error) {
|
|
408
|
+
console.log(chalk.gray(`${detailIndent}Project: `) + chalk.magenta(status.projectName || 'N/A') + chalk.gray(' → ') + chalk.green(status.provider || 'unknown'))
|
|
409
|
+
console.log(chalk.red(`${detailIndent}Error: ${status.error}`))
|
|
410
|
+
const isNotFoundError = status.error.toLowerCase().includes('not found')
|
|
411
|
+
if (!isNotFoundError && status.rawDefinitionFilepaths && status.rawDefinitionFilepaths.length > 0) {
|
|
412
|
+
status.rawDefinitionFilepaths.forEach((filepath: string) => {
|
|
413
|
+
console.log(chalk.gray(`${detailIndent}Fact: `) + chalk.white(filepath))
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
console.log(chalk.gray(`${detailIndent}Project: `) + chalk.magenta(status.projectName || 'N/A') + chalk.gray(' → ') + chalk.green(status.provider || 'unknown'))
|
|
418
|
+
|
|
419
|
+
const statusColor = status.status === 'READY' ? chalk.green :
|
|
420
|
+
status.status === 'ERROR' ? chalk.red :
|
|
421
|
+
status.status === 'DISABLED' ? chalk.red :
|
|
422
|
+
chalk.yellow
|
|
423
|
+
console.log(chalk.gray(`${detailIndent}Status: `) + statusColor(status.status || 'UNKNOWN'))
|
|
424
|
+
|
|
425
|
+
if (status.publicUrl) {
|
|
426
|
+
console.log(chalk.gray(`${detailIndent}URL: `) + chalk.blue(status.publicUrl))
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (status.createdAt) {
|
|
430
|
+
const date = new Date(status.createdAt)
|
|
431
|
+
const elapsed = formatElapsedTime(status.createdAt)
|
|
432
|
+
console.log(chalk.gray(`${detailIndent}Created: `) + chalk.white(date.toLocaleString()) + chalk.gray(` (${elapsed})`))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (status.updatedAt) {
|
|
436
|
+
const date = new Date(status.updatedAt)
|
|
437
|
+
const elapsed = formatElapsedTime(status.updatedAt)
|
|
438
|
+
console.log(chalk.gray(`${detailIndent}Updated: `) + chalk.white(date.toLocaleString()) + chalk.gray(` (${elapsed})`))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (status.usage) {
|
|
442
|
+
if (status.usage.storageBytes !== undefined) {
|
|
443
|
+
const storageMB = (status.usage.storageBytes / (1024 * 1024)).toFixed(2)
|
|
444
|
+
console.log(chalk.gray(`${detailIndent}Storage: `) + chalk.white(`${storageMB} MB`) + chalk.gray(` (${status.usage.filesCount || 0} files)`))
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (status.usage.bandwidthBytes !== undefined) {
|
|
448
|
+
const bandwidthGB = (status.usage.bandwidthBytes / (1024 * 1024 * 1024)).toFixed(2)
|
|
449
|
+
console.log(chalk.gray(`${detailIndent}Bandwidth: `) + chalk.white(`${bandwidthGB} GB this month`))
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (status.usage.charges !== undefined) {
|
|
453
|
+
console.log(chalk.gray(`${detailIndent}Charges: `) + chalk.white(`$${status.usage.charges.toFixed(2)} this month`))
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (status.providerPortalUrl) {
|
|
458
|
+
console.log(chalk.gray(`${detailIndent}Portal: `) + chalk.blue(status.providerPortalUrl))
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (status.rawDefinitionFilepaths && status.rawDefinitionFilepaths.length > 0) {
|
|
462
|
+
status.rawDefinitionFilepaths.forEach((filepath: string) => {
|
|
463
|
+
console.log(chalk.gray(`${detailIndent}Fact: `) + chalk.white(filepath))
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for (const status of statusArray) {
|
|
470
|
+
printStatus(status)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (node.children.length > 0) {
|
|
474
|
+
const childIndent = indent + (isLast ? ' ' : '│ ')
|
|
475
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
476
|
+
printNode(node.children[i], childIndent, i === node.children.length - 1)
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
tree.forEach((node, index) => {
|
|
482
|
+
printNode(node, ' ' + continueLine, index === tree.length - 1)
|
|
483
|
+
})
|
|
484
|
+
} else {
|
|
485
|
+
// Compact mode: one line per provider status;
|
|
486
|
+
// aliases with no cached status get a single 'not deployed' line
|
|
487
|
+
const allLines: { status: any, alias: string }[] = []
|
|
488
|
+
const collectAllLines = (nodes: any[]) => {
|
|
489
|
+
for (const node of nodes) {
|
|
490
|
+
const statusArray = statusResults.get(node.alias) || []
|
|
491
|
+
if (statusArray.length > 0) {
|
|
492
|
+
for (const status of statusArray) {
|
|
493
|
+
allLines.push({ status, alias: node.alias })
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
allLines.push({ status: null, alias: node.alias })
|
|
497
|
+
}
|
|
498
|
+
if (node.children.length > 0) {
|
|
499
|
+
collectAllLines(node.children)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
collectAllLines(tree)
|
|
504
|
+
|
|
505
|
+
for (const { status, alias } of allLines) {
|
|
506
|
+
itemIndex++
|
|
507
|
+
const isLastItem = itemIndex === totalItems
|
|
508
|
+
const connector = isLastItem ? '└── ' : '├── '
|
|
509
|
+
|
|
510
|
+
if (status === null) {
|
|
511
|
+
// No cached data — extract provider name from config
|
|
512
|
+
const aliasConfig = (tree.find((n: any) => n.alias === alias) || { config: {} }).config
|
|
513
|
+
const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
|
|
514
|
+
const vendorNames = providers.map((p: any) => {
|
|
515
|
+
const match = (p.capsule || '').match(/\/caps\/patterns\/([^\/]+)\//)
|
|
516
|
+
return match ? match[1] : 'unknown'
|
|
517
|
+
}).join(' & ')
|
|
518
|
+
console.log(chalk.gray(' ' + connector +
|
|
519
|
+
alias + ' → ' +
|
|
520
|
+
vendorNames + ' [not deployed]'))
|
|
521
|
+
} else if (status.error) {
|
|
522
|
+
const providerName = status?.provider || 'unknown'
|
|
523
|
+
const projName = status?.projectName || 'unknown'
|
|
524
|
+
console.log(chalk.gray(' ' + connector +
|
|
525
|
+
deploymentName + ' → ' +
|
|
526
|
+
providerName + ' [' + projName + ']'))
|
|
527
|
+
} else {
|
|
528
|
+
const updatedAgo = status.updatedAt ? formatElapsedTime(status.updatedAt) : null
|
|
529
|
+
const nameWithAge = chalk.yellow(deploymentName) +
|
|
530
|
+
(updatedAgo ? chalk.gray(' (') + chalk.magenta(updatedAgo) + chalk.gray(')') : '')
|
|
531
|
+
|
|
532
|
+
const parts = [
|
|
533
|
+
nameWithAge + chalk.gray(' → ') +
|
|
534
|
+
chalk.green(status.provider) + chalk.gray(' [') + chalk.magenta(status.projectName) + chalk.gray(']')
|
|
535
|
+
]
|
|
536
|
+
if (status.publicUrl) {
|
|
537
|
+
parts.push(chalk.blue(status.publicUrl))
|
|
538
|
+
}
|
|
539
|
+
if (status.providerPortalUrl) {
|
|
540
|
+
parts.push(chalk.gray('portal: ') + chalk.blue(status.providerPortalUrl))
|
|
541
|
+
}
|
|
542
|
+
console.log(chalk.gray(' ' + connector) + parts.join(chalk.gray(' | ')))
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
console.log(chalk.bold.yellow('PROJECTS:'), chalk.gray('None configured\n'))
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log(chalk.bold('═══════════════════════════════════════════════════════════════\n'))
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}, {
|
|
558
|
+
importMeta: import.meta,
|
|
559
|
+
importStack: makeImportStack(),
|
|
560
|
+
capsuleName: capsule['#'],
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
capsule['#'] = '@stream44.studio/t44/caps/WorkspaceInfo'
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
function buildDependencyTree(projectAliases: Record<string, any>): { alias: string, children: any[], config: any }[] {
|
|
568
|
+
const aliasMap = new Map<string, { alias: string, children: any[], config: any }>()
|
|
569
|
+
const roots: any[] = []
|
|
570
|
+
|
|
571
|
+
// Create nodes for all aliases
|
|
572
|
+
for (const [alias, config] of Object.entries(projectAliases)) {
|
|
573
|
+
aliasMap.set(alias, { alias, children: [], config })
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Build parent-child relationships
|
|
577
|
+
for (const [alias, config] of Object.entries(projectAliases)) {
|
|
578
|
+
const node = aliasMap.get(alias)!
|
|
579
|
+
const depends = config.depends || []
|
|
580
|
+
|
|
581
|
+
if (depends.length === 0) {
|
|
582
|
+
// No dependencies, this is a root
|
|
583
|
+
roots.push(node)
|
|
584
|
+
} else {
|
|
585
|
+
// Add this node as a child to all its dependencies
|
|
586
|
+
for (const dep of depends) {
|
|
587
|
+
const parent = aliasMap.get(dep)
|
|
588
|
+
if (parent) {
|
|
589
|
+
parent.children.push(node)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return roots
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function formatElapsedTime(timestamp: string | number): string {
|
|
599
|
+
const now = Date.now()
|
|
600
|
+
const ts = typeof timestamp === 'string' ? new Date(timestamp).getTime() : timestamp
|
|
601
|
+
const elapsed = now - ts
|
|
602
|
+
|
|
603
|
+
const days = Math.floor(elapsed / (1000 * 60 * 60 * 24))
|
|
604
|
+
const hours = Math.floor(elapsed / (1000 * 60 * 60))
|
|
605
|
+
const minutes = Math.floor(elapsed / (1000 * 60))
|
|
606
|
+
const seconds = Math.floor(elapsed / 1000)
|
|
607
|
+
|
|
608
|
+
if (days > 0) {
|
|
609
|
+
return `${days}d`
|
|
610
|
+
} else if (hours > 0) {
|
|
611
|
+
return `${hours}h`
|
|
612
|
+
} else if (minutes > 0) {
|
|
613
|
+
return `${minutes}m`
|
|
614
|
+
} else if (seconds > 0) {
|
|
615
|
+
return `${seconds}s`
|
|
616
|
+
} else {
|
|
617
|
+
return 'now'
|
|
618
|
+
}
|
|
619
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export async function capsule({
|
|
4
|
+
encapsulate,
|
|
5
|
+
CapsulePropertyTypes,
|
|
6
|
+
makeImportStack
|
|
7
|
+
}: {
|
|
8
|
+
encapsulate: any
|
|
9
|
+
CapsulePropertyTypes: any
|
|
10
|
+
makeImportStack: any
|
|
11
|
+
}) {
|
|
12
|
+
return encapsulate({
|
|
13
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
14
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
15
|
+
'#': {
|
|
16
|
+
run: {
|
|
17
|
+
type: CapsulePropertyTypes.Function,
|
|
18
|
+
value: async function (this: any, { args }: any): Promise<void> {
|
|
19
|
+
console.log(chalk.green('You have successfully initialized a Terminal 44 Workspace!'))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}, {
|
|
25
|
+
importMeta: import.meta,
|
|
26
|
+
importStack: makeImportStack(),
|
|
27
|
+
capsuleName: capsule['#'],
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
capsule['#'] = '@stream44.studio/t44/caps/WorkspaceInit'
|