@stream44.studio/t44 0.4.0-rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dco-signatures +9 -0
- package/.github/workflows/dco.yaml +12 -0
- package/.github/workflows/gordian-open-integrity.yaml +13 -0
- package/.github/workflows/test.yaml +31 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +21 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
- package/.repo-identifier +1 -0
- package/DCO.md +34 -0
- package/LICENSE.txt +186 -0
- package/README.md +189 -0
- package/bin/activate +36 -0
- package/bin/activate.ts +30 -0
- package/bin/postinstall.sh +19 -0
- package/bin/shell +27 -0
- package/bin/t44 +27 -0
- package/caps/ConfigSchemaStruct.ts +55 -0
- package/caps/Home.ts +57 -0
- package/caps/HomeRegistry.ts +319 -0
- package/caps/HomeRegistryFile.ts +144 -0
- package/caps/JsonSchemas.ts +220 -0
- package/caps/OpenApiSchema.ts +67 -0
- package/caps/PackageDescriptor.ts +88 -0
- package/caps/ProjectCatalogs.ts +153 -0
- package/caps/ProjectDeployment.ts +426 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +654 -0
- package/caps/ProjectPulling.ts +234 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +332 -0
- package/caps/ProjectTest.ts +251 -0
- package/caps/ProjectTestLib.ts +257 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/TaskWorkflow.ts +192 -0
- package/caps/WorkspaceCli.ts +448 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +87 -0
- package/caps/WorkspaceConfigFile.ts +902 -0
- package/caps/WorkspaceConnection.ts +329 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +619 -0
- package/caps/WorkspaceInit.ts +30 -0
- package/caps/WorkspaceKey.ts +338 -0
- package/caps/WorkspaceModel.ts +373 -0
- package/caps/WorkspaceProjects.ts +636 -0
- package/caps/WorkspacePrompt.ts +430 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.ts +104 -0
- package/caps/WorkspaceShell.yaml +64 -0
- package/caps/WorkspaceShellCli.ts +109 -0
- package/caps/patterns/README.md +2 -0
- package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
- package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/examples/01-Lifecycle/main.test.ts +223 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +381 -0
- package/lib/schema-console-renderer.ts +181 -0
- package/lib/schema-resolver.ts +349 -0
- package/lib/ucan.ts +137 -0
- package/package.json +91 -0
- package/standalone-rt.test.ts +150 -0
- package/standalone-rt.ts +140 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +60 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingConfig.ts +78 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectPullingConfig.ts +52 -0
- package/structs/ProjectRack.ts +51 -0
- package/structs/ProjectRackConfig.ts +56 -0
- package/structs/RepositoryOriginDescriptor.ts +51 -0
- package/structs/RootKeyConfig.ts +64 -0
- package/structs/SigningKeyConfig.ts +64 -0
- package/structs/Workspace.ts +56 -0
- package/structs/WorkspaceCatalogs.ts +56 -0
- package/structs/WorkspaceCliConfig.ts +53 -0
- package/structs/WorkspaceConfig.ts +64 -0
- package/structs/WorkspaceConfigFile.ts +50 -0
- package/structs/WorkspaceConfigFileMeta.ts +70 -0
- package/structs/WorkspaceKey.ts +55 -0
- package/structs/WorkspaceKeyConfig.ts +56 -0
- package/structs/WorkspaceMappingsConfig.ts +56 -0
- package/structs/WorkspaceProject.ts +104 -0
- package/structs/WorkspaceProjectsConfig.ts +67 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/patterns/README.md +2 -0
- package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/tsconfig.json +33 -0
- package/workspace-rt.ts +152 -0
- package/workspace.yaml +3 -0
|
@@ -0,0 +1,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'
|