t44 0.4.0-rc.10
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/.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 +185 -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 +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 +807 -0
- package/caps/WorkspaceConnection.ts +256 -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 +133 -0
- package/caps/providers/github.com/api.ts +130 -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/examples/01-Lifecycle/main.test.ts +228 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +369 -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 +102 -0
- package/standalone-rt.ts +121 -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/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/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +3 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
|
|
4
|
+
// IMPORTANT: Connection config files contain encrypted credentials.
|
|
5
|
+
// NEVER delete these files programmatically. If decryption fails,
|
|
6
|
+
// log a clear error and exit so the user can investigate. The user must manually delete and re-enter
|
|
7
|
+
// credentials if the workspace key has changed.
|
|
8
|
+
|
|
9
|
+
// Track which connection setup titles and descriptions have been shown
|
|
10
|
+
const shownConnectionTitles = new Set<string>()
|
|
11
|
+
const shownDescriptions = new Set<string>()
|
|
12
|
+
|
|
13
|
+
export async function capsule({
|
|
14
|
+
encapsulate,
|
|
15
|
+
CapsulePropertyTypes,
|
|
16
|
+
makeImportStack
|
|
17
|
+
}: {
|
|
18
|
+
encapsulate: any
|
|
19
|
+
CapsulePropertyTypes: any
|
|
20
|
+
makeImportStack: any
|
|
21
|
+
}) {
|
|
22
|
+
return encapsulate({
|
|
23
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
24
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
25
|
+
'#': {
|
|
26
|
+
Home: {
|
|
27
|
+
type: CapsulePropertyTypes.Mapping,
|
|
28
|
+
value: 't44/caps/Home'
|
|
29
|
+
},
|
|
30
|
+
WorkspaceConfig: {
|
|
31
|
+
type: CapsulePropertyTypes.Mapping,
|
|
32
|
+
value: 't44/caps/WorkspaceConfig'
|
|
33
|
+
},
|
|
34
|
+
WorkspacePrompt: {
|
|
35
|
+
type: CapsulePropertyTypes.Mapping,
|
|
36
|
+
value: 't44/caps/WorkspacePrompt'
|
|
37
|
+
},
|
|
38
|
+
WorkspaceKey: {
|
|
39
|
+
type: CapsulePropertyTypes.Mapping,
|
|
40
|
+
value: 't44/caps/WorkspaceKey'
|
|
41
|
+
},
|
|
42
|
+
HomeRegistry: {
|
|
43
|
+
type: CapsulePropertyTypes.Mapping,
|
|
44
|
+
value: 't44/caps/HomeRegistry'
|
|
45
|
+
},
|
|
46
|
+
JsonSchema: {
|
|
47
|
+
type: CapsulePropertyTypes.Mapping,
|
|
48
|
+
value: 't44/caps/JsonSchemas'
|
|
49
|
+
},
|
|
50
|
+
RegisterSchemas: {
|
|
51
|
+
type: CapsulePropertyTypes.StructInit,
|
|
52
|
+
value: async function (this: any): Promise<void> {
|
|
53
|
+
if (this.schema?.schema) {
|
|
54
|
+
const version = this.schemaMinorVersion || '0'
|
|
55
|
+
await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
getFilepath: {
|
|
60
|
+
type: CapsulePropertyTypes.Function,
|
|
61
|
+
value: async function (this: any): Promise<string> {
|
|
62
|
+
const registryDir = await this.Home.registryDir
|
|
63
|
+
const workspaceConfig = await this.WorkspaceConfig.config
|
|
64
|
+
const workspaceConfigStruct = workspaceConfig?.['#t44/structs/WorkspaceConfig'] || {}
|
|
65
|
+
const workspaceName = workspaceConfigStruct.name
|
|
66
|
+
const connectionType = this.capsuleName.replace(/\//g, '~')
|
|
67
|
+
return join(registryDir, '@t44.sh~t44~caps~WorkspaceConnection', workspaceName, `${connectionType}.json`)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
getRelativeFilepath: {
|
|
71
|
+
type: CapsulePropertyTypes.Function,
|
|
72
|
+
value: async function (this: any): Promise<string> {
|
|
73
|
+
const fullPath = await this.getFilepath()
|
|
74
|
+
return this.Home.relativePath(fullPath)
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
getStoredConfig: {
|
|
78
|
+
type: CapsulePropertyTypes.Function,
|
|
79
|
+
value: async function (this: any): Promise<Record<string, any> | null> {
|
|
80
|
+
const { readFile } = await import('fs/promises')
|
|
81
|
+
const filepath = await this.getFilepath()
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const content = await readFile(filepath, 'utf-8')
|
|
85
|
+
const parsed = JSON.parse(content)
|
|
86
|
+
const config = parsed.config || {}
|
|
87
|
+
|
|
88
|
+
// Handle legacy encryptedConfig format (migrate to per-value encryption)
|
|
89
|
+
if (parsed.encryptedConfig) {
|
|
90
|
+
const decrypted = await this.WorkspaceKey.decryptString(parsed.encryptedConfig)
|
|
91
|
+
const legacyConfig = JSON.parse(decrypted)
|
|
92
|
+
// Re-save with per-value encryption
|
|
93
|
+
await this.setStoredConfig(legacyConfig)
|
|
94
|
+
return legacyConfig
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result: Record<string, any> = {}
|
|
98
|
+
let needsMigration = false
|
|
99
|
+
|
|
100
|
+
for (const [key, value] of Object.entries(config)) {
|
|
101
|
+
if (typeof value === 'string' && value.startsWith('aes-256-gcm:')) {
|
|
102
|
+
// Encrypted value: <algo>:<keyName>-<did>:<enc value>
|
|
103
|
+
// Also supports legacy format: <algo>:<keyName>:<enc value>
|
|
104
|
+
// Use lastIndexOf since DID contains colons but base64 does not
|
|
105
|
+
const lastColon = value.lastIndexOf(':')
|
|
106
|
+
if (lastColon > 'aes-256-gcm:'.length) {
|
|
107
|
+
const encryptedValue = value.substring(lastColon + 1)
|
|
108
|
+
const keyIdentifier = value.substring('aes-256-gcm:'.length, lastColon)
|
|
109
|
+
try {
|
|
110
|
+
const decrypted = await this.WorkspaceKey.decryptString(encryptedValue)
|
|
111
|
+
result[key] = JSON.parse(decrypted)
|
|
112
|
+
} catch (decryptErr: any) {
|
|
113
|
+
const chalk = (await import('chalk')).default
|
|
114
|
+
let currentKeyId = 'unknown'
|
|
115
|
+
try {
|
|
116
|
+
const keyConfig = await this.WorkspaceKey.ensureKey()
|
|
117
|
+
const did = await this.WorkspaceKey.getDid()
|
|
118
|
+
currentKeyId = `${keyConfig.keyName}-${did}`
|
|
119
|
+
} catch { }
|
|
120
|
+
console.error(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
|
|
121
|
+
console.error(chalk.red(`│ ✗ Connection Credential Decryption Failed │`))
|
|
122
|
+
console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
|
|
123
|
+
console.error(chalk.red(`│ │`))
|
|
124
|
+
console.error(chalk.red(`│ Cannot decrypt '${key}' in ${this.capsuleName}`))
|
|
125
|
+
console.error(chalk.red(`│ │`))
|
|
126
|
+
console.error(chalk.red(`│ This credential was encrypted with a different workspace key. │`))
|
|
127
|
+
console.error(chalk.red(`│ Encrypted with: ${keyIdentifier}`))
|
|
128
|
+
console.error(chalk.red(`│ Current key: ${currentKeyId}`))
|
|
129
|
+
console.error(chalk.red(`│ │`))
|
|
130
|
+
console.error(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
|
|
131
|
+
console.error(chalk.red(`│ To fix, delete the connection config and re-enter credentials: │`))
|
|
132
|
+
console.error(chalk.red(`│ │`))
|
|
133
|
+
console.error(chalk.red(`│ rm ${filepath}`))
|
|
134
|
+
console.error(chalk.red(`│ │`))
|
|
135
|
+
console.error(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// Plain text value - needs migration
|
|
141
|
+
result[key] = value
|
|
142
|
+
needsMigration = true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Auto-migrate plain text values to encrypted
|
|
147
|
+
if (needsMigration) {
|
|
148
|
+
await this.setStoredConfig(result)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return Object.keys(result).length > 0 ? result : null
|
|
152
|
+
} catch (err: any) {
|
|
153
|
+
// If file doesn't exist, that's normal - user hasn't configured this provider yet
|
|
154
|
+
if (err?.code === 'ENOENT') {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// For other errors (permission issues, malformed JSON, etc.), log and exit
|
|
159
|
+
const chalk = (await import('chalk')).default
|
|
160
|
+
console.error(chalk.red(`\n\u2717 Failed to read connection config for '${this.capsuleName}'\n`))
|
|
161
|
+
console.error(chalk.red(` File: ${filepath}`))
|
|
162
|
+
console.error(chalk.red(` Error: ${err?.message || err}\n`))
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
setStoredConfig: {
|
|
168
|
+
type: CapsulePropertyTypes.Function,
|
|
169
|
+
value: async function (this: any, config: Record<string, any>): Promise<void> {
|
|
170
|
+
const { mkdir, writeFile } = await import('fs/promises')
|
|
171
|
+
const { dirname } = await import('path')
|
|
172
|
+
const filepath = await this.getFilepath()
|
|
173
|
+
const dir = dirname(filepath)
|
|
174
|
+
|
|
175
|
+
await mkdir(dir, { recursive: true })
|
|
176
|
+
|
|
177
|
+
// Ensure workspace key exists and get key name + DID
|
|
178
|
+
const { keyName } = await this.WorkspaceKey.ensureKey()
|
|
179
|
+
const did = await this.WorkspaceKey.getDid()
|
|
180
|
+
|
|
181
|
+
// Encrypt each value separately with prefix format
|
|
182
|
+
const encryptedConfig: Record<string, string> = {}
|
|
183
|
+
for (const [key, value] of Object.entries(config)) {
|
|
184
|
+
const valueJson = JSON.stringify(value)
|
|
185
|
+
const encrypted = await this.WorkspaceKey.encryptString(valueJson)
|
|
186
|
+
// Format: <algo>:<keyName>-<did>:<enc value>
|
|
187
|
+
encryptedConfig[key] = `aes-256-gcm:${keyName}-${did}:${encrypted}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const output = {
|
|
191
|
+
config: encryptedConfig
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await writeFile(filepath, JSON.stringify(output, null, 4), { mode: 0o600 })
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
getConfigValue: {
|
|
198
|
+
type: CapsulePropertyTypes.Function,
|
|
199
|
+
value: async function (this: any, key: string): Promise<any> {
|
|
200
|
+
const storedConfig = await this.getStoredConfig() || {}
|
|
201
|
+
|
|
202
|
+
if (storedConfig[key] !== undefined) {
|
|
203
|
+
return storedConfig[key]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Value not set, need to prompt user
|
|
207
|
+
const propertySchema = this.schema?.schema?.properties?.[key]
|
|
208
|
+
if (!propertySchema) {
|
|
209
|
+
throw new Error(`No schema defined for config key "${key}" in ${this.capsuleName} connection config`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create promptFactId for deduplication
|
|
213
|
+
const promptFactId = `${this.capsuleName}:${key}`
|
|
214
|
+
|
|
215
|
+
// Show title once per capsuleName
|
|
216
|
+
const chalk = (await import('chalk')).default
|
|
217
|
+
if (!shownConnectionTitles.has(this.capsuleName)) {
|
|
218
|
+
console.log(chalk.cyan(`\n🔑 ${this.capsuleName} Connection Setup\n`))
|
|
219
|
+
shownConnectionTitles.add(this.capsuleName)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Show description once per promptFactId
|
|
223
|
+
if (propertySchema.description && !shownDescriptions.has(promptFactId)) {
|
|
224
|
+
console.log(chalk.gray(` ${propertySchema.description}\n`))
|
|
225
|
+
shownDescriptions.add(promptFactId)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const value = await this.WorkspacePrompt.input({
|
|
229
|
+
message: `${propertySchema.title || key}:`,
|
|
230
|
+
validate: (input: string) => {
|
|
231
|
+
if (!input || input.trim().length === 0) {
|
|
232
|
+
return `${propertySchema.title || key} cannot be empty`
|
|
233
|
+
}
|
|
234
|
+
return true
|
|
235
|
+
},
|
|
236
|
+
promptFactId
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Store the value
|
|
240
|
+
storedConfig[key] = value
|
|
241
|
+
await this.setStoredConfig(storedConfig)
|
|
242
|
+
|
|
243
|
+
console.log(chalk.green(`\n ✓ ${propertySchema.title || key} saved to connection config\n`))
|
|
244
|
+
|
|
245
|
+
return value
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}, {
|
|
251
|
+
importMeta: import.meta,
|
|
252
|
+
importStack: makeImportStack(),
|
|
253
|
+
capsuleName: capsule['#'],
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
capsule['#'] = 't44/caps/WorkspaceConnection'
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
'#': {
|
|
15
|
+
WorkspaceConfig: {
|
|
16
|
+
type: CapsulePropertyTypes.Mapping,
|
|
17
|
+
value: 't44/caps/WorkspaceConfig'
|
|
18
|
+
},
|
|
19
|
+
JsonSchema: {
|
|
20
|
+
type: CapsulePropertyTypes.Mapping,
|
|
21
|
+
value: 't44/caps/JsonSchemas'
|
|
22
|
+
},
|
|
23
|
+
config: {
|
|
24
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
25
|
+
value: async function (this: any): Promise<void> {
|
|
26
|
+
|
|
27
|
+
const config = await this.WorkspaceConfig.config
|
|
28
|
+
|
|
29
|
+
const configKey = '#' + this.capsuleName
|
|
30
|
+
|
|
31
|
+
const entityConfig = config[configKey] || undefined
|
|
32
|
+
|
|
33
|
+
return entityConfig
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
setConfigValue: {
|
|
37
|
+
type: CapsulePropertyTypes.Function,
|
|
38
|
+
value: async function (this: any, path: string[], value: any): Promise<void> {
|
|
39
|
+
|
|
40
|
+
const configKey = '#' + this.capsuleName
|
|
41
|
+
|
|
42
|
+
const now = new Date().toISOString()
|
|
43
|
+
await this.WorkspaceConfig.ensureEntityTimestamps(
|
|
44
|
+
{ entityName: this.capsuleName },
|
|
45
|
+
configKey, now
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const changed = await this.WorkspaceConfig.setConfigValueForEntity(
|
|
49
|
+
{ entityName: this.capsuleName, schema: this.schema },
|
|
50
|
+
[configKey, ...path], value
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (changed) {
|
|
54
|
+
await this.WorkspaceConfig.setConfigValueForEntity(
|
|
55
|
+
{ entityName: this.capsuleName, schema: this.schema },
|
|
56
|
+
[configKey, 'updatedAt'], new Date().toISOString()
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
RegisterSchemas: {
|
|
62
|
+
type: CapsulePropertyTypes.StructInit,
|
|
63
|
+
value: async function (this: any): Promise<void> {
|
|
64
|
+
if (this.schema?.schema) {
|
|
65
|
+
const version = this.schemaMinorVersion || '0'
|
|
66
|
+
await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, {
|
|
73
|
+
importMeta: import.meta,
|
|
74
|
+
importStack: makeImportStack(),
|
|
75
|
+
capsuleName: capsule['#'],
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
capsule['#'] = 't44/caps/WorkspaceEntityConfig'
|
|
@@ -0,0 +1,77 @@
|
|
|
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.v0': {},
|
|
14
|
+
'#': {
|
|
15
|
+
WorkspaceConfig: {
|
|
16
|
+
type: CapsulePropertyTypes.Mapping,
|
|
17
|
+
value: 't44/caps/WorkspaceConfig.v0'
|
|
18
|
+
},
|
|
19
|
+
JsonSchema: {
|
|
20
|
+
type: CapsulePropertyTypes.Mapping,
|
|
21
|
+
value: 't44/caps/JsonSchemas.v0'
|
|
22
|
+
},
|
|
23
|
+
config: {
|
|
24
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
25
|
+
value: async function (this: any): Promise<void> {
|
|
26
|
+
|
|
27
|
+
const config = await this.WorkspaceConfig.config
|
|
28
|
+
|
|
29
|
+
const configKey = '#' + this.capsuleName
|
|
30
|
+
|
|
31
|
+
const entityConfig = config[configKey] || undefined
|
|
32
|
+
|
|
33
|
+
return entityConfig
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
setConfigValue: {
|
|
37
|
+
type: CapsulePropertyTypes.Function,
|
|
38
|
+
value: async function (this: any, path: string[], value: any): Promise<void> {
|
|
39
|
+
|
|
40
|
+
const configKey = '#' + this.capsuleName
|
|
41
|
+
|
|
42
|
+
await this.WorkspaceConfig.setConfigValueForEntity(
|
|
43
|
+
{ entityName: this.capsuleName, schema: this.schema },
|
|
44
|
+
[configKey, 'createdAt'], new Date().toISOString(), { ifAbsent: true }
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const changed = await this.WorkspaceConfig.setConfigValueForEntity(
|
|
48
|
+
{ entityName: this.capsuleName, schema: this.schema },
|
|
49
|
+
[configKey, ...path], value
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if (changed) {
|
|
53
|
+
await this.WorkspaceConfig.setConfigValueForEntity(
|
|
54
|
+
{ entityName: this.capsuleName, schema: this.schema },
|
|
55
|
+
[configKey, 'updatedAt'], new Date().toISOString()
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
RegisterSchemas: {
|
|
61
|
+
type: CapsulePropertyTypes.StructInit,
|
|
62
|
+
value: async function (this: any): Promise<void> {
|
|
63
|
+
if (this.schema?.schema) {
|
|
64
|
+
const version = this.schemaMinorVersion || '0'
|
|
65
|
+
await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, {
|
|
72
|
+
importMeta: import.meta,
|
|
73
|
+
importStack: makeImportStack(),
|
|
74
|
+
capsuleName: capsule['#'],
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
capsule['#'] = 't44/caps/WorkspaceEntityConfig.v0'
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { mkdir, writeFile, readFile, stat } from 'fs/promises'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function capsule({
|
|
7
|
+
encapsulate,
|
|
8
|
+
CapsulePropertyTypes,
|
|
9
|
+
makeImportStack
|
|
10
|
+
}: {
|
|
11
|
+
encapsulate: any
|
|
12
|
+
CapsulePropertyTypes: any
|
|
13
|
+
makeImportStack: any
|
|
14
|
+
}) {
|
|
15
|
+
return encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
18
|
+
'#': {
|
|
19
|
+
WorkspaceConfig: {
|
|
20
|
+
type: CapsulePropertyTypes.Mapping,
|
|
21
|
+
value: 't44/caps/WorkspaceConfig'
|
|
22
|
+
},
|
|
23
|
+
JsonSchema: {
|
|
24
|
+
type: CapsulePropertyTypes.Mapping,
|
|
25
|
+
value: 't44/caps/JsonSchemas'
|
|
26
|
+
},
|
|
27
|
+
RegisterSchemas: {
|
|
28
|
+
type: CapsulePropertyTypes.StructInit,
|
|
29
|
+
value: async function (this: any): Promise<void> {
|
|
30
|
+
if (this.schema?.schema) {
|
|
31
|
+
const version = this.schemaMinorVersion || '0'
|
|
32
|
+
await this.JsonSchema.registerSchema(this.capsuleName, this.schema.schema, version)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
getFilepath: {
|
|
37
|
+
type: CapsulePropertyTypes.Function,
|
|
38
|
+
value: function (this: any, instanceName: string): string {
|
|
39
|
+
return join(
|
|
40
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
41
|
+
this.getRelativeFilepath(instanceName)
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
getRelativeFilepath: {
|
|
46
|
+
type: CapsulePropertyTypes.Function,
|
|
47
|
+
value: function (this: any, instanceName: string): string {
|
|
48
|
+
return join(
|
|
49
|
+
'.~o',
|
|
50
|
+
'workspace.foundation',
|
|
51
|
+
capsule['#'].replace(/\//g, '~'),
|
|
52
|
+
this.capsuleName.replace(/\//g, '~'),
|
|
53
|
+
instanceName + '.json'
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
get: {
|
|
58
|
+
type: CapsulePropertyTypes.Function,
|
|
59
|
+
value: async function (this: any, instanceName: string, rawFilepaths?: string[]): Promise<{ data: any; stale: boolean } | null> {
|
|
60
|
+
const factFilepath = this.getFilepath(instanceName)
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const factStat = await stat(factFilepath)
|
|
64
|
+
const factMtime = factStat.mtimeMs
|
|
65
|
+
|
|
66
|
+
// Check if any raw filepaths are newer than our cached fact
|
|
67
|
+
let stale = false
|
|
68
|
+
if (rawFilepaths && rawFilepaths.length > 0) {
|
|
69
|
+
for (const rawPath of rawFilepaths) {
|
|
70
|
+
const fullRawPath = rawPath.startsWith('/')
|
|
71
|
+
? rawPath
|
|
72
|
+
: join(this.WorkspaceConfig.workspaceRootDir, rawPath)
|
|
73
|
+
try {
|
|
74
|
+
const rawStat = await stat(fullRawPath)
|
|
75
|
+
if (rawStat.mtimeMs > factMtime) {
|
|
76
|
+
stale = true
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Raw file doesn't exist, consider stale
|
|
81
|
+
stale = true
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const content = await readFile(factFilepath, 'utf-8')
|
|
88
|
+
const parsed = JSON.parse(content)
|
|
89
|
+
const { $schema, $id, _ValidationFeedback, ...data } = parsed
|
|
90
|
+
|
|
91
|
+
return { data, stale }
|
|
92
|
+
} catch {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
set: {
|
|
98
|
+
type: CapsulePropertyTypes.Function,
|
|
99
|
+
value: async function (this: any, instanceName: string, data: any): Promise<void> {
|
|
100
|
+
|
|
101
|
+
const factDir = join(
|
|
102
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
103
|
+
'.~o',
|
|
104
|
+
'workspace.foundation',
|
|
105
|
+
capsule['#'].replace(/\//g, '~'),
|
|
106
|
+
this.capsuleName.replace(/\//g, '~')
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
await mkdir(factDir, { recursive: true })
|
|
110
|
+
|
|
111
|
+
const factFilepath = join(factDir, instanceName + '.json')
|
|
112
|
+
|
|
113
|
+
// Check if schema defines updatedAt or createdAt
|
|
114
|
+
const schemaProperties = this.schema?.schema?.properties || {}
|
|
115
|
+
const hasUpdatedAt = 'updatedAt' in schemaProperties
|
|
116
|
+
const hasCreatedAt = 'createdAt' in schemaProperties
|
|
117
|
+
|
|
118
|
+
// Read existing data to check for changes and preserve timestamps
|
|
119
|
+
let existingData: any = null
|
|
120
|
+
try {
|
|
121
|
+
const existingContent = await readFile(factFilepath, 'utf-8')
|
|
122
|
+
const existingParsed = JSON.parse(existingContent)
|
|
123
|
+
const { $schema, $id, _ValidationFeedback, ...existing } = existingParsed
|
|
124
|
+
existingData = existing
|
|
125
|
+
} catch {
|
|
126
|
+
// File doesn't exist or can't be read
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Prepare output data
|
|
130
|
+
const outputData = { ...data }
|
|
131
|
+
|
|
132
|
+
// Track if we're adding missing timestamps
|
|
133
|
+
let addedCreatedAt = false
|
|
134
|
+
let addedUpdatedAt = false
|
|
135
|
+
|
|
136
|
+
// Set createdAt if schema defines it and it's not set
|
|
137
|
+
if (hasCreatedAt && !outputData.createdAt) {
|
|
138
|
+
if (existingData?.createdAt) {
|
|
139
|
+
// Preserve existing createdAt
|
|
140
|
+
outputData.createdAt = existingData.createdAt
|
|
141
|
+
} else {
|
|
142
|
+
// Set new createdAt (file exists but missing timestamp)
|
|
143
|
+
outputData.createdAt = new Date().toISOString()
|
|
144
|
+
addedCreatedAt = true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Set updatedAt if schema defines it
|
|
149
|
+
if (hasUpdatedAt) {
|
|
150
|
+
if (!outputData.updatedAt && existingData?.updatedAt) {
|
|
151
|
+
// Preserve existing updatedAt for comparison
|
|
152
|
+
outputData.updatedAt = existingData.updatedAt
|
|
153
|
+
} else if (!outputData.updatedAt) {
|
|
154
|
+
// Set new updatedAt (file exists but missing timestamp, or new file)
|
|
155
|
+
outputData.updatedAt = new Date().toISOString()
|
|
156
|
+
if (existingData) {
|
|
157
|
+
addedUpdatedAt = true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const version = this.schemaMinorVersion || '0'
|
|
163
|
+
const output: Record<string, any> = {
|
|
164
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
165
|
+
$id: this.capsuleName + '.v' + version,
|
|
166
|
+
...outputData
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if content has changed
|
|
170
|
+
if (existingData) {
|
|
171
|
+
const existingOutput = {
|
|
172
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
173
|
+
$id: this.capsuleName + '.v' + version,
|
|
174
|
+
...existingData
|
|
175
|
+
}
|
|
176
|
+
const existingContent = JSON.stringify(existingOutput, null, 4)
|
|
177
|
+
const newContent = JSON.stringify(output, null, 4)
|
|
178
|
+
|
|
179
|
+
if (existingContent === newContent) {
|
|
180
|
+
// Content is identical, skip write
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Content changed - update updatedAt if schema defines it and it matches existing
|
|
185
|
+
// But only if we didn't just add it (which means data actually changed)
|
|
186
|
+
if (hasUpdatedAt && !addedUpdatedAt && !addedCreatedAt && outputData.updatedAt === existingData?.updatedAt) {
|
|
187
|
+
output.updatedAt = new Date().toISOString()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await writeFile(factFilepath, JSON.stringify(output, null, 4))
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
delete: {
|
|
195
|
+
type: CapsulePropertyTypes.Function,
|
|
196
|
+
value: async function (this: any, instanceName: string): Promise<void> {
|
|
197
|
+
const { unlink } = await import('fs/promises')
|
|
198
|
+
const factFilepath = this.getFilepath(instanceName)
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await unlink(factFilepath)
|
|
202
|
+
} catch (error: any) {
|
|
203
|
+
// Ignore if file doesn't exist
|
|
204
|
+
if (error.code !== 'ENOENT') {
|
|
205
|
+
throw error
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}, {
|
|
213
|
+
importMeta: import.meta,
|
|
214
|
+
importStack: makeImportStack(),
|
|
215
|
+
capsuleName: capsule['#'],
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
capsule['#'] = 't44/caps/WorkspaceEntityFact'
|