t44 0.2.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of t44 might be problematic. Click here for more details.
- package/LICENSE.md +203 -0
- package/README.md +154 -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/HomeRegistry.v0.ts +298 -0
- package/caps/OpenApiSchema.v0.ts +192 -0
- package/caps/ProjectDeployment.v0.ts +363 -0
- package/caps/ProjectDevelopment.v0.ts +246 -0
- package/caps/ProjectPublishing.v0.ts +307 -0
- package/caps/ProjectRack.v0.ts +128 -0
- package/caps/WorkspaceCli.v0.ts +391 -0
- package/caps/WorkspaceConfig.v0.ts +626 -0
- package/caps/WorkspaceConfig.yaml +53 -0
- package/caps/WorkspaceConnection.v0.ts +240 -0
- package/caps/WorkspaceEntityConfig.v0.ts +64 -0
- package/caps/WorkspaceEntityFact.v0.ts +193 -0
- package/caps/WorkspaceInfo.v0.ts +554 -0
- package/caps/WorkspaceInit.v0.ts +30 -0
- package/caps/WorkspaceKey.v0.ts +186 -0
- package/caps/WorkspaceProjects.v0.ts +455 -0
- package/caps/WorkspacePrompt.v0.ts +396 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.v0.ts +104 -0
- package/caps/WorkspaceShell.yaml +65 -0
- package/caps/WorkspaceShellCli.v0.ts +109 -0
- package/caps/WorkspaceTest.v0.ts +167 -0
- package/caps/providers/LICENSE.md +8 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.v0.ts +328 -0
- package/caps/providers/bunny.net/api-pull.v0.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.v0.ts +161 -0
- package/caps/providers/bunny.net/api-storage.v0.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.v0.ts +245 -0
- package/caps/providers/bunny.net/api.v0.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.v0.ts +207 -0
- package/caps/providers/dynadot.com/api-domains.v0.test.ts +147 -0
- package/caps/providers/dynadot.com/api-domains.v0.ts +137 -0
- package/caps/providers/dynadot.com/api.v0.ts +88 -0
- package/caps/providers/git-scm.com/ProjectPublishing.v0.ts +231 -0
- package/caps/providers/github.com/ProjectPublishing.v0.ts +75 -0
- package/caps/providers/github.com/api.v0.ts +90 -0
- package/caps/providers/npmjs.com/ProjectPublishing.v0.ts +741 -0
- package/caps/providers/vercel.com/ProjectDeployment.v0.ts +339 -0
- package/caps/providers/vercel.com/api.v0.test.ts +67 -0
- package/caps/providers/vercel.com/api.v0.ts +132 -0
- package/caps/providers/vercel.com/bun.lock +194 -0
- package/caps/providers/vercel.com/package.json +10 -0
- package/caps/providers/vercel.com/project.v0.test.ts +108 -0
- package/caps/providers/vercel.com/project.v0.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +189 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/openapi.ts +132 -0
- package/lib/ucan.ts +137 -0
- package/package.json +41 -0
- package/structs/HomeRegistryConfig.v0.ts +27 -0
- package/structs/ProjectDeploymentConfig.v0.ts +27 -0
- package/structs/ProjectDeploymentFact.v0.ts +110 -0
- package/structs/ProjectPublishingFact.v0.ts +69 -0
- package/structs/ProjectRackConfig.v0.ts +27 -0
- package/structs/WorkspaceCliConfig.v0.ts +27 -0
- package/structs/WorkspaceConfig.v0.ts +27 -0
- package/structs/WorkspaceKeyConfig.v0.ts +27 -0
- package/structs/WorkspaceMappings.v0.ts +27 -0
- package/structs/WorkspaceProjectsConfig.v0.ts +27 -0
- package/structs/WorkspaceRepositories.v0.ts +27 -0
- package/structs/WorkspaceShellConfig.v0.ts +45 -0
- package/structs/providers/LICENSE.md +8 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/ProjectDeploymentFact.v0.ts +41 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.v0.ts +42 -0
- package/structs/providers/dynadot.com/DomainFact.v0.ts +146 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0.ts +41 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.v0.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.v0.ts +52 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.v0.ts +42 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.v0.ts +48 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.v0.ts +38 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.v0.ts +48 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +5 -0
|
@@ -0,0 +1,298 @@
|
|
|
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.v0': {},
|
|
13
|
+
'#t44/structs/HomeRegistryConfig.v0': {
|
|
14
|
+
as: '$HomeRegistryConfig'
|
|
15
|
+
},
|
|
16
|
+
'#': {
|
|
17
|
+
WorkspacePrompt: {
|
|
18
|
+
type: CapsulePropertyTypes.Mapping,
|
|
19
|
+
value: 't44/caps/WorkspacePrompt.v0'
|
|
20
|
+
},
|
|
21
|
+
rootDir: {
|
|
22
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
23
|
+
value: async function (this: any): Promise<string> {
|
|
24
|
+
const config = await this.$HomeRegistryConfig.config
|
|
25
|
+
if (!config?.rootDir) {
|
|
26
|
+
throw new Error('Home registry rootDir is not configured. Run ensureRootDir() first.')
|
|
27
|
+
}
|
|
28
|
+
return config.rootDir
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
ensureRootDir: {
|
|
32
|
+
type: CapsulePropertyTypes.Function,
|
|
33
|
+
value: async function (this: any): Promise<string> {
|
|
34
|
+
const { join } = await import('path')
|
|
35
|
+
const { stat, mkdir, readFile, writeFile } = await import('fs/promises')
|
|
36
|
+
const { homedir } = await import('os')
|
|
37
|
+
const chalk = (await import('chalk')).default
|
|
38
|
+
|
|
39
|
+
const config = await this.$HomeRegistryConfig.config
|
|
40
|
+
const defaultDir = join(homedir(), '.o/workspace.foundation')
|
|
41
|
+
|
|
42
|
+
let chosenDir: string
|
|
43
|
+
|
|
44
|
+
if (config?.rootDir) {
|
|
45
|
+
// Validate that the directory exists
|
|
46
|
+
try {
|
|
47
|
+
const s = await stat(config.rootDir)
|
|
48
|
+
if (!s.isDirectory()) {
|
|
49
|
+
throw new Error(`Home registry path '${config.rootDir}' exists but is not a directory.`)
|
|
50
|
+
}
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
if (error.code === 'ENOENT') {
|
|
53
|
+
throw new Error(`Home registry directory '${config.rootDir}' does not exist. Please create it or reconfigure.`)
|
|
54
|
+
}
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
chosenDir = config.rootDir
|
|
58
|
+
} else {
|
|
59
|
+
// rootDir not set — prompt user
|
|
60
|
+
console.log(chalk.cyan(`\n🏠 Home Registry Setup\n`))
|
|
61
|
+
console.log(chalk.gray(' The home registry is the place in your home directory that keeps'))
|
|
62
|
+
console.log(chalk.gray(' details about your workspaces and projects.'))
|
|
63
|
+
console.log('')
|
|
64
|
+
|
|
65
|
+
chosenDir = await this.WorkspacePrompt.input({
|
|
66
|
+
message: 'Enter the home registry directory:',
|
|
67
|
+
defaultValue: defaultDir,
|
|
68
|
+
validate: (input: string) => {
|
|
69
|
+
if (!input || input.trim().length === 0) {
|
|
70
|
+
return 'Directory path cannot be empty'
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Check if directory exists
|
|
77
|
+
let dirExists = false
|
|
78
|
+
try {
|
|
79
|
+
const s = await stat(chosenDir)
|
|
80
|
+
dirExists = s.isDirectory()
|
|
81
|
+
} catch {
|
|
82
|
+
// Does not exist
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (dirExists) {
|
|
86
|
+
const confirmed = await this.WorkspacePrompt.confirm({
|
|
87
|
+
message: `Directory '${chosenDir}' already exists. Use this existing registry as the home registry for your workspace?`,
|
|
88
|
+
defaultValue: true
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (!confirmed) {
|
|
92
|
+
console.log(chalk.red('\n\nABORTED\n'))
|
|
93
|
+
process.exit(0)
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Create the directory
|
|
97
|
+
await mkdir(chosenDir, { recursive: true })
|
|
98
|
+
console.log(chalk.green(`\n ✓ Created home registry directory: ${chosenDir}\n`))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await this.$HomeRegistryConfig.setConfigValue(['rootDir'], chosenDir)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Ensure registry.json exists at the root of the home registry
|
|
105
|
+
const registryFilePath = join(chosenDir, 'registry.json')
|
|
106
|
+
let registryData: any
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
registryData = JSON.parse(await readFile(registryFilePath, 'utf-8'))
|
|
110
|
+
} catch {
|
|
111
|
+
// registry.json does not exist — generate identity
|
|
112
|
+
console.log(chalk.cyan(`\n Generating home registry identity...\n`))
|
|
113
|
+
|
|
114
|
+
const { generateKeypair } = await import('../lib/ucan.js')
|
|
115
|
+
const { did, privateKey } = await generateKeypair()
|
|
116
|
+
|
|
117
|
+
registryData = {
|
|
118
|
+
did,
|
|
119
|
+
privateKey,
|
|
120
|
+
createdAt: new Date().toISOString()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await writeFile(registryFilePath, JSON.stringify(registryData, null, 2), { mode: 0o600 })
|
|
124
|
+
|
|
125
|
+
console.log(chalk.green(` ✓ Registry identity saved to:`))
|
|
126
|
+
console.log(chalk.green(` ${registryFilePath}`))
|
|
127
|
+
console.log(chalk.green(` ✓ DID: ${registryData.did}\n`))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Ensure identifier is set in config
|
|
131
|
+
if (!config?.identifier || config.identifier !== registryData.did) {
|
|
132
|
+
await this.$HomeRegistryConfig.setConfigValue(['identifier'], registryData.did)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return chosenDir
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
getRegistryPath: {
|
|
139
|
+
type: CapsulePropertyTypes.Function,
|
|
140
|
+
value: async function (this: any): Promise<string> {
|
|
141
|
+
const { join } = await import('path')
|
|
142
|
+
const rootDir = await this.rootDir
|
|
143
|
+
return join(rootDir, 'registry.json')
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
getRegistry: {
|
|
147
|
+
type: CapsulePropertyTypes.Function,
|
|
148
|
+
value: async function (this: any): Promise<any | null> {
|
|
149
|
+
const { join } = await import('path')
|
|
150
|
+
const { readFile } = await import('fs/promises')
|
|
151
|
+
const rootDir = await this.rootDir
|
|
152
|
+
const filePath = join(rootDir, 'registry.json')
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(await readFile(filePath, 'utf-8'))
|
|
155
|
+
} catch {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
getWorkspace: {
|
|
161
|
+
type: CapsulePropertyTypes.Function,
|
|
162
|
+
value: async function (this: any, name: string): Promise<any | null> {
|
|
163
|
+
const { join } = await import('path')
|
|
164
|
+
const { readFile } = await import('fs/promises')
|
|
165
|
+
|
|
166
|
+
const rootDir = await this.rootDir
|
|
167
|
+
const filePath = join(rootDir, 'workspaces', `${name}.json`)
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(await readFile(filePath, 'utf-8'))
|
|
170
|
+
} catch {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
setWorkspace: {
|
|
176
|
+
type: CapsulePropertyTypes.Function,
|
|
177
|
+
value: async function (this: any, name: string, data: { did: string; privateKey: string; createdAt: string; workspaceRootDir: string }): Promise<string> {
|
|
178
|
+
const { join } = await import('path')
|
|
179
|
+
const { writeFile, mkdir } = await import('fs/promises')
|
|
180
|
+
|
|
181
|
+
const rootDir = await this.rootDir
|
|
182
|
+
const dir = join(rootDir, 'workspaces')
|
|
183
|
+
const filePath = join(dir, `${name}.json`)
|
|
184
|
+
await mkdir(dir, { recursive: true })
|
|
185
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 })
|
|
186
|
+
return filePath
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
getWorkspacePath: {
|
|
190
|
+
type: CapsulePropertyTypes.Function,
|
|
191
|
+
value: async function (this: any, name: string): Promise<string> {
|
|
192
|
+
const { join } = await import('path')
|
|
193
|
+
|
|
194
|
+
const rootDir = await this.rootDir
|
|
195
|
+
return join(rootDir, 'workspaces', `${name}.json`)
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
getKey: {
|
|
199
|
+
type: CapsulePropertyTypes.Function,
|
|
200
|
+
value: async function (this: any, name: string): Promise<any | null> {
|
|
201
|
+
const { join } = await import('path')
|
|
202
|
+
const { readFile } = await import('fs/promises')
|
|
203
|
+
|
|
204
|
+
const rootDir = await this.rootDir
|
|
205
|
+
const filePath = join(rootDir, 'workspace-keys', `${name}.json`)
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(await readFile(filePath, 'utf-8'))
|
|
208
|
+
} catch {
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
setKey: {
|
|
214
|
+
type: CapsulePropertyTypes.Function,
|
|
215
|
+
value: async function (this: any, name: string, data: { did: string; privateKey: string; createdAt: string }): Promise<string> {
|
|
216
|
+
const { join } = await import('path')
|
|
217
|
+
const { writeFile, mkdir } = await import('fs/promises')
|
|
218
|
+
|
|
219
|
+
const rootDir = await this.rootDir
|
|
220
|
+
const dir = join(rootDir, 'workspace-keys')
|
|
221
|
+
const filePath = join(dir, `${name}.json`)
|
|
222
|
+
await mkdir(dir, { recursive: true })
|
|
223
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 })
|
|
224
|
+
return filePath
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
getKeyPath: {
|
|
228
|
+
type: CapsulePropertyTypes.Function,
|
|
229
|
+
value: async function (this: any, name: string): Promise<string> {
|
|
230
|
+
const { join } = await import('path')
|
|
231
|
+
|
|
232
|
+
const rootDir = await this.rootDir
|
|
233
|
+
return join(rootDir, 'workspace-keys', `${name}.json`)
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
keyExists: {
|
|
237
|
+
type: CapsulePropertyTypes.Function,
|
|
238
|
+
value: async function (this: any, name: string): Promise<boolean> {
|
|
239
|
+
const { join } = await import('path')
|
|
240
|
+
const { access } = await import('fs/promises')
|
|
241
|
+
|
|
242
|
+
const rootDir = await this.rootDir
|
|
243
|
+
const filePath = join(rootDir, 'workspace-keys', `${name}.json`)
|
|
244
|
+
try {
|
|
245
|
+
await access(filePath)
|
|
246
|
+
return true
|
|
247
|
+
} catch {
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
getRack: {
|
|
253
|
+
type: CapsulePropertyTypes.Function,
|
|
254
|
+
value: async function (this: any, name: string): Promise<any | null> {
|
|
255
|
+
const { join } = await import('path')
|
|
256
|
+
const { readFile } = await import('fs/promises')
|
|
257
|
+
|
|
258
|
+
const rootDir = await this.rootDir
|
|
259
|
+
const filePath = join(rootDir, 'project-racks', `${name}.json`)
|
|
260
|
+
try {
|
|
261
|
+
return JSON.parse(await readFile(filePath, 'utf-8'))
|
|
262
|
+
} catch {
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
setRack: {
|
|
268
|
+
type: CapsulePropertyTypes.Function,
|
|
269
|
+
value: async function (this: any, name: string, data: { did: string; privateKey: string; createdAt: string }): Promise<string> {
|
|
270
|
+
const { join } = await import('path')
|
|
271
|
+
const { writeFile, mkdir } = await import('fs/promises')
|
|
272
|
+
|
|
273
|
+
const rootDir = await this.rootDir
|
|
274
|
+
const dir = join(rootDir, 'project-racks')
|
|
275
|
+
const filePath = join(dir, `${name}.json`)
|
|
276
|
+
await mkdir(dir, { recursive: true })
|
|
277
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 })
|
|
278
|
+
return filePath
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
getRackPath: {
|
|
282
|
+
type: CapsulePropertyTypes.Function,
|
|
283
|
+
value: async function (this: any, name: string): Promise<string> {
|
|
284
|
+
const { join } = await import('path')
|
|
285
|
+
|
|
286
|
+
const rootDir = await this.rootDir
|
|
287
|
+
return join(rootDir, 'project-racks', `${name}.json`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}, {
|
|
293
|
+
importMeta: import.meta,
|
|
294
|
+
importStack: makeImportStack(),
|
|
295
|
+
capsuleName: capsule['#'],
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
capsule['#'] = 't44/caps/HomeRegistry.v0'
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { convertOpenApiToJsonSchema, createAjvInstance } from '../lib/openapi.js'
|
|
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.v0': {},
|
|
17
|
+
'#': {
|
|
18
|
+
WorkspaceConfig: {
|
|
19
|
+
type: CapsulePropertyTypes.Mapping,
|
|
20
|
+
value: 't44/caps/WorkspaceConfig.v0'
|
|
21
|
+
},
|
|
22
|
+
url: {
|
|
23
|
+
type: CapsulePropertyTypes.Literal,
|
|
24
|
+
value: undefined,
|
|
25
|
+
},
|
|
26
|
+
definitions: {
|
|
27
|
+
type: CapsulePropertyTypes.Literal,
|
|
28
|
+
value: {}
|
|
29
|
+
},
|
|
30
|
+
resolveDefinition: {
|
|
31
|
+
type: CapsulePropertyTypes.Function,
|
|
32
|
+
value: async function (this: any, defName: string): Promise<any | null> {
|
|
33
|
+
const defValue = this.definitions[defName]
|
|
34
|
+
if (!defValue) return null
|
|
35
|
+
|
|
36
|
+
if (typeof defValue === 'object') {
|
|
37
|
+
return defValue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof defValue === 'string' && this.url) {
|
|
41
|
+
const cacheDir = join(
|
|
42
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
43
|
+
'.~o',
|
|
44
|
+
'workspace.foundation',
|
|
45
|
+
'WorkspaceEntityFacts',
|
|
46
|
+
'OpenApiSchemas',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
await mkdir(cacheDir, { recursive: true })
|
|
50
|
+
|
|
51
|
+
const schemaFile = join(cacheDir, this.url.replace(/\//g, '~'))
|
|
52
|
+
|
|
53
|
+
let openApiSpec: any
|
|
54
|
+
try {
|
|
55
|
+
const cached = await readFile(schemaFile, 'utf-8')
|
|
56
|
+
openApiSpec = JSON.parse(cached)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const response = await fetch(this.url)
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
openApiSpec = await response.json()
|
|
63
|
+
await writeFile(schemaFile, JSON.stringify(openApiSpec, null, 2))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const refParts = defValue.replace(/^#\//, '').split('/')
|
|
67
|
+
let schema = openApiSpec
|
|
68
|
+
for (const part of refParts) {
|
|
69
|
+
const decodedPart = part.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
70
|
+
schema = schema[decodedPart]
|
|
71
|
+
if (!schema) return null
|
|
72
|
+
}
|
|
73
|
+
return schema
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
validate: {
|
|
80
|
+
type: CapsulePropertyTypes.Function,
|
|
81
|
+
value: async function (this: any, definitionName: string, data: any): Promise<{ warnings: any[], errors: any[] }> {
|
|
82
|
+
const warnings: any[] = []
|
|
83
|
+
const errors: any[] = []
|
|
84
|
+
|
|
85
|
+
const schemaRef = this.definitions[definitionName]
|
|
86
|
+
if (!schemaRef) {
|
|
87
|
+
errors.push({
|
|
88
|
+
type: 'schema_not_found',
|
|
89
|
+
message: `Definition "${definitionName}" not found in schema definitions`,
|
|
90
|
+
definitionName
|
|
91
|
+
})
|
|
92
|
+
return { warnings, errors }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let schema: any
|
|
96
|
+
let openApiSpec: any
|
|
97
|
+
|
|
98
|
+
if (typeof schemaRef === 'object') {
|
|
99
|
+
schema = schemaRef
|
|
100
|
+
openApiSpec = { definitions: this.definitions }
|
|
101
|
+
} else {
|
|
102
|
+
const cacheDir = join(
|
|
103
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
104
|
+
'.~o',
|
|
105
|
+
'workspace.foundation',
|
|
106
|
+
'WorkspaceEntityFacts',
|
|
107
|
+
'OpenApiSchemas',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await mkdir(cacheDir, { recursive: true })
|
|
111
|
+
|
|
112
|
+
const schemaFile = join(cacheDir, this.url.replace(/\//g, '~'))
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const cached = await readFile(schemaFile, 'utf-8')
|
|
116
|
+
openApiSpec = JSON.parse(cached)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const response = await fetch(this.url)
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
errors.push({
|
|
121
|
+
type: 'fetch_failed',
|
|
122
|
+
message: `Failed to fetch OpenAPI spec from ${this.url}: ${response.statusText}`,
|
|
123
|
+
url: this.url
|
|
124
|
+
})
|
|
125
|
+
return { warnings, errors }
|
|
126
|
+
}
|
|
127
|
+
openApiSpec = await response.json()
|
|
128
|
+
await writeFile(schemaFile, JSON.stringify(openApiSpec, null, 2))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const refParts = schemaRef.replace(/^#\//, '').split('/')
|
|
132
|
+
schema = openApiSpec
|
|
133
|
+
for (const part of refParts) {
|
|
134
|
+
const decodedPart = part.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
135
|
+
schema = schema[decodedPart]
|
|
136
|
+
if (!schema) {
|
|
137
|
+
errors.push({
|
|
138
|
+
type: 'schema_reference_not_found',
|
|
139
|
+
message: `Schema reference "${schemaRef}" not found in OpenAPI spec`,
|
|
140
|
+
schemaRef
|
|
141
|
+
})
|
|
142
|
+
return { warnings, errors }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const ajv = createAjvInstance()
|
|
148
|
+
|
|
149
|
+
const jsonSchema = convertOpenApiToJsonSchema(schema, openApiSpec, warnings)
|
|
150
|
+
|
|
151
|
+
let validate
|
|
152
|
+
try {
|
|
153
|
+
validate = ajv.compile(jsonSchema)
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
warnings.push({
|
|
156
|
+
type: 'schema_compilation_failed',
|
|
157
|
+
message: `Schema compilation failed for "${definitionName}": ${error.message}`,
|
|
158
|
+
definitionName,
|
|
159
|
+
error: error.message
|
|
160
|
+
})
|
|
161
|
+
return { warnings, errors }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!validate(data)) {
|
|
165
|
+
const validationErrors = validate.errors?.map(err => ({
|
|
166
|
+
path: err.instancePath || '/',
|
|
167
|
+
message: err.message,
|
|
168
|
+
params: err.params,
|
|
169
|
+
keyword: err.keyword,
|
|
170
|
+
schemaPath: err.schemaPath
|
|
171
|
+
})) || []
|
|
172
|
+
|
|
173
|
+
errors.push({
|
|
174
|
+
type: 'validation_failed',
|
|
175
|
+
message: `Data does not conform to schema "${definitionName}"`,
|
|
176
|
+
definitionName,
|
|
177
|
+
validationErrors
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { warnings, errors }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}, {
|
|
187
|
+
importMeta: import.meta,
|
|
188
|
+
importStack: makeImportStack(),
|
|
189
|
+
capsuleName: capsule['#'],
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
capsule['#'] = 't44/caps/OpenApiSchema.v0'
|