@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,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'
|