t44 0.4.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yml +12 -0
  3. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  4. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity.yaml +25 -0
  6. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  7. package/DCO.md +34 -0
  8. package/LICENSE.md +203 -0
  9. package/README.md +183 -0
  10. package/bin/activate +36 -0
  11. package/bin/activate.ts +30 -0
  12. package/bin/postinstall.sh +19 -0
  13. package/bin/shell +27 -0
  14. package/bin/t44 +27 -0
  15. package/caps/ConfigSchemaStruct.ts +55 -0
  16. package/caps/Home.ts +51 -0
  17. package/caps/HomeRegistry.ts +313 -0
  18. package/caps/HomeRegistryFile.ts +144 -0
  19. package/caps/JsonSchemas.ts +220 -0
  20. package/caps/OpenApiSchema.ts +67 -0
  21. package/caps/PackageDescriptor.ts +88 -0
  22. package/caps/ProjectCatalogs.ts +153 -0
  23. package/caps/ProjectDeployment.ts +363 -0
  24. package/caps/ProjectDevelopment.ts +257 -0
  25. package/caps/ProjectPublishing.ts +522 -0
  26. package/caps/ProjectRack.ts +155 -0
  27. package/caps/ProjectRepository.ts +322 -0
  28. package/caps/RootKey.ts +219 -0
  29. package/caps/SigningKey.ts +243 -0
  30. package/caps/WorkspaceCli.ts +442 -0
  31. package/caps/WorkspaceConfig.ts +268 -0
  32. package/caps/WorkspaceConfig.yaml +71 -0
  33. package/caps/WorkspaceConfigFile.ts +799 -0
  34. package/caps/WorkspaceConnection.ts +249 -0
  35. package/caps/WorkspaceEntityConfig.ts +78 -0
  36. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  37. package/caps/WorkspaceEntityFact.ts +218 -0
  38. package/caps/WorkspaceInfo.ts +595 -0
  39. package/caps/WorkspaceInit.ts +30 -0
  40. package/caps/WorkspaceKey.ts +338 -0
  41. package/caps/WorkspaceModel.ts +373 -0
  42. package/caps/WorkspaceProjects.ts +636 -0
  43. package/caps/WorkspacePrompt.ts +406 -0
  44. package/caps/WorkspaceShell.sh +39 -0
  45. package/caps/WorkspaceShell.ts +104 -0
  46. package/caps/WorkspaceShell.yaml +64 -0
  47. package/caps/WorkspaceShellCli.ts +109 -0
  48. package/caps/WorkspaceTest.ts +167 -0
  49. package/caps/providers/README.md +2 -0
  50. package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
  51. package/caps/providers/bunny.net/api-pull.test.ts +319 -0
  52. package/caps/providers/bunny.net/api-pull.ts +164 -0
  53. package/caps/providers/bunny.net/api-storage.test.ts +168 -0
  54. package/caps/providers/bunny.net/api-storage.ts +248 -0
  55. package/caps/providers/bunny.net/api.ts +95 -0
  56. package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
  57. package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
  58. package/caps/providers/dynadot.com/api-domains.ts +169 -0
  59. package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
  60. package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
  61. package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
  62. package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
  63. package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
  64. package/caps/providers/github.com/ProjectPublishing.ts +118 -0
  65. package/caps/providers/github.com/api.ts +115 -0
  66. package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
  67. package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
  68. package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
  69. package/caps/providers/vercel.com/api.test.ts +67 -0
  70. package/caps/providers/vercel.com/api.ts +132 -0
  71. package/caps/providers/vercel.com/bun.lock +194 -0
  72. package/caps/providers/vercel.com/package.json +10 -0
  73. package/caps/providers/vercel.com/project.test.ts +108 -0
  74. package/caps/providers/vercel.com/project.ts +150 -0
  75. package/caps/providers/vercel.com/tsconfig.json +28 -0
  76. package/docs/Overview.drawio +248 -0
  77. package/docs/Overview.svg +4 -0
  78. package/lib/crypto.ts +53 -0
  79. package/lib/key.ts +365 -0
  80. package/lib/schema-console-renderer.ts +181 -0
  81. package/lib/schema-resolver.ts +349 -0
  82. package/lib/ucan.ts +137 -0
  83. package/package.json +101 -0
  84. package/structs/HomeRegistry.ts +55 -0
  85. package/structs/HomeRegistryConfig.ts +56 -0
  86. package/structs/ProjectCatalogsConfig.ts +53 -0
  87. package/structs/ProjectDeploymentConfig.ts +56 -0
  88. package/structs/ProjectDeploymentFact.ts +106 -0
  89. package/structs/ProjectPublishingFact.ts +68 -0
  90. package/structs/ProjectRack.ts +51 -0
  91. package/structs/ProjectRackConfig.ts +56 -0
  92. package/structs/RepositoryOriginDescriptor.ts +51 -0
  93. package/structs/RootKeyConfig.ts +64 -0
  94. package/structs/SigningKeyConfig.ts +64 -0
  95. package/structs/Workspace.ts +56 -0
  96. package/structs/WorkspaceCatalogs.ts +56 -0
  97. package/structs/WorkspaceCliConfig.ts +53 -0
  98. package/structs/WorkspaceConfig.ts +64 -0
  99. package/structs/WorkspaceConfigFile.ts +50 -0
  100. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  101. package/structs/WorkspaceKey.ts +55 -0
  102. package/structs/WorkspaceKeyConfig.ts +56 -0
  103. package/structs/WorkspaceMappingsConfig.ts +56 -0
  104. package/structs/WorkspaceProject.ts +104 -0
  105. package/structs/WorkspaceProjectsConfig.ts +67 -0
  106. package/structs/WorkspacePublishingConfig.ts +65 -0
  107. package/structs/WorkspaceShellConfig.ts +83 -0
  108. package/structs/providers/README.md +2 -0
  109. package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
  110. package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
  111. package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
  112. package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
  113. package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
  114. package/structs/providers/dynadot.com/DomainFact.ts +46 -0
  115. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
  116. package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
  117. package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
  118. package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
  119. package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
  120. package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
  121. package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
  122. package/tests/01-Lifecycle/main.test.ts +173 -0
  123. package/tsconfig.json +28 -0
  124. package/workspace-rt.ts +134 -0
  125. package/workspace.yaml +3 -0
@@ -0,0 +1,595 @@
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
+ '#t44/structs/ProjectDeploymentConfig': {
17
+ as: '$config'
18
+ },
19
+ '#t44/structs/WorkspaceConfig': {
20
+ as: '$Config'
21
+ },
22
+ '#t44/structs/WorkspacePublishingConfig': {
23
+ as: '$WorkspaceRepositories'
24
+ },
25
+ '#': {
26
+ WorkspaceProjects: {
27
+ type: CapsulePropertyTypes.Mapping,
28
+ value: 't44/caps/WorkspaceProjects'
29
+ },
30
+ WorkspaceConfig: {
31
+ type: CapsulePropertyTypes.Mapping,
32
+ value: 't44/caps/WorkspaceConfig'
33
+ },
34
+ Vercel: {
35
+ type: CapsulePropertyTypes.Mapping,
36
+ value: 't44/caps/providers/vercel.com/ProjectDeployment'
37
+ },
38
+ Bunny: {
39
+ type: CapsulePropertyTypes.Mapping,
40
+ value: 't44/caps/providers/bunny.net/ProjectDeployment'
41
+ },
42
+ Dynadot: {
43
+ type: CapsulePropertyTypes.Mapping,
44
+ value: 't44/caps/providers/dynadot.com/ProjectDeployment'
45
+ },
46
+ ProjectCatalogs: {
47
+ type: CapsulePropertyTypes.Mapping,
48
+ value: '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 === 't44/caps/providers/vercel.com/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 === 't44/caps/providers/bunny.net/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 === 't44/caps/providers/dynadot.com/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
+ await Promise.all(
243
+ Array.from(statusPromises.entries()).map(async ([alias, promise]) => {
244
+ const result = await promise
245
+ // Filter out null results (passive mode, no cached data)
246
+ const filtered = Array.isArray(result) ? result.filter((r: any) => r !== null) : result
247
+ if (Array.isArray(filtered) && filtered.length > 0) {
248
+ statusResults.set(alias, filtered)
249
+ } else if (!Array.isArray(filtered) && filtered !== null) {
250
+ statusResults.set(alias, filtered)
251
+ }
252
+ })
253
+ )
254
+
255
+ resolvedDeployments.push({ deploymentName, tree, statusResults })
256
+ }
257
+ }
258
+
259
+ // Count total lines for tree connectors
260
+ let totalItems: number
261
+ if (args?.full) {
262
+ totalItems = repoEntries.length + resolvedDeployments.length
263
+ } else {
264
+ // In compact mode, each provider status is one line;
265
+ // aliases with no cached status still get one line
266
+ let compactDeploymentLines = 0
267
+ for (const { tree, statusResults } of resolvedDeployments) {
268
+ const countLines = (nodes: any[]): number => {
269
+ let count = 0
270
+ for (const node of nodes) {
271
+ const statuses = statusResults.get(node.alias) || []
272
+ count += statuses.length > 0 ? statuses.length : 1
273
+ if (node.children.length > 0) count += countLines(node.children)
274
+ }
275
+ return count
276
+ }
277
+ compactDeploymentLines += countLines(tree)
278
+ }
279
+ totalItems = repoEntries.length + compactDeploymentLines
280
+ }
281
+
282
+ let itemIndex = 0
283
+
284
+ // Build reverse lookup: repoName -> catalog names
285
+ const catalogList = await this.ProjectCatalogs.list
286
+ const repoCatalogMap: Record<string, string[]> = {}
287
+ if (catalogList && typeof catalogList === 'object') {
288
+ for (const [catalogName, catalogConfig] of Object.entries(catalogList)) {
289
+ const repos = (catalogConfig as any)?.repositories
290
+ if (repos && typeof repos === 'object') {
291
+ for (const repoKey of Object.keys(repos)) {
292
+ if (!repoCatalogMap[repoKey]) repoCatalogMap[repoKey] = []
293
+ repoCatalogMap[repoKey].push(catalogName)
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // Display repositories for this project (first)
300
+ if (hasRepositories) {
301
+ for (const [repoName, repoConfig] of repoEntries) {
302
+ const typedConfig = repoConfig as any
303
+ const sourceDirPath = typedConfig.sourceDir ? resolve(typedConfig.sourceDir) : 'N/A'
304
+ let relPath = typedConfig.sourceDir ? relative(workspaceRootDir, sourceDirPath) : '.'
305
+ if (relPath === '') relPath = '.'
306
+
307
+ const providers = Array.isArray(typedConfig.providers)
308
+ ? typedConfig.providers
309
+ : typedConfig.provider
310
+ ? [typedConfig.provider]
311
+ : []
312
+
313
+ const vendors = providers
314
+ .map((p: any) => {
315
+ const capsule = p.capsule || ''
316
+ const vendorMatch = capsule.match(/\/caps\/providers\/([^\/]+)\//)
317
+ return vendorMatch ? vendorMatch[1] : 'unknown'
318
+ })
319
+ .filter((v: string, i: number, arr: string[]) => arr.indexOf(v) === i)
320
+
321
+ const vendor = vendors.length > 0 ? vendors.join(' & ') : 'unknown'
322
+
323
+ const gitProvider = providers.find((p: any) =>
324
+ p.capsule && p.capsule.includes('git-scm.com')
325
+ )
326
+ const origin = gitProvider?.config?.RepositorySettings?.origin || 'N/A'
327
+
328
+ itemIndex++
329
+ const connector = itemIndex === totalItems ? '└── ' : '├── '
330
+ console.log(chalk.gray(' ' + connector) + chalk.blue('repository: ') + chalk.white(relPath) + chalk.gray(' | ') +
331
+ chalk.cyan(repoName) + chalk.gray(' → ') +
332
+ chalk.green(vendor) + chalk.gray(' | ') +
333
+ chalk.yellow(origin))
334
+
335
+ const catalogs = repoCatalogMap[repoName]
336
+ if (catalogs && catalogs.length > 0) {
337
+ const continueLine = itemIndex === totalItems ? ' ' : '│ '
338
+ for (let ci = 0; ci < catalogs.length; ci++) {
339
+ const catConnector = ci === catalogs.length - 1 ? '└── ' : '├── '
340
+ console.log(chalk.gray(' ' + continueLine + catConnector) + chalk.gray('catalog: ') + chalk.yellow(catalogs[ci]))
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ // Display deployments for this project (after repositories)
347
+ for (const { deploymentName, tree, statusResults } of resolvedDeployments) {
348
+ if (args?.full) {
349
+ // Full mode: verbose multi-line display
350
+ itemIndex++
351
+ const isLastItem = itemIndex === totalItems
352
+ const connector = isLastItem ? '└── ' : '├── '
353
+ const continueLine = isLastItem ? ' ' : '│ '
354
+
355
+ console.log(chalk.gray(' ' + connector) + chalk.yellow('deployment: ') + chalk.white(deploymentName))
356
+
357
+ const printNode = (node: any, indent: string, isLast: boolean) => {
358
+ const aliasConfig = node.config
359
+ const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
360
+
361
+ if (providers.length === 0) {
362
+ const nodeConnector = isLast ? '└── ' : '├── '
363
+ console.log(chalk.gray(indent + nodeConnector) + chalk.cyan(node.alias) + chalk.gray(': ') + chalk.red('No provider capsule configured'))
364
+ return
365
+ }
366
+
367
+ const sourceDirPath = aliasConfig.sourceDir ? resolve(aliasConfig.sourceDir) : 'N/A'
368
+ const relPath = aliasConfig.sourceDir ? relative(workspaceRootDir, sourceDirPath) : 'N/A'
369
+
370
+ const nodeConnector = isLast ? '└── ' : '├── '
371
+ console.log(chalk.gray(indent + nodeConnector) + chalk.cyan(node.alias) + chalk.gray(' (') + chalk.white(relPath) + chalk.gray(')'))
372
+
373
+ const detailIndent = indent + (isLast ? ' ' : '│ ')
374
+ const statusArray = statusResults.get(node.alias) || []
375
+
376
+ if (statusArray.length === 0) {
377
+ console.log(chalk.gray(`${detailIndent}Status: `) + chalk.gray('not deployed'))
378
+ }
379
+
380
+ const printStatus = (status: any) => {
381
+ if (!status) {
382
+ console.log(chalk.yellow(`${detailIndent}Status method not available for this provider`))
383
+ } else if (status.error) {
384
+ console.log(chalk.gray(`${detailIndent}Project: `) + chalk.magenta(status.projectName || 'N/A') + chalk.gray(' → ') + chalk.green(status.provider || 'unknown'))
385
+ console.log(chalk.red(`${detailIndent}Error: ${status.error}`))
386
+ const isNotFoundError = status.error.toLowerCase().includes('not found')
387
+ if (!isNotFoundError && status.rawDefinitionFilepaths && status.rawDefinitionFilepaths.length > 0) {
388
+ status.rawDefinitionFilepaths.forEach((filepath: string) => {
389
+ console.log(chalk.gray(`${detailIndent}Fact: `) + chalk.white(filepath))
390
+ })
391
+ }
392
+ } else {
393
+ console.log(chalk.gray(`${detailIndent}Project: `) + chalk.magenta(status.projectName || 'N/A') + chalk.gray(' → ') + chalk.green(status.provider || 'unknown'))
394
+
395
+ const statusColor = status.status === 'READY' ? chalk.green :
396
+ status.status === 'ERROR' ? chalk.red :
397
+ status.status === 'DISABLED' ? chalk.red :
398
+ chalk.yellow
399
+ console.log(chalk.gray(`${detailIndent}Status: `) + statusColor(status.status || 'UNKNOWN'))
400
+
401
+ if (status.publicUrl) {
402
+ console.log(chalk.gray(`${detailIndent}URL: `) + chalk.blue(status.publicUrl))
403
+ }
404
+
405
+ if (status.createdAt) {
406
+ const date = new Date(status.createdAt)
407
+ const elapsed = formatElapsedTime(status.createdAt)
408
+ console.log(chalk.gray(`${detailIndent}Created: `) + chalk.white(date.toLocaleString()) + chalk.gray(` (${elapsed})`))
409
+ }
410
+
411
+ if (status.updatedAt) {
412
+ const date = new Date(status.updatedAt)
413
+ const elapsed = formatElapsedTime(status.updatedAt)
414
+ console.log(chalk.gray(`${detailIndent}Updated: `) + chalk.white(date.toLocaleString()) + chalk.gray(` (${elapsed})`))
415
+ }
416
+
417
+ if (status.usage) {
418
+ if (status.usage.storageBytes !== undefined) {
419
+ const storageMB = (status.usage.storageBytes / (1024 * 1024)).toFixed(2)
420
+ console.log(chalk.gray(`${detailIndent}Storage: `) + chalk.white(`${storageMB} MB`) + chalk.gray(` (${status.usage.filesCount || 0} files)`))
421
+ }
422
+
423
+ if (status.usage.bandwidthBytes !== undefined) {
424
+ const bandwidthGB = (status.usage.bandwidthBytes / (1024 * 1024 * 1024)).toFixed(2)
425
+ console.log(chalk.gray(`${detailIndent}Bandwidth: `) + chalk.white(`${bandwidthGB} GB this month`))
426
+ }
427
+
428
+ if (status.usage.charges !== undefined) {
429
+ console.log(chalk.gray(`${detailIndent}Charges: `) + chalk.white(`$${status.usage.charges.toFixed(2)} this month`))
430
+ }
431
+ }
432
+
433
+ if (status.providerPortalUrl) {
434
+ console.log(chalk.gray(`${detailIndent}Portal: `) + chalk.blue(status.providerPortalUrl))
435
+ }
436
+
437
+ if (status.rawDefinitionFilepaths && status.rawDefinitionFilepaths.length > 0) {
438
+ status.rawDefinitionFilepaths.forEach((filepath: string) => {
439
+ console.log(chalk.gray(`${detailIndent}Fact: `) + chalk.white(filepath))
440
+ })
441
+ }
442
+ }
443
+ }
444
+
445
+ for (const status of statusArray) {
446
+ printStatus(status)
447
+ }
448
+
449
+ if (node.children.length > 0) {
450
+ const childIndent = indent + (isLast ? ' ' : '│ ')
451
+ for (let i = 0; i < node.children.length; i++) {
452
+ printNode(node.children[i], childIndent, i === node.children.length - 1)
453
+ }
454
+ }
455
+ }
456
+
457
+ tree.forEach((node, index) => {
458
+ printNode(node, ' ' + continueLine, index === tree.length - 1)
459
+ })
460
+ } else {
461
+ // Compact mode: one line per provider status;
462
+ // aliases with no cached status get a single 'not deployed' line
463
+ const allLines: { status: any, alias: string }[] = []
464
+ const collectAllLines = (nodes: any[]) => {
465
+ for (const node of nodes) {
466
+ const statusArray = statusResults.get(node.alias) || []
467
+ if (statusArray.length > 0) {
468
+ for (const status of statusArray) {
469
+ allLines.push({ status, alias: node.alias })
470
+ }
471
+ } else {
472
+ allLines.push({ status: null, alias: node.alias })
473
+ }
474
+ if (node.children.length > 0) {
475
+ collectAllLines(node.children)
476
+ }
477
+ }
478
+ }
479
+ collectAllLines(tree)
480
+
481
+ for (const { status, alias } of allLines) {
482
+ itemIndex++
483
+ const isLastItem = itemIndex === totalItems
484
+ const connector = isLastItem ? '└── ' : '├── '
485
+
486
+ if (status === null) {
487
+ // No cached data — extract provider name from config
488
+ const aliasConfig = (tree.find((n: any) => n.alias === alias) || { config: {} }).config
489
+ const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
490
+ const vendorNames = providers.map((p: any) => {
491
+ const match = (p.capsule || '').match(/\/caps\/providers\/([^\/]+)\//)
492
+ return match ? match[1] : 'unknown'
493
+ }).join(' & ')
494
+ console.log(chalk.gray(' ' + connector +
495
+ alias + ' → ' +
496
+ vendorNames + ' [not deployed]'))
497
+ } else if (status.error) {
498
+ const providerName = status?.provider || 'unknown'
499
+ const projName = status?.projectName || 'unknown'
500
+ console.log(chalk.gray(' ' + connector +
501
+ deploymentName + ' → ' +
502
+ providerName + ' [' + projName + ']'))
503
+ } else {
504
+ const updatedAgo = status.updatedAt ? formatElapsedTime(status.updatedAt) : null
505
+ const nameWithAge = chalk.yellow(deploymentName) +
506
+ (updatedAgo ? chalk.gray(' (') + chalk.magenta(updatedAgo) + chalk.gray(')') : '')
507
+
508
+ const parts = [
509
+ nameWithAge + chalk.gray(' → ') +
510
+ chalk.green(status.provider) + chalk.gray(' [') + chalk.magenta(status.projectName) + chalk.gray(']')
511
+ ]
512
+ if (status.publicUrl) {
513
+ parts.push(chalk.blue(status.publicUrl))
514
+ }
515
+ if (status.providerPortalUrl) {
516
+ parts.push(chalk.gray('portal: ') + chalk.blue(status.providerPortalUrl))
517
+ }
518
+ console.log(chalk.gray(' ' + connector) + parts.join(chalk.gray(' | ')))
519
+ }
520
+ }
521
+ }
522
+ }
523
+ }
524
+ } else {
525
+ console.log(chalk.bold.yellow('PROJECTS:'), chalk.gray('None configured\n'))
526
+ }
527
+
528
+ console.log(chalk.bold('═══════════════════════════════════════════════════════════════\n'))
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }, {
534
+ importMeta: import.meta,
535
+ importStack: makeImportStack(),
536
+ capsuleName: capsule['#'],
537
+ })
538
+ }
539
+ capsule['#'] = 't44/caps/WorkspaceInfo'
540
+
541
+
542
+
543
+ function buildDependencyTree(projectAliases: Record<string, any>): { alias: string, children: any[], config: any }[] {
544
+ const aliasMap = new Map<string, { alias: string, children: any[], config: any }>()
545
+ const roots: any[] = []
546
+
547
+ // Create nodes for all aliases
548
+ for (const [alias, config] of Object.entries(projectAliases)) {
549
+ aliasMap.set(alias, { alias, children: [], config })
550
+ }
551
+
552
+ // Build parent-child relationships
553
+ for (const [alias, config] of Object.entries(projectAliases)) {
554
+ const node = aliasMap.get(alias)!
555
+ const depends = config.depends || []
556
+
557
+ if (depends.length === 0) {
558
+ // No dependencies, this is a root
559
+ roots.push(node)
560
+ } else {
561
+ // Add this node as a child to all its dependencies
562
+ for (const dep of depends) {
563
+ const parent = aliasMap.get(dep)
564
+ if (parent) {
565
+ parent.children.push(node)
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ return roots
572
+ }
573
+
574
+ function formatElapsedTime(timestamp: string | number): string {
575
+ const now = Date.now()
576
+ const ts = typeof timestamp === 'string' ? new Date(timestamp).getTime() : timestamp
577
+ const elapsed = now - ts
578
+
579
+ const days = Math.floor(elapsed / (1000 * 60 * 60 * 24))
580
+ const hours = Math.floor(elapsed / (1000 * 60 * 60))
581
+ const minutes = Math.floor(elapsed / (1000 * 60))
582
+ const seconds = Math.floor(elapsed / 1000)
583
+
584
+ if (days > 0) {
585
+ return `${days}d`
586
+ } else if (hours > 0) {
587
+ return `${hours}h`
588
+ } else if (minutes > 0) {
589
+ return `${minutes}m`
590
+ } else if (seconds > 0) {
591
+ return `${seconds}s`
592
+ } else {
593
+ return 'now'
594
+ }
595
+ }
@@ -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['#'] = 't44/caps/WorkspaceInit'