t44 0.4.0-rc.3
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.yml +12 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +203 -0
- package/README.md +183 -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 +51 -0
- package/caps/HomeRegistry.ts +313 -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 +363 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +522 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +322 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/WorkspaceCli.ts +442 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +71 -0
- package/caps/WorkspaceConfigFile.ts +799 -0
- package/caps/WorkspaceConnection.ts +249 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +595 -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 +406 -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/WorkspaceTest.ts +167 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
- package/caps/providers/bunny.net/api-pull.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.ts +164 -0
- package/caps/providers/bunny.net/api-storage.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.ts +248 -0
- package/caps/providers/bunny.net/api.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
- package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
- package/caps/providers/dynadot.com/api-domains.ts +169 -0
- package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
- package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
- package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
- package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
- package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
- package/caps/providers/github.com/ProjectPublishing.ts +118 -0
- package/caps/providers/github.com/api.ts +115 -0
- package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
- package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
- package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
- package/caps/providers/vercel.com/api.test.ts +67 -0
- package/caps/providers/vercel.com/api.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.test.ts +108 -0
- package/caps/providers/vercel.com/project.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +365 -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 +101 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +56 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingFact.ts +68 -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/WorkspacePublishingConfig.ts +65 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
- package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/dynadot.com/DomainFact.ts +46 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
- package/tests/01-Lifecycle/main.test.ts +173 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +3 -0
|
@@ -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: '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['#'] = 't44/caps/JsonSchemas'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
export async function capsule({
|
|
5
|
+
encapsulate,
|
|
6
|
+
CapsulePropertyTypes,
|
|
7
|
+
makeImportStack
|
|
8
|
+
}: {
|
|
9
|
+
encapsulate: any
|
|
10
|
+
CapsulePropertyTypes: any
|
|
11
|
+
makeImportStack: any
|
|
12
|
+
}) {
|
|
13
|
+
return encapsulate({
|
|
14
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
15
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
16
|
+
'#': {
|
|
17
|
+
WorkspaceConfig: {
|
|
18
|
+
type: CapsulePropertyTypes.Mapping,
|
|
19
|
+
value: 't44/caps/WorkspaceConfig'
|
|
20
|
+
},
|
|
21
|
+
url: {
|
|
22
|
+
type: CapsulePropertyTypes.Literal,
|
|
23
|
+
value: undefined,
|
|
24
|
+
},
|
|
25
|
+
schemas: {
|
|
26
|
+
type: CapsulePropertyTypes.Literal,
|
|
27
|
+
value: {}
|
|
28
|
+
},
|
|
29
|
+
ensureCached: {
|
|
30
|
+
type: CapsulePropertyTypes.Function,
|
|
31
|
+
value: async function (this: any): Promise<any | null> {
|
|
32
|
+
if (!this.url) return null
|
|
33
|
+
|
|
34
|
+
const cacheDir = join(
|
|
35
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
36
|
+
'.~o',
|
|
37
|
+
'workspace.foundation',
|
|
38
|
+
'OpenApiSchemas',
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
await mkdir(cacheDir, { recursive: true })
|
|
42
|
+
|
|
43
|
+
const schemaFile = join(cacheDir, this.url.replace(/\//g, '~'))
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const cached = await readFile(schemaFile, 'utf-8')
|
|
47
|
+
return JSON.parse(cached)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const response = await fetch(this.url)
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
const openApiSpec = await response.json()
|
|
54
|
+
await writeFile(schemaFile, JSON.stringify(openApiSpec, null, 2))
|
|
55
|
+
return openApiSpec
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}, {
|
|
62
|
+
importMeta: import.meta,
|
|
63
|
+
importStack: makeImportStack(),
|
|
64
|
+
capsuleName: capsule['#'],
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
capsule['#'] = 't44/caps/OpenApiSchema'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { readFile, writeFile } 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
|
+
'#': {
|
|
18
|
+
JsonSchema: {
|
|
19
|
+
type: CapsulePropertyTypes.Mapping,
|
|
20
|
+
value: 't44/caps/JsonSchemas'
|
|
21
|
+
},
|
|
22
|
+
RegisterSchemas: {
|
|
23
|
+
type: CapsulePropertyTypes.StructInit,
|
|
24
|
+
value: async function (this: any): Promise<void> {
|
|
25
|
+
if (this.schema?.schema) {
|
|
26
|
+
const version = this.schemaMinorVersion || '0'
|
|
27
|
+
await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
_readPackageJson: {
|
|
32
|
+
type: CapsulePropertyTypes.Function,
|
|
33
|
+
value: async function (this: any, packageJsonPath: string): Promise<any> {
|
|
34
|
+
const content = await readFile(packageJsonPath, 'utf-8')
|
|
35
|
+
return JSON.parse(content)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
_writePackageJson: {
|
|
39
|
+
type: CapsulePropertyTypes.Function,
|
|
40
|
+
value: async function (this: any, packageJsonPath: string, pkg: any): Promise<void> {
|
|
41
|
+
await writeFile(packageJsonPath, JSON.stringify(pkg, null, 4) + '\n')
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
_resolveConfigSection: {
|
|
45
|
+
type: CapsulePropertyTypes.Function,
|
|
46
|
+
value: function (this: any, pkg: any): any {
|
|
47
|
+
const structKey = this.capsuleName
|
|
48
|
+
if (!pkg.config) return undefined
|
|
49
|
+
if (!pkg.config.o) return undefined
|
|
50
|
+
if (!pkg.config.o['workspace.foundation']) return undefined
|
|
51
|
+
return pkg.config.o['workspace.foundation'][structKey] || undefined
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
get: {
|
|
55
|
+
type: CapsulePropertyTypes.Function,
|
|
56
|
+
value: async function (this: any, packageJsonPath: string): Promise<any | null> {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = await this._readPackageJson(packageJsonPath)
|
|
59
|
+
return this._resolveConfigSection(pkg) || null
|
|
60
|
+
} catch {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
set: {
|
|
66
|
+
type: CapsulePropertyTypes.Function,
|
|
67
|
+
value: async function (this: any, packageJsonPath: string, data: any): Promise<void> {
|
|
68
|
+
const pkg = await this._readPackageJson(packageJsonPath)
|
|
69
|
+
const structKey = this.capsuleName
|
|
70
|
+
|
|
71
|
+
if (!pkg.config) pkg.config = {}
|
|
72
|
+
if (!pkg.config.o) pkg.config.o = {}
|
|
73
|
+
if (!pkg.config.o['workspace.foundation']) pkg.config.o['workspace.foundation'] = {}
|
|
74
|
+
|
|
75
|
+
pkg.config.o['workspace.foundation'][structKey] = data
|
|
76
|
+
|
|
77
|
+
await this._writePackageJson(packageJsonPath, pkg)
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}, {
|
|
83
|
+
importMeta: import.meta,
|
|
84
|
+
importStack: makeImportStack(),
|
|
85
|
+
capsuleName: capsule['#'],
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
capsule['#'] = 't44/caps/PackageDescriptor'
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
|
|
2
|
+
export async function capsule({
|
|
3
|
+
encapsulate,
|
|
4
|
+
CapsulePropertyTypes,
|
|
5
|
+
makeImportStack
|
|
6
|
+
}: {
|
|
7
|
+
encapsulate: any
|
|
8
|
+
CapsulePropertyTypes: any
|
|
9
|
+
makeImportStack: any
|
|
10
|
+
}) {
|
|
11
|
+
return encapsulate({
|
|
12
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
13
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
14
|
+
'#t44/structs/ProjectCatalogsConfig': {
|
|
15
|
+
as: '$ProjectCatalogsConfig',
|
|
16
|
+
},
|
|
17
|
+
'#': {
|
|
18
|
+
HomeRegistry: {
|
|
19
|
+
type: CapsulePropertyTypes.Mapping,
|
|
20
|
+
value: 't44/caps/HomeRegistry'
|
|
21
|
+
},
|
|
22
|
+
list: {
|
|
23
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
24
|
+
value: async function (this: any): Promise<Record<string, any>> {
|
|
25
|
+
const catalogsConfig = await this.$ProjectCatalogsConfig.config
|
|
26
|
+
return catalogsConfig?.catalogs || {}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
validate: {
|
|
30
|
+
type: CapsulePropertyTypes.Function,
|
|
31
|
+
value: async function (this: any): Promise<boolean> {
|
|
32
|
+
const catalogsConfig = await this.$ProjectCatalogsConfig.config
|
|
33
|
+
const catalogs = catalogsConfig?.catalogs
|
|
34
|
+
|
|
35
|
+
if (!catalogs || typeof catalogs !== 'object') {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const chalk = (await import('chalk')).default
|
|
40
|
+
|
|
41
|
+
for (const [catalogName, catalogConfig] of Object.entries(catalogs)) {
|
|
42
|
+
if (!catalogConfig || typeof catalogConfig !== 'object') {
|
|
43
|
+
console.log(chalk.red(`\nā Invalid catalog '${catalogName}': must be an object.\n`))
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fileName = catalogName.replace(/\//g, '~')
|
|
48
|
+
const typedConfig = catalogConfig as Record<string, any>
|
|
49
|
+
|
|
50
|
+
const existing = await this.HomeRegistry.getCatalog(fileName) || {}
|
|
51
|
+
const now = new Date().toISOString()
|
|
52
|
+
|
|
53
|
+
// Deep merge: config keys into existing, preserving existing nested data
|
|
54
|
+
let changed = false
|
|
55
|
+
for (const [key, value] of Object.entries(typedConfig)) {
|
|
56
|
+
if (key === 'repositories' && typeof value === 'object' && typeof existing[key] === 'object') {
|
|
57
|
+
// Merge repositories: add missing repos, keep existing repo data
|
|
58
|
+
for (const [repoName, repoConfig] of Object.entries(value as Record<string, any>)) {
|
|
59
|
+
if (!existing[key][repoName]) {
|
|
60
|
+
existing[key][repoName] = repoConfig || {}
|
|
61
|
+
changed = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else if (JSON.stringify(existing[key]) !== JSON.stringify(value)) {
|
|
65
|
+
existing[key] = value
|
|
66
|
+
changed = true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (changed) {
|
|
71
|
+
existing.updatedAt = now
|
|
72
|
+
}
|
|
73
|
+
if (!existing.createdAt) {
|
|
74
|
+
existing.createdAt = now
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const filePath = await this.HomeRegistry.setCatalog(fileName, existing)
|
|
78
|
+
if (changed) {
|
|
79
|
+
console.log(chalk.green(` ā Catalog '${catalogName}' ā ${filePath}`))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
updateCatalogRepository: {
|
|
87
|
+
type: CapsulePropertyTypes.Function,
|
|
88
|
+
value: async function (this: any, {
|
|
89
|
+
repoName,
|
|
90
|
+
providerKey,
|
|
91
|
+
providerData,
|
|
92
|
+
}: {
|
|
93
|
+
repoName: string
|
|
94
|
+
providerKey: string
|
|
95
|
+
providerData: Record<string, any>
|
|
96
|
+
}): Promise<void> {
|
|
97
|
+
const catalogsConfig = await this.$ProjectCatalogsConfig.config
|
|
98
|
+
const catalogs = catalogsConfig?.catalogs
|
|
99
|
+
|
|
100
|
+
if (!catalogs || typeof catalogs !== 'object') return
|
|
101
|
+
|
|
102
|
+
for (const [catalogName, catalogConfig] of Object.entries(catalogs)) {
|
|
103
|
+
const typedConfig = catalogConfig as Record<string, any>
|
|
104
|
+
const repositories = typedConfig.repositories
|
|
105
|
+
if (!repositories || typeof repositories !== 'object') continue
|
|
106
|
+
if (!(repoName in repositories)) continue
|
|
107
|
+
|
|
108
|
+
const fileName = catalogName.replace(/\//g, '~')
|
|
109
|
+
const existing = await this.HomeRegistry.getCatalog(fileName)
|
|
110
|
+
if (!existing) continue
|
|
111
|
+
|
|
112
|
+
if (!existing.repositories) existing.repositories = {}
|
|
113
|
+
if (!existing.repositories[repoName]) existing.repositories[repoName] = {}
|
|
114
|
+
const repoEntry = existing.repositories[repoName]
|
|
115
|
+
const now = new Date().toISOString()
|
|
116
|
+
|
|
117
|
+
const existingEntity = repoEntry[providerKey]
|
|
118
|
+
const entityCreatedAt = existingEntity?.createdAt || now
|
|
119
|
+
|
|
120
|
+
// Check if data actually changed (compare without timestamps, order-independent)
|
|
121
|
+
const { createdAt: _ec, updatedAt: _eu, ...existingData } = existingEntity || {}
|
|
122
|
+
const stableStringify = (obj: any): string => JSON.stringify(obj, (_, v) =>
|
|
123
|
+
v && typeof v === 'object' && !Array.isArray(v)
|
|
124
|
+
? Object.keys(v).sort().reduce((o: any, k) => { o[k] = v[k]; return o }, {})
|
|
125
|
+
: v
|
|
126
|
+
)
|
|
127
|
+
const dataChanged = stableStringify(existingData) !== stableStringify(providerData)
|
|
128
|
+
|
|
129
|
+
repoEntry[providerKey] = {
|
|
130
|
+
...providerData,
|
|
131
|
+
createdAt: entityCreatedAt,
|
|
132
|
+
updatedAt: dataChanged ? now : (existingEntity?.updatedAt || now),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!repoEntry.createdAt) repoEntry.createdAt = now
|
|
136
|
+
if (dataChanged) {
|
|
137
|
+
repoEntry.updatedAt = now
|
|
138
|
+
existing.updatedAt = now
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await this.HomeRegistry.setCatalog(fileName, existing)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}, {
|
|
148
|
+
importMeta: import.meta,
|
|
149
|
+
importStack: makeImportStack(),
|
|
150
|
+
capsuleName: capsule['#'],
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
capsule['#'] = 't44/caps/ProjectCatalogs'
|