@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.
Files changed (99) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yaml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yaml +13 -0
  4. package/.github/workflows/test.yaml +31 -0
  5. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  7. package/.o/GordianOpenIntegrity.yaml +21 -0
  8. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  9. package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
  10. package/.repo-identifier +1 -0
  11. package/DCO.md +34 -0
  12. package/LICENSE.txt +186 -0
  13. package/README.md +189 -0
  14. package/bin/activate +36 -0
  15. package/bin/activate.ts +30 -0
  16. package/bin/postinstall.sh +19 -0
  17. package/bin/shell +27 -0
  18. package/bin/t44 +27 -0
  19. package/caps/ConfigSchemaStruct.ts +55 -0
  20. package/caps/Home.ts +57 -0
  21. package/caps/HomeRegistry.ts +319 -0
  22. package/caps/HomeRegistryFile.ts +144 -0
  23. package/caps/JsonSchemas.ts +220 -0
  24. package/caps/OpenApiSchema.ts +67 -0
  25. package/caps/PackageDescriptor.ts +88 -0
  26. package/caps/ProjectCatalogs.ts +153 -0
  27. package/caps/ProjectDeployment.ts +426 -0
  28. package/caps/ProjectDevelopment.ts +257 -0
  29. package/caps/ProjectPublishing.ts +654 -0
  30. package/caps/ProjectPulling.ts +234 -0
  31. package/caps/ProjectRack.ts +155 -0
  32. package/caps/ProjectRepository.ts +332 -0
  33. package/caps/ProjectTest.ts +251 -0
  34. package/caps/ProjectTestLib.ts +257 -0
  35. package/caps/RootKey.ts +219 -0
  36. package/caps/SigningKey.ts +243 -0
  37. package/caps/TaskWorkflow.ts +192 -0
  38. package/caps/WorkspaceCli.ts +448 -0
  39. package/caps/WorkspaceConfig.ts +268 -0
  40. package/caps/WorkspaceConfig.yaml +87 -0
  41. package/caps/WorkspaceConfigFile.ts +902 -0
  42. package/caps/WorkspaceConnection.ts +329 -0
  43. package/caps/WorkspaceEntityConfig.ts +78 -0
  44. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  45. package/caps/WorkspaceEntityFact.ts +218 -0
  46. package/caps/WorkspaceInfo.ts +619 -0
  47. package/caps/WorkspaceInit.ts +30 -0
  48. package/caps/WorkspaceKey.ts +338 -0
  49. package/caps/WorkspaceModel.ts +373 -0
  50. package/caps/WorkspaceProjects.ts +636 -0
  51. package/caps/WorkspacePrompt.ts +430 -0
  52. package/caps/WorkspaceShell.sh +39 -0
  53. package/caps/WorkspaceShell.ts +104 -0
  54. package/caps/WorkspaceShell.yaml +64 -0
  55. package/caps/WorkspaceShellCli.ts +109 -0
  56. package/caps/patterns/README.md +2 -0
  57. package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
  58. package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
  59. package/docs/Overview.drawio +248 -0
  60. package/docs/Overview.svg +4 -0
  61. package/examples/01-Lifecycle/main.test.ts +223 -0
  62. package/lib/crypto.ts +53 -0
  63. package/lib/key.ts +381 -0
  64. package/lib/schema-console-renderer.ts +181 -0
  65. package/lib/schema-resolver.ts +349 -0
  66. package/lib/ucan.ts +137 -0
  67. package/package.json +91 -0
  68. package/standalone-rt.test.ts +150 -0
  69. package/standalone-rt.ts +140 -0
  70. package/structs/HomeRegistry.ts +55 -0
  71. package/structs/HomeRegistryConfig.ts +60 -0
  72. package/structs/ProjectCatalogsConfig.ts +53 -0
  73. package/structs/ProjectDeploymentConfig.ts +56 -0
  74. package/structs/ProjectDeploymentFact.ts +106 -0
  75. package/structs/ProjectPublishingConfig.ts +78 -0
  76. package/structs/ProjectPublishingFact.ts +68 -0
  77. package/structs/ProjectPullingConfig.ts +52 -0
  78. package/structs/ProjectRack.ts +51 -0
  79. package/structs/ProjectRackConfig.ts +56 -0
  80. package/structs/RepositoryOriginDescriptor.ts +51 -0
  81. package/structs/RootKeyConfig.ts +64 -0
  82. package/structs/SigningKeyConfig.ts +64 -0
  83. package/structs/Workspace.ts +56 -0
  84. package/structs/WorkspaceCatalogs.ts +56 -0
  85. package/structs/WorkspaceCliConfig.ts +53 -0
  86. package/structs/WorkspaceConfig.ts +64 -0
  87. package/structs/WorkspaceConfigFile.ts +50 -0
  88. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  89. package/structs/WorkspaceKey.ts +55 -0
  90. package/structs/WorkspaceKeyConfig.ts +56 -0
  91. package/structs/WorkspaceMappingsConfig.ts +56 -0
  92. package/structs/WorkspaceProject.ts +104 -0
  93. package/structs/WorkspaceProjectsConfig.ts +67 -0
  94. package/structs/WorkspaceShellConfig.ts +83 -0
  95. package/structs/patterns/README.md +2 -0
  96. package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
  97. package/tsconfig.json +33 -0
  98. package/workspace-rt.ts +152 -0
  99. 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'