@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,319 @@
1
+ export async function capsule({
2
+ encapsulate,
3
+ CapsulePropertyTypes,
4
+ makeImportStack
5
+ }: {
6
+ encapsulate: any
7
+ CapsulePropertyTypes: any
8
+ makeImportStack: any
9
+ }) {
10
+ return encapsulate({
11
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
12
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
13
+ '#@stream44.studio/t44/structs/HomeRegistryConfig': {
14
+ as: '$HomeRegistryConfig'
15
+ },
16
+ '#@stream44.studio/t44/structs/HomeRegistry': {
17
+ as: '$HomeRegistry'
18
+ },
19
+ '#@stream44.studio/t44/structs/Workspace': {
20
+ as: '$Workspace'
21
+ },
22
+ '#@stream44.studio/t44/structs/WorkspaceKey': {
23
+ as: '$WorkspaceKey'
24
+ },
25
+ '#@stream44.studio/t44/structs/ProjectRack': {
26
+ as: '$ProjectRack'
27
+ },
28
+ '#@stream44.studio/t44/structs/WorkspaceCatalogs': {
29
+ as: '$WorkspaceCatalogs'
30
+ },
31
+ '#': {
32
+ Home: {
33
+ type: CapsulePropertyTypes.Mapping,
34
+ value: '@stream44.studio/t44/caps/Home'
35
+ },
36
+ WorkspacePrompt: {
37
+ type: CapsulePropertyTypes.Mapping,
38
+ value: '@stream44.studio/t44/caps/WorkspacePrompt'
39
+ },
40
+ rootDir: {
41
+ type: CapsulePropertyTypes.GetterFunction,
42
+ value: async function (this: any): Promise<string> {
43
+ const config = await this.$HomeRegistryConfig.config
44
+ if (!config?.rootDir) {
45
+ throw new Error('Home registry rootDir is not configured. Run ensureRootDir() first.')
46
+ }
47
+ return config.rootDir
48
+ }
49
+ },
50
+ ensureRootDir: {
51
+ type: CapsulePropertyTypes.Function,
52
+ value: async function (this: any): Promise<string> {
53
+ const { stat, mkdir } = await import('fs/promises')
54
+ const { join } = await import('path')
55
+ const chalk = (await import('chalk')).default
56
+
57
+ const config = await this.$HomeRegistryConfig.config
58
+ const defaultHomeDir = await this.Home.homeDir
59
+
60
+ let chosenDir: string
61
+
62
+ if (config?.rootDir) {
63
+ // Validate that the directory exists
64
+ try {
65
+ const s = await stat(config.rootDir)
66
+ if (!s.isDirectory()) {
67
+ throw new Error(`Home registry path '${config.rootDir}' exists but is not a directory.`)
68
+ }
69
+ } catch (error: any) {
70
+ if (error.code === 'ENOENT') {
71
+ throw new Error(`Home registry directory '${config.rootDir}' does not exist. Please create it or reconfigure.`)
72
+ }
73
+ throw error
74
+ }
75
+ chosenDir = config.rootDir
76
+ } else {
77
+ // rootDir not set — prompt user for home directory
78
+ console.log(chalk.cyan(`\n🏠 Home Directory Setup\n`))
79
+ console.log(chalk.gray(' The home directory is where your workspace keeps its registry,'))
80
+ console.log(chalk.gray(' SSH keys, and other workspace-related files.'))
81
+ console.log('')
82
+
83
+ const chosenHomeDir = await this.WorkspacePrompt.input({
84
+ message: 'Enter the home directory:',
85
+ defaultValue: defaultHomeDir,
86
+ validate: (input: string) => {
87
+ if (!input || input.trim().length === 0) {
88
+ return 'Directory path cannot be empty'
89
+ }
90
+ return true
91
+ }
92
+ })
93
+
94
+ // Check if home directory exists
95
+ let homeDirExists = false
96
+ try {
97
+ const s = await stat(chosenHomeDir)
98
+ homeDirExists = s.isDirectory()
99
+ } catch {
100
+ // Does not exist
101
+ }
102
+
103
+ if (homeDirExists) {
104
+ const confirmed = await this.WorkspacePrompt.confirm({
105
+ message: `Directory '${chosenHomeDir}' already exists. Use it as the home directory for your workspace?`,
106
+ defaultValue: true
107
+ })
108
+
109
+ if (!confirmed) {
110
+ console.log(chalk.red('\n\nABORTED\n'))
111
+ process.exit(0)
112
+ }
113
+ } else {
114
+ // Create the home directory
115
+ await mkdir(chosenHomeDir, { recursive: true })
116
+ console.log(chalk.green(`\n ✓ Created home directory: ${chosenHomeDir}\n`))
117
+ }
118
+
119
+ // Derive registry dir from home dir
120
+ chosenDir = join(chosenHomeDir, '.o/workspace.foundation')
121
+ await mkdir(chosenDir, { recursive: true })
122
+
123
+ await this.$HomeRegistryConfig.setConfigValue(['homeDir'], chosenHomeDir)
124
+ await this.$HomeRegistryConfig.setConfigValue(['rootDir'], chosenDir)
125
+ }
126
+
127
+ // Ensure registry.json exists via the HomeRegistry struct
128
+ let registryData = await this.$HomeRegistry.get('registry')
129
+
130
+ if (!registryData) {
131
+ // registry.json does not exist — generate identity
132
+ console.log(chalk.cyan(`\n Generating home registry identity...\n`))
133
+
134
+ const { generateKeypair } = await import('../lib/ucan.js')
135
+ const { did, privateKey } = await generateKeypair()
136
+
137
+ registryData = {
138
+ did,
139
+ privateKey,
140
+ createdAt: new Date().toISOString(),
141
+ rootDir: chosenDir
142
+ }
143
+
144
+ const registryFilePath = await this.$HomeRegistry.set('registry', registryData)
145
+
146
+ console.log(chalk.green(` ✓ Registry identity saved to:`))
147
+ console.log(chalk.green(` ${registryFilePath}`))
148
+ console.log(chalk.green(` ✓ DID: ${registryData.did}\n`))
149
+ }
150
+
151
+ // Ensure rootDir is set in registry data
152
+ if (!registryData.rootDir || registryData.rootDir !== chosenDir) {
153
+ registryData.rootDir = chosenDir
154
+ await this.$HomeRegistry.set('registry', registryData)
155
+ }
156
+
157
+ // Ensure identifier is set in config
158
+ if (!config?.identifier || config.identifier !== registryData.did) {
159
+ await this.$HomeRegistryConfig.setConfigValue(['identifier'], registryData.did)
160
+ }
161
+
162
+ return chosenDir
163
+ }
164
+ },
165
+ getRegistryPath: {
166
+ type: CapsulePropertyTypes.Function,
167
+ value: async function (this: any): Promise<string> {
168
+ return this.$HomeRegistry.getPath('registry')
169
+ }
170
+ },
171
+ getRegistry: {
172
+ type: CapsulePropertyTypes.Function,
173
+ value: async function (this: any): Promise<any | null> {
174
+ return this.$HomeRegistry.get('registry')
175
+ }
176
+ },
177
+ getWorkspace: {
178
+ type: CapsulePropertyTypes.Function,
179
+ value: async function (this: any, name: string): Promise<any | null> {
180
+ return this.$Workspace.get(name)
181
+ }
182
+ },
183
+ setWorkspace: {
184
+ type: CapsulePropertyTypes.Function,
185
+ value: async function (this: any, name: string, data: { did: string; privateKey: string; createdAt: string; workspaceRootDir: string }): Promise<string> {
186
+ return this.$Workspace.set(name, data)
187
+ }
188
+ },
189
+ getWorkspacePath: {
190
+ type: CapsulePropertyTypes.Function,
191
+ value: async function (this: any, name: string): Promise<string> {
192
+ return this.$Workspace.getPath(name)
193
+ }
194
+ },
195
+ listKeys: {
196
+ type: CapsulePropertyTypes.Function,
197
+ value: async function (this: any): Promise<Array<{ name: string; did: string; createdAt?: string }>> {
198
+ const { readdir, readFile } = await import('fs/promises')
199
+ const { join } = await import('path')
200
+ const rootDir = await this.rootDir
201
+ const keyDir = join(rootDir, '@t44.sh~t44~structs~WorkspaceKey')
202
+ try {
203
+ const files = await readdir(keyDir)
204
+ const keys: Array<{ name: string; did: string; createdAt?: string }> = []
205
+ for (const file of files) {
206
+ if (file.endsWith('.json')) {
207
+ try {
208
+ const data = JSON.parse(await readFile(join(keyDir, file), 'utf-8'))
209
+ keys.push({
210
+ name: file.replace(/\.json$/, ''),
211
+ did: data.did || '',
212
+ createdAt: data.createdAt,
213
+ })
214
+ } catch { }
215
+ }
216
+ }
217
+ return keys
218
+ } catch {
219
+ return []
220
+ }
221
+ }
222
+ },
223
+ getKey: {
224
+ type: CapsulePropertyTypes.Function,
225
+ value: async function (this: any, name: string): Promise<any | null> {
226
+ return this.$WorkspaceKey.get(name)
227
+ }
228
+ },
229
+ setKey: {
230
+ type: CapsulePropertyTypes.Function,
231
+ value: async function (this: any, name: string, data: { did: string; privateKey: string; createdAt: string }): Promise<string> {
232
+ return this.$WorkspaceKey.set(name, data)
233
+ }
234
+ },
235
+ getKeyPath: {
236
+ type: CapsulePropertyTypes.Function,
237
+ value: async function (this: any, name: string): Promise<string> {
238
+ return this.$WorkspaceKey.getPath(name)
239
+ }
240
+ },
241
+ keyExists: {
242
+ type: CapsulePropertyTypes.Function,
243
+ value: async function (this: any, name: string): Promise<boolean> {
244
+ return this.$WorkspaceKey.exists(name)
245
+ }
246
+ },
247
+ listRacks: {
248
+ type: CapsulePropertyTypes.Function,
249
+ value: async function (this: any): Promise<Array<{ name: string; did: string; createdAt?: string }>> {
250
+ const { readdir, readFile } = await import('fs/promises')
251
+ const { join } = await import('path')
252
+ const rootDir = await this.rootDir
253
+ const rackDir = join(rootDir, '@t44.sh~t44~structs~ProjectRack')
254
+ try {
255
+ const files = await readdir(rackDir)
256
+ const racks: Array<{ name: string; did: string; createdAt?: string }> = []
257
+ for (const file of files) {
258
+ if (file.endsWith('.json')) {
259
+ try {
260
+ const data = JSON.parse(await readFile(join(rackDir, file), 'utf-8'))
261
+ racks.push({
262
+ name: file.replace(/\.json$/, ''),
263
+ did: data.did || '',
264
+ createdAt: data.createdAt,
265
+ })
266
+ } catch { }
267
+ }
268
+ }
269
+ return racks
270
+ } catch {
271
+ return []
272
+ }
273
+ }
274
+ },
275
+ getRack: {
276
+ type: CapsulePropertyTypes.Function,
277
+ value: async function (this: any, name: string): Promise<any | null> {
278
+ return this.$ProjectRack.get(name)
279
+ }
280
+ },
281
+ setRack: {
282
+ type: CapsulePropertyTypes.Function,
283
+ value: async function (this: any, name: string, data: { did: string; privateKey: string; createdAt: string }): Promise<string> {
284
+ return this.$ProjectRack.set(name, data)
285
+ }
286
+ },
287
+ getRackPath: {
288
+ type: CapsulePropertyTypes.Function,
289
+ value: async function (this: any, name: string): Promise<string> {
290
+ return this.$ProjectRack.getPath(name)
291
+ }
292
+ },
293
+ getCatalog: {
294
+ type: CapsulePropertyTypes.Function,
295
+ value: async function (this: any, name: string): Promise<any | null> {
296
+ return this.$WorkspaceCatalogs.get(name)
297
+ }
298
+ },
299
+ setCatalog: {
300
+ type: CapsulePropertyTypes.Function,
301
+ value: async function (this: any, name: string, data: any): Promise<string> {
302
+ return this.$WorkspaceCatalogs.set(name, data)
303
+ }
304
+ },
305
+ getCatalogPath: {
306
+ type: CapsulePropertyTypes.Function,
307
+ value: async function (this: any, name: string): Promise<string> {
308
+ return this.$WorkspaceCatalogs.getPath(name)
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }, {
314
+ importMeta: import.meta,
315
+ importStack: makeImportStack(),
316
+ capsuleName: capsule['#'],
317
+ })
318
+ }
319
+ capsule['#'] = '@stream44.studio/t44/caps/HomeRegistry'
@@ -0,0 +1,144 @@
1
+
2
+ import { join, relative, dirname } from 'path'
3
+ import { readFile, writeFile, mkdir, access } from 'fs/promises'
4
+
5
+ export async function capsule({
6
+ encapsulate,
7
+ CapsulePropertyTypes,
8
+ makeImportStack
9
+ }: {
10
+ encapsulate: any
11
+ CapsulePropertyTypes: any
12
+ makeImportStack: any
13
+ }) {
14
+ return encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
17
+ '#@stream44.studio/t44/structs/HomeRegistryConfig': {
18
+ as: '$HomeRegistryConfig'
19
+ },
20
+ '#': {
21
+ JsonSchema: {
22
+ type: CapsulePropertyTypes.Mapping,
23
+ value: '@stream44.studio/t44/caps/JsonSchemas'
24
+ },
25
+ RegisterSchemas: {
26
+ type: CapsulePropertyTypes.StructInit,
27
+ value: async function (this: any): Promise<void> {
28
+ if (this.schema?.schema) {
29
+ const version = this.schemaMinorVersion || '0'
30
+ await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
31
+ }
32
+ }
33
+ },
34
+ _resolveDir: {
35
+ type: CapsulePropertyTypes.GetterFunction,
36
+ value: async function (this: any): Promise<string> {
37
+ const config = await this.$HomeRegistryConfig.config
38
+ if (!config?.rootDir) {
39
+ throw new Error('Home registry rootDir is not configured. Run ensureRootDir() first.')
40
+ }
41
+ const dirName = this.capsuleName.replace(/\//g, '~')
42
+ return join(config.rootDir, dirName)
43
+ }
44
+ },
45
+ get: {
46
+ type: CapsulePropertyTypes.Function,
47
+ value: async function (this: any, name: string): Promise<any | null> {
48
+ const filePath = join(await this._resolveDir, `${name}.json`)
49
+ let parsed: any
50
+ try {
51
+ parsed = JSON.parse(await readFile(filePath, 'utf-8'))
52
+ } catch {
53
+ return null
54
+ }
55
+
56
+ // Migrate old wrapper format: { $schema, $defs?, SchemaName: { ...data }, ... }
57
+ // to flat format: { $schema, $id, ...data }
58
+ if (parsed && parsed.$schema) {
59
+ const schemaName = this.capsuleName.split('/').pop()?.replace(/\.v\d+$/, '')
60
+ if (schemaName && parsed[schemaName] && typeof parsed[schemaName] === 'object') {
61
+ const innerData = parsed[schemaName]
62
+ // Re-write in new format with $schema and $id
63
+ const version = this.schemaMinorVersion || '0'
64
+ const output = this.schema.wrapWithSchema(innerData, this.capsuleName, version)
65
+ await writeFile(filePath, JSON.stringify(output, null, 2), { mode: 0o600 })
66
+ return innerData
67
+ }
68
+ }
69
+
70
+ // Migrate old relative $schema format to new $schema + $id format
71
+ if (parsed && parsed.$schema && !parsed.$id && typeof parsed.$schema === 'string' && parsed.$schema.includes('/')) {
72
+ const data = { ...parsed }
73
+ delete data.$schema
74
+ // Re-write in new format with $schema and $id
75
+ const version = this.schemaMinorVersion || '0'
76
+ const output = this.schema.wrapWithSchema(data, this.capsuleName, version)
77
+ await writeFile(filePath, JSON.stringify(output, null, 2), { mode: 0o600 })
78
+ return data
79
+ }
80
+
81
+ // Already schema-wrapped (new format) — return data (strip $schema and $id)
82
+ if (parsed && (parsed.$schema || parsed.$id)) {
83
+ const data = { ...parsed }
84
+ delete data.$schema
85
+ delete data.$id
86
+ return data
87
+ }
88
+
89
+ // Raw data — wrap with schema and re-write
90
+ const data = parsed
91
+ if (this.schema?.wrapWithSchema) {
92
+ const version = this.schemaMinorVersion || '0'
93
+ const output = this.schema.wrapWithSchema(data, this.capsuleName, version)
94
+ await writeFile(filePath, JSON.stringify(output, null, 2), { mode: 0o600 })
95
+ }
96
+
97
+ return data
98
+ }
99
+ },
100
+ set: {
101
+ type: CapsulePropertyTypes.Function,
102
+ value: async function (this: any, name: string, data: any): Promise<string> {
103
+ const dir = await this._resolveDir
104
+ const filePath = join(dir, `${name}.json`)
105
+ await mkdir(dir, { recursive: true })
106
+
107
+ if (this.schema?.wrapWithSchema) {
108
+ const version = this.schemaMinorVersion || '0'
109
+ const output = this.schema.wrapWithSchema(data, this.capsuleName, version)
110
+ await writeFile(filePath, JSON.stringify(output, null, 2), { mode: 0o600 })
111
+ } else {
112
+ await writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 })
113
+ }
114
+
115
+ return filePath
116
+ }
117
+ },
118
+ getPath: {
119
+ type: CapsulePropertyTypes.Function,
120
+ value: async function (this: any, name: string): Promise<string> {
121
+ return join(await this._resolveDir, `${name}.json`)
122
+ }
123
+ },
124
+ exists: {
125
+ type: CapsulePropertyTypes.Function,
126
+ value: async function (this: any, name: string): Promise<boolean> {
127
+ const filePath = join(await this._resolveDir, `${name}.json`)
128
+ try {
129
+ await access(filePath)
130
+ return true
131
+ } catch {
132
+ return false
133
+ }
134
+ }
135
+ },
136
+ }
137
+ }
138
+ }, {
139
+ importMeta: import.meta,
140
+ importStack: makeImportStack(),
141
+ capsuleName: capsule['#'],
142
+ })
143
+ }
144
+ capsule['#'] = '@stream44.studio/t44/caps/HomeRegistryFile'
@@ -0,0 +1,220 @@
1
+
2
+ import { join } from 'path'
3
+ import { mkdir, readFile, writeFile } from 'fs/promises'
4
+ import Ajv from 'ajv'
5
+ import addFormats from 'ajv-formats'
6
+ import chalk from 'chalk'
7
+
8
+ function createAjvInstance(): Ajv {
9
+ const ajv = new Ajv({
10
+ allErrors: true,
11
+ strict: false,
12
+ validateFormats: true,
13
+ logger: false
14
+ })
15
+ addFormats(ajv)
16
+ return ajv
17
+ }
18
+
19
+ export async function capsule({
20
+ encapsulate,
21
+ CapsulePropertyTypes,
22
+ makeImportStack
23
+ }: {
24
+ encapsulate: any
25
+ CapsulePropertyTypes: any
26
+ makeImportStack: any
27
+ }) {
28
+ return encapsulate({
29
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
30
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
31
+ '#': {
32
+ WorkspaceConfig: {
33
+ type: CapsulePropertyTypes.Mapping,
34
+ value: '@stream44.studio/t44/caps/WorkspaceConfig'
35
+ },
36
+ schemas: {
37
+ type: CapsulePropertyTypes.Literal,
38
+ value: {}
39
+ },
40
+ resolveDefinition: {
41
+ type: CapsulePropertyTypes.Function,
42
+ value: async function (this: any, defName: string): Promise<any | null> {
43
+ const defValue = this.schemas[defName]
44
+ if (!defValue) return null
45
+ if (typeof defValue === 'object') return defValue
46
+ return null
47
+ }
48
+ },
49
+ validate: {
50
+ type: CapsulePropertyTypes.Function,
51
+ value: async function (this: any, definitionName: string, data: any): Promise<{ warnings: any[], errors: any[] }> {
52
+ const warnings: any[] = []
53
+ const errors: any[] = []
54
+
55
+ const schema = this.schemas[definitionName]
56
+ if (!schema) {
57
+ errors.push({
58
+ type: 'schema_not_found',
59
+ message: `Definition "${definitionName}" not found in schema schemas`,
60
+ definitionName
61
+ })
62
+ return { warnings, errors }
63
+ }
64
+
65
+ const ajv = createAjvInstance()
66
+
67
+ let validate
68
+ try {
69
+ validate = ajv.compile(schema)
70
+ } catch (error: any) {
71
+ warnings.push({
72
+ type: 'schema_compilation_failed',
73
+ message: `Schema compilation failed for "${definitionName}": ${error.message}`,
74
+ definitionName,
75
+ error: error.message
76
+ })
77
+ return { warnings, errors }
78
+ }
79
+
80
+ if (!validate(data)) {
81
+ const validationErrors = validate.errors?.map((err: any) => ({
82
+ path: err.instancePath || '/',
83
+ message: err.message,
84
+ params: err.params,
85
+ keyword: err.keyword,
86
+ schemaPath: err.schemaPath
87
+ })) || []
88
+
89
+ errors.push({
90
+ type: 'validation_failed',
91
+ message: `Data does not conform to schema "${definitionName}"`,
92
+ definitionName,
93
+ validationErrors
94
+ })
95
+ }
96
+
97
+ return { warnings, errors }
98
+ }
99
+ },
100
+ registerSchema: {
101
+ type: CapsulePropertyTypes.Function,
102
+ value: async function (this: any, capsuleName: string, schema: Record<string, any>, schemaMinorVersion?: string): Promise<string | undefined> {
103
+ if (!schema || !capsuleName) {
104
+ return undefined
105
+ }
106
+
107
+ this.schemas[capsuleName] = schema
108
+
109
+ const workspaceRootDir = this.WorkspaceConfig?.workspaceRootDir
110
+ if (!workspaceRootDir) {
111
+ return undefined
112
+ }
113
+
114
+ const jsonSchemaDir = join(
115
+ workspaceRootDir,
116
+ '.~o',
117
+ 'workspace.foundation',
118
+ capsule['#'].replace(/\//g, '~')
119
+ )
120
+ await mkdir(jsonSchemaDir, { recursive: true })
121
+
122
+ const version = schemaMinorVersion || '0'
123
+ const schemaFilename = capsuleName.replace(/\//g, '~') + '.json'
124
+ const schemaFilePath = join(jsonSchemaDir, schemaFilename)
125
+
126
+ const schemaOutput: Record<string, any> = {
127
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
128
+ $id: capsuleName + '.v' + version,
129
+ ...schema
130
+ }
131
+
132
+ const schemaJson = JSON.stringify(schemaOutput, null, 4)
133
+ const existing = await readFile(schemaFilePath, 'utf-8').catch(() => null)
134
+ if (existing !== schemaJson) {
135
+ await writeFile(schemaFilePath, schemaJson)
136
+ }
137
+ return schemaFilePath
138
+ }
139
+ },
140
+ formatValidationFeedback: {
141
+ type: CapsulePropertyTypes.Function,
142
+ value: function (this: any, feedback: { warnings: any[], errors: any[] }, context: { filePath?: string, schemaRef?: string }): string {
143
+ const lines: string[] = [
144
+ '',
145
+ chalk.bold.red('✗ Schema Validation Failed'),
146
+ ''
147
+ ]
148
+
149
+ if (context.filePath) {
150
+ lines.push(
151
+ chalk.gray(' File:'),
152
+ chalk.yellow(` ${context.filePath}`),
153
+ ''
154
+ )
155
+ }
156
+
157
+ if (context.schemaRef) {
158
+ lines.push(
159
+ chalk.gray(' Schema:'),
160
+ chalk.cyan(` ${context.schemaRef}`),
161
+ ''
162
+ )
163
+ }
164
+
165
+ if (feedback.errors.length > 0) {
166
+ lines.push(
167
+ chalk.gray(' Errors:'),
168
+ ...feedback.errors.map((e: any) => chalk.red(` • ${e.message || e}`)),
169
+ ''
170
+ )
171
+ }
172
+
173
+ if (feedback.warnings.length > 0) {
174
+ lines.push(
175
+ chalk.gray(' Warnings:'),
176
+ ...feedback.warnings.map((w: any) => chalk.yellow(` • ${w.message || w}`)),
177
+ ''
178
+ )
179
+ }
180
+
181
+ return lines.join('\n')
182
+ }
183
+ },
184
+ resolveSchemaFilePath: {
185
+ type: CapsulePropertyTypes.Function,
186
+ value: function (this: any, capsuleName: string): string | undefined {
187
+ const workspaceRootDir = this.WorkspaceConfig?.workspaceRootDir
188
+ if (!workspaceRootDir) return undefined
189
+ const jsonSchemaDir = join(
190
+ workspaceRootDir,
191
+ '.~o',
192
+ 'workspace.foundation',
193
+ capsule['#'].replace(/\//g, '~')
194
+ )
195
+ const schemaFilename = capsuleName.replace(/\//g, '~') + '.json'
196
+ return join(jsonSchemaDir, schemaFilename)
197
+ }
198
+ },
199
+ wrapWithSchema: {
200
+ type: CapsulePropertyTypes.Function,
201
+ value: function (this: any, data: any, capsuleName: string, version?: string): Record<string, any> {
202
+ const output: Record<string, any> = {}
203
+ // Use standard JSON Schema URL for $schema
204
+ output.$schema = 'https://json-schema.org/draft/2020-12/schema'
205
+ // Use entity identifier with version for $id
206
+ const versionSuffix = version ? `.v${version}` : '.v0'
207
+ output.$id = `${capsuleName}${versionSuffix}`
208
+ Object.assign(output, data)
209
+ return output
210
+ }
211
+ },
212
+ }
213
+ }
214
+ }, {
215
+ importMeta: import.meta,
216
+ importStack: makeImportStack(),
217
+ capsuleName: capsule['#'],
218
+ })
219
+ }
220
+ capsule['#'] = '@stream44.studio/t44/caps/JsonSchemas'