t44 0.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of t44 might be problematic. Click here for more details.

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