@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,332 @@
1
+
2
+ import { join } from 'path'
3
+ import { $ } from 'bun'
4
+ import { mkdir, access, readFile, writeFile } from 'fs/promises'
5
+ import { constants } from 'fs'
6
+
7
+ export async function capsule({
8
+ encapsulate,
9
+ CapsulePropertyTypes,
10
+ makeImportStack
11
+ }: {
12
+ encapsulate: any
13
+ CapsulePropertyTypes: any
14
+ makeImportStack: any
15
+ }) {
16
+ return encapsulate({
17
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
18
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
19
+ '#@stream44.studio/t44/structs/WorkspaceConfig': {
20
+ as: '$WorkspaceConfig'
21
+ },
22
+ '#': {
23
+ WorkspaceConfig: {
24
+ type: CapsulePropertyTypes.Mapping,
25
+ value: '@stream44.studio/t44/caps/WorkspaceConfig'
26
+ },
27
+ getStagePath: {
28
+ type: CapsulePropertyTypes.Function,
29
+ value: async function (this: any, { repoUri }: { repoUri: string }): Promise<string> {
30
+ const normalizedUri = repoUri.replace(/[\/]/g, '~')
31
+ return join(
32
+ this.WorkspaceConfig.workspaceRootDir,
33
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectRepository/stage',
34
+ normalizedUri
35
+ )
36
+ }
37
+ },
38
+ init: {
39
+ type: CapsulePropertyTypes.Function,
40
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
41
+ await mkdir(rootDir, { recursive: true })
42
+
43
+ const gitDir = join(rootDir, '.git')
44
+ let isGitRepo = false
45
+ try {
46
+ await access(gitDir, constants.F_OK)
47
+ isGitRepo = true
48
+ } catch { }
49
+
50
+ if (!isGitRepo) {
51
+ await $`git init`.cwd(rootDir).quiet()
52
+ await $`git commit --allow-empty -m init`.cwd(rootDir).quiet()
53
+ }
54
+ }
55
+ },
56
+ reset: {
57
+ type: CapsulePropertyTypes.Function,
58
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
59
+ await $`git checkout -- .`.cwd(rootDir).quiet().nothrow()
60
+ await $`git clean -fd`.cwd(rootDir).quiet().nothrow()
61
+ }
62
+ },
63
+ sync: {
64
+ type: CapsulePropertyTypes.Function,
65
+ value: async function (this: any, { rootDir, sourceDir, gitignorePath, excludePatterns }: {
66
+ rootDir: string
67
+ sourceDir: string
68
+ gitignorePath?: string
69
+ excludePatterns?: string[]
70
+ }): Promise<void> {
71
+ let gitignoreExists = false
72
+ if (gitignorePath) {
73
+ try {
74
+ await access(gitignorePath, constants.F_OK)
75
+ gitignoreExists = true
76
+ } catch { }
77
+ }
78
+
79
+ const rsyncArgs = ['rsync', '-a', '--delete', '--exclude', '.git']
80
+ if (gitignoreExists && gitignorePath) {
81
+ rsyncArgs.push('--exclude-from=' + gitignorePath)
82
+ }
83
+ // Add additional exclude patterns from alwaysIgnore config
84
+ if (excludePatterns && excludePatterns.length > 0) {
85
+ for (const pattern of excludePatterns) {
86
+ rsyncArgs.push('--exclude', pattern)
87
+ }
88
+ }
89
+ rsyncArgs.push(sourceDir + '/', rootDir + '/')
90
+ await $`${rsyncArgs}`
91
+ }
92
+ },
93
+ hasChanges: {
94
+ type: CapsulePropertyTypes.Function,
95
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
96
+ await $`git add -A`.cwd(rootDir).quiet()
97
+ const diff = await $`git diff --cached --stat`.cwd(rootDir).quiet().nothrow()
98
+ const hasChanges = diff.text().trim().length > 0
99
+ await $`git reset`.cwd(rootDir).quiet().nothrow()
100
+ return hasChanges
101
+ }
102
+ },
103
+ commit: {
104
+ type: CapsulePropertyTypes.Function,
105
+ value: async function (this: any, { rootDir, message }: {
106
+ rootDir: string
107
+ message: string
108
+ }): Promise<void> {
109
+ await $`git add -A`.cwd(rootDir).quiet()
110
+ await $`git commit -m ${message}`.cwd(rootDir).quiet().nothrow()
111
+ }
112
+ },
113
+ getHeadCommit: {
114
+ type: CapsulePropertyTypes.Function,
115
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<string> {
116
+ const result = await $`git rev-parse HEAD`.cwd(rootDir).quiet().nothrow()
117
+ if (result.exitCode !== 0) return ''
118
+ return result.text().trim()
119
+ }
120
+ },
121
+ getBranch: {
122
+ type: CapsulePropertyTypes.Function,
123
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<string> {
124
+ const result = await $`git rev-parse --abbrev-ref HEAD`.cwd(rootDir).quiet()
125
+ return result.text().trim()
126
+ }
127
+ },
128
+ getLastCommitMessage: {
129
+ type: CapsulePropertyTypes.Function,
130
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<string> {
131
+ const result = await $`git log -1 --pretty=%B`.cwd(rootDir).quiet()
132
+ return result.text().trim()
133
+ }
134
+ },
135
+ clone: {
136
+ type: CapsulePropertyTypes.Function,
137
+ value: async function (this: any, { originUri, targetDir }: {
138
+ originUri: string
139
+ targetDir: string
140
+ }): Promise<{ isNewEmptyRepo: boolean }> {
141
+ const parentDir = join(targetDir, '..')
142
+ await mkdir(parentDir, { recursive: true })
143
+ await $`git clone ${originUri} ${targetDir}`.cwd(parentDir)
144
+
145
+ const headCheck = await $`git rev-parse HEAD`.cwd(targetDir).quiet().nothrow()
146
+ return { isNewEmptyRepo: headCheck.exitCode !== 0 }
147
+ }
148
+ },
149
+ addAll: {
150
+ type: CapsulePropertyTypes.Function,
151
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
152
+ await $`git add .`.cwd(rootDir).quiet()
153
+ const statusResult = await $`git status --porcelain`.cwd(rootDir).quiet()
154
+ return statusResult.text().trim().length > 0
155
+ }
156
+ },
157
+ isAheadOfRemote: {
158
+ type: CapsulePropertyTypes.Function,
159
+ value: async function (this: any, { rootDir, branch }: { rootDir: string, branch?: string }): Promise<boolean> {
160
+ const branchName = branch || 'main'
161
+ const lsRemoteResult = await $`git ls-remote origin`.cwd(rootDir).quiet().nothrow()
162
+ const lsRemoteOutput = lsRemoteResult.text().trim()
163
+
164
+ if (!lsRemoteOutput) {
165
+ return true
166
+ }
167
+
168
+ const localHead = (await $`git rev-parse HEAD`.cwd(rootDir).quiet()).text().trim()
169
+ const remoteHeadLine = lsRemoteOutput.split('\n').find((l: string) => l.includes(`refs/heads/${branchName}`))
170
+ const remoteHead = remoteHeadLine ? remoteHeadLine.split('\t')[0] : null
171
+
172
+ return !remoteHead || remoteHead !== localHead
173
+ }
174
+ },
175
+ push: {
176
+ type: CapsulePropertyTypes.Function,
177
+ value: async function (this: any, { rootDir, branch }: { rootDir: string, branch?: string }): Promise<void> {
178
+ const branchName = branch || 'main'
179
+ await $`git push -u origin ${branchName} --tags`.cwd(rootDir)
180
+ }
181
+ },
182
+ forcePush: {
183
+ type: CapsulePropertyTypes.Function,
184
+ value: async function (this: any, { rootDir, branch }: { rootDir: string, branch?: string }): Promise<void> {
185
+ const branchName = branch || 'main'
186
+ await $`git push --force origin ${branchName} --tags`.cwd(rootDir)
187
+ }
188
+ },
189
+ squashAllCommits: {
190
+ type: CapsulePropertyTypes.Function,
191
+ value: async function (this: any, { rootDir, message }: {
192
+ rootDir: string
193
+ message: string
194
+ }): Promise<void> {
195
+ const rootCommit = await $`git rev-list --max-parents=0 HEAD`.cwd(rootDir).text()
196
+ await $`git reset --soft ${rootCommit.trim()}`.cwd(rootDir)
197
+ await $`git commit --amend -m ${message}`.cwd(rootDir)
198
+ }
199
+ },
200
+ tag: {
201
+ type: CapsulePropertyTypes.Function,
202
+ value: async function (this: any, { rootDir, tag }: {
203
+ rootDir: string
204
+ tag: string
205
+ }): Promise<void> {
206
+ await $`git tag ${tag}`.cwd(rootDir)
207
+ }
208
+ },
209
+ hasTag: {
210
+ type: CapsulePropertyTypes.Function,
211
+ value: async function (this: any, { rootDir, tag }: {
212
+ rootDir: string
213
+ tag: string
214
+ }): Promise<{ exists: boolean, commit?: string }> {
215
+ const localTagCheck = await $`git tag -l ${tag}`.cwd(rootDir).quiet().nothrow()
216
+ if (localTagCheck.text().trim() === tag) {
217
+ const tagCommit = (await $`git rev-parse ${tag}^{}`.cwd(rootDir).quiet().nothrow()).text().trim()
218
+ return { exists: true, commit: tagCommit }
219
+ }
220
+ return { exists: false }
221
+ }
222
+ },
223
+ hasRemoteTag: {
224
+ type: CapsulePropertyTypes.Function,
225
+ value: async function (this: any, { rootDir, tag }: {
226
+ rootDir: string
227
+ tag: string
228
+ }): Promise<{ exists: boolean, commit?: string }> {
229
+ const remoteTagCheck = await $`git ls-remote --tags origin ${tag}`.cwd(rootDir).quiet().nothrow()
230
+ const output = remoteTagCheck.text().trim()
231
+ if (output.length > 0) {
232
+ const commit = output.split(/\s+/)[0]
233
+ return { exists: true, commit }
234
+ }
235
+ return { exists: false }
236
+ }
237
+ },
238
+ diff: {
239
+ type: CapsulePropertyTypes.Function,
240
+ value: async function (this: any, { rootDir, from, to }: {
241
+ rootDir: string
242
+ from: string
243
+ to?: string
244
+ }): Promise<string> {
245
+ const toRef = to || 'HEAD'
246
+ const result = await $`git diff ${from} ${toRef}`.cwd(rootDir).quiet().nothrow()
247
+ return result.text().trim()
248
+ }
249
+ },
250
+ exists: {
251
+ type: CapsulePropertyTypes.Function,
252
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
253
+ try {
254
+ await access(join(rootDir, '.git'), constants.F_OK)
255
+ return true
256
+ } catch {
257
+ return false
258
+ }
259
+ }
260
+ },
261
+ initBare: {
262
+ type: CapsulePropertyTypes.Function,
263
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
264
+ await mkdir(rootDir, { recursive: true })
265
+
266
+ let isBareRepo = false
267
+ try {
268
+ await access(join(rootDir, 'HEAD'), constants.F_OK)
269
+ isBareRepo = true
270
+ } catch { }
271
+
272
+ if (!isBareRepo) {
273
+ await $`git init --bare`.cwd(rootDir).quiet()
274
+ }
275
+ }
276
+ },
277
+ hasRemote: {
278
+ type: CapsulePropertyTypes.Function,
279
+ value: async function (this: any, { rootDir, name }: {
280
+ rootDir: string
281
+ name: string
282
+ }): Promise<boolean> {
283
+ const result = await $`git remote`.cwd(rootDir).quiet().nothrow()
284
+ const remotes = result.text().trim().split('\n').filter(Boolean)
285
+ return remotes.includes(name)
286
+ }
287
+ },
288
+ addRemote: {
289
+ type: CapsulePropertyTypes.Function,
290
+ value: async function (this: any, { rootDir, name, url }: {
291
+ rootDir: string
292
+ name: string
293
+ url: string
294
+ }): Promise<void> {
295
+ await $`git remote add ${name} ${url}`.cwd(rootDir).quiet()
296
+ }
297
+ },
298
+ setRemoteUrl: {
299
+ type: CapsulePropertyTypes.Function,
300
+ value: async function (this: any, { rootDir, name, url }: {
301
+ rootDir: string
302
+ name: string
303
+ url: string
304
+ }): Promise<void> {
305
+ await $`git remote set-url ${name} ${url}`.cwd(rootDir).quiet()
306
+ }
307
+ },
308
+ pushToRemote: {
309
+ type: CapsulePropertyTypes.Function,
310
+ value: async function (this: any, { rootDir, remote, branch, force }: {
311
+ rootDir: string
312
+ remote: string
313
+ branch?: string
314
+ force?: boolean
315
+ }): Promise<void> {
316
+ const branchName = branch || 'main'
317
+ if (force) {
318
+ await $`git push --force ${remote} ${branchName}`.cwd(rootDir).quiet()
319
+ } else {
320
+ await $`git push ${remote} ${branchName}`.cwd(rootDir).quiet()
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ }, {
327
+ importMeta: import.meta,
328
+ importStack: makeImportStack(),
329
+ capsuleName: capsule['#'],
330
+ })
331
+ }
332
+ capsule['#'] = '@stream44.studio/t44/caps/ProjectRepository'
@@ -0,0 +1,251 @@
1
+ import type * as BunTest from 'bun:test'
2
+ import { config as loadDotenv } from 'dotenv'
3
+ import { join, dirname, basename } from 'path'
4
+ import { mkdir } from 'fs/promises'
5
+
6
+ // Global cache for loaded env files (this is fine as a cache)
7
+ const loadedEnvFiles = new Set<string>()
8
+
9
+ export async function capsule({
10
+ encapsulate,
11
+ CapsulePropertyTypes,
12
+ makeImportStack
13
+ }: {
14
+ encapsulate: any
15
+ CapsulePropertyTypes: any
16
+ makeImportStack: any
17
+ }) {
18
+ return encapsulate({
19
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
20
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
21
+ '#': {
22
+ lib: {
23
+ type: CapsulePropertyTypes.Mapping,
24
+ value: '@stream44.studio/t44/caps/ProjectTestLib',
25
+ },
26
+ bunTest: {
27
+ type: CapsulePropertyTypes.Literal,
28
+ value: undefined as any as typeof BunTest,
29
+ },
30
+ env: {
31
+ type: CapsulePropertyTypes.Literal,
32
+ value: undefined
33
+ },
34
+ verbose: {
35
+ type: CapsulePropertyTypes.Literal,
36
+ value: false,
37
+ },
38
+ testRootDir: {
39
+ type: CapsulePropertyTypes.Literal,
40
+ value: undefined as string | undefined,
41
+ },
42
+ _envLoaded: {
43
+ type: CapsulePropertyTypes.Literal,
44
+ value: false,
45
+ },
46
+ loadEnvFiles: {
47
+ type: CapsulePropertyTypes.Function,
48
+ value: function (this: any, cwd: string): void {
49
+ if (this._envLoaded) return
50
+
51
+ // Load .env file if it exists
52
+ const envPath = join(cwd, '.env')
53
+ if (!loadedEnvFiles.has(envPath)) {
54
+ loadDotenv({ path: envPath, quiet: true })
55
+ loadedEnvFiles.add(envPath)
56
+ }
57
+
58
+ // Load .env.dev file if it exists
59
+ const envDevPath = join(cwd, '.env.dev')
60
+ if (!loadedEnvFiles.has(envDevPath)) {
61
+ loadDotenv({ path: envDevPath, quiet: true })
62
+ loadedEnvFiles.add(envDevPath)
63
+ }
64
+
65
+ this._envLoaded = true
66
+ }
67
+ },
68
+ getEnvValue: {
69
+ type: CapsulePropertyTypes.Function,
70
+ value: function (this: any, envVarName: string): string | undefined {
71
+ // Auto-load env files from testRootDir if available
72
+ if (this.testRootDir) {
73
+ this.loadEnvFiles(this.testRootDir)
74
+ }
75
+ return process.env[envVarName]
76
+ }
77
+ },
78
+ workbenchDir: {
79
+ type: CapsulePropertyTypes.GetterFunction,
80
+ value: function (this: any): string {
81
+
82
+ const moduleFilepath = this['#@stream44.studio/encapsulate/structs/Capsule'].rootCapsule.moduleFilepath
83
+ const dir = join(this.testRootDir, '.~o/workspace.foundation/workbenches', basename(moduleFilepath).replace(/\.[^\.]+$/, ''))
84
+
85
+ return dir
86
+ }
87
+ },
88
+ emptyWorkbenchDir: {
89
+ type: CapsulePropertyTypes.Function,
90
+ value: async function (this: any): Promise<void> {
91
+ const dir = this.workbenchDir
92
+
93
+ // Ensure the directory exists first
94
+ await mkdir(dir, { recursive: true })
95
+
96
+ // Remove directory contents (not the directory itself) including dotfiles
97
+ // Use shell with proper globbing to handle both regular files and dotfiles
98
+ await Bun.$`sh -c 'rm -rf ${dir}/* ${dir}/.[!.]* ${dir}/..?* 2>/dev/null || true'`.quiet()
99
+ }
100
+ },
101
+ EnsureEmptyWorkbenchDir: {
102
+ type: CapsulePropertyTypes.Init,
103
+ value: async function (this: any) {
104
+ // Only run if bunTest is available (test mode)
105
+ if (!this.bunTest) return
106
+ await this.emptyWorkbenchDir()
107
+ }
108
+ },
109
+ describe: {
110
+ type: CapsulePropertyTypes.GetterFunction,
111
+ value: function (this: any) {
112
+ const bunTestModule = this.bunTest
113
+ const describeMethod = (name: string, fn: () => void) => {
114
+ return bunTestModule.describe(name, async () => {
115
+ await fn()
116
+ })
117
+ }
118
+ describeMethod.skip = (name: string, fn: () => void) => {
119
+ return bunTestModule.describe.skip(name, async () => {
120
+ await fn()
121
+ })
122
+ }
123
+ return describeMethod
124
+ }
125
+ },
126
+ it: {
127
+ type: CapsulePropertyTypes.GetterFunction,
128
+ value: function (this: any) {
129
+ const bunTestModule = this.bunTest
130
+ const itMethod = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
131
+ return bunTestModule.it(name, async () => {
132
+ try {
133
+ await fn()
134
+ } catch (error: any) {
135
+ // Check for MISSING_CREDENTIALS error - skip test gracefully
136
+ if (error?.message?.startsWith('MISSING_CREDENTIALS:')) {
137
+ const parts = error.message.slice('MISSING_CREDENTIALS:'.length).split(':')
138
+ const provider = parts[0] || 'unknown'
139
+ const credentialName = parts[1] || 'credentials'
140
+ console.log(`\n ⚠️ Skipping test: ${provider} credentials not configured (${credentialName})`)
141
+ bunTestModule.expect(true).toBe(true) // Mark as passed/skipped
142
+ return
143
+ }
144
+ throw error
145
+ }
146
+ }, options)
147
+ }
148
+ itMethod.skip = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
149
+ return bunTestModule.it.skip(name, async () => {
150
+ await fn()
151
+ }, options)
152
+ }
153
+ return itMethod
154
+ }
155
+ },
156
+ test: {
157
+ type: CapsulePropertyTypes.GetterFunction,
158
+ value: function (this: any) {
159
+ const bunTestModule = this.bunTest
160
+ const testMethod = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
161
+ return bunTestModule.test(name, async () => {
162
+ try {
163
+ await fn()
164
+ } catch (error: any) {
165
+ // Check for MISSING_CREDENTIALS error - skip test gracefully
166
+ if (error?.message?.startsWith('MISSING_CREDENTIALS:')) {
167
+ const parts = error.message.slice('MISSING_CREDENTIALS:'.length).split(':')
168
+ const provider = parts[0] || 'unknown'
169
+ const credentialName = parts[1] || 'credentials'
170
+ console.log(`\n ⚠️ Skipping test: ${provider} credentials not configured (${credentialName})`)
171
+ bunTestModule.expect(true).toBe(true) // Mark as passed/skipped
172
+ return
173
+ }
174
+ throw error
175
+ }
176
+ }, options)
177
+ }
178
+ testMethod.skip = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
179
+ return bunTestModule.test.skip(name, async () => {
180
+ await fn()
181
+ }, options)
182
+ }
183
+ return testMethod
184
+ }
185
+ },
186
+ expect: {
187
+ type: CapsulePropertyTypes.GetterFunction,
188
+ value: function (this: any): typeof BunTest.expect {
189
+ return this.bunTest.expect
190
+ }
191
+ },
192
+ beforeAll: {
193
+ type: CapsulePropertyTypes.Function,
194
+ value: function (this: any, fn: () => void | Promise<void>) {
195
+ return this.bunTest.beforeAll(async () => {
196
+ await fn()
197
+ })
198
+ }
199
+ },
200
+ afterAll: {
201
+ type: CapsulePropertyTypes.Function,
202
+ value: function (this: any, fn: () => void | Promise<void>) {
203
+ return this.bunTest.afterAll(async () => {
204
+ await fn()
205
+ })
206
+ }
207
+ },
208
+ beforeEach: {
209
+ type: CapsulePropertyTypes.Function,
210
+ value: function (this: any, fn: () => void | Promise<void>) {
211
+ return this.bunTest.beforeEach(async () => {
212
+ await fn()
213
+ })
214
+ }
215
+ },
216
+ afterEach: {
217
+ type: CapsulePropertyTypes.Function,
218
+ value: function (this: any, fn: () => void | Promise<void>) {
219
+ return this.bunTest.afterEach(async () => {
220
+ await fn()
221
+ })
222
+ }
223
+ },
224
+ getRandomPort: {
225
+ type: CapsulePropertyTypes.Function,
226
+ value: async function (this: any): Promise<number> {
227
+ const net = await import('net')
228
+ const isPortAvailable = (port: number): Promise<boolean> => {
229
+ return new Promise((resolve) => {
230
+ const server = net.createServer()
231
+ server.once('error', () => resolve(false))
232
+ server.once('listening', () => { server.close(); resolve(true) })
233
+ server.listen(port, '127.0.0.1')
234
+ })
235
+ }
236
+ for (let attempt = 0; attempt < 10; attempt++) {
237
+ const port = 10000 + Math.floor(Math.random() * (65535 - 10000))
238
+ if (await isPortAvailable(port)) return port
239
+ }
240
+ throw new Error('Could not find an available port after 10 attempts')
241
+ }
242
+ },
243
+ }
244
+ }
245
+ }, {
246
+ importMeta: import.meta,
247
+ importStack: makeImportStack(),
248
+ capsuleName: capsule['#']
249
+ })
250
+ }
251
+ capsule['#'] = '@stream44.studio/t44/caps/ProjectTest'