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,240 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { mkdir, writeFile, readFile } from 'fs/promises'
|
|
4
|
+
import { validatePropertyValue } from '../lib/openapi.js'
|
|
5
|
+
|
|
6
|
+
// IMPORTANT: Connection config files (.~o/workspace.foundation/WorkspaceConnections/o/<origin>/config.json)
|
|
7
|
+
// contain encrypted credentials. NEVER delete these files programmatically. If decryption fails,
|
|
8
|
+
// log a clear error and exit so the user can investigate. The user must manually delete and re-enter
|
|
9
|
+
// credentials if the workspace key has changed.
|
|
10
|
+
|
|
11
|
+
// Track which connection setup titles and descriptions have been shown
|
|
12
|
+
const shownConnectionTitles = new Set<string>()
|
|
13
|
+
const shownDescriptions = new Set<string>()
|
|
14
|
+
|
|
15
|
+
export async function capsule({
|
|
16
|
+
encapsulate,
|
|
17
|
+
CapsulePropertyTypes,
|
|
18
|
+
makeImportStack
|
|
19
|
+
}: {
|
|
20
|
+
encapsulate: any
|
|
21
|
+
CapsulePropertyTypes: any
|
|
22
|
+
makeImportStack: any
|
|
23
|
+
}) {
|
|
24
|
+
return encapsulate({
|
|
25
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
26
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
27
|
+
'#': {
|
|
28
|
+
WorkspaceConfig: {
|
|
29
|
+
type: CapsulePropertyTypes.Mapping,
|
|
30
|
+
value: 't44/caps/WorkspaceConfig.v0'
|
|
31
|
+
},
|
|
32
|
+
WorkspacePrompt: {
|
|
33
|
+
type: CapsulePropertyTypes.Mapping,
|
|
34
|
+
value: 't44/caps/WorkspacePrompt.v0'
|
|
35
|
+
},
|
|
36
|
+
WorkspaceKey: {
|
|
37
|
+
type: CapsulePropertyTypes.Mapping,
|
|
38
|
+
value: 't44/caps/WorkspaceKey.v0'
|
|
39
|
+
},
|
|
40
|
+
origin: {
|
|
41
|
+
type: CapsulePropertyTypes.Literal,
|
|
42
|
+
value: undefined,
|
|
43
|
+
},
|
|
44
|
+
schema: {
|
|
45
|
+
type: CapsulePropertyTypes.Literal,
|
|
46
|
+
value: {},
|
|
47
|
+
},
|
|
48
|
+
getFilepath: {
|
|
49
|
+
type: CapsulePropertyTypes.Function,
|
|
50
|
+
value: function (this: any): string {
|
|
51
|
+
return join(
|
|
52
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
53
|
+
this.getRelativeFilepath()
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
getRelativeFilepath: {
|
|
58
|
+
type: CapsulePropertyTypes.Function,
|
|
59
|
+
value: function (this: any): string {
|
|
60
|
+
return join(
|
|
61
|
+
'.~o',
|
|
62
|
+
'workspace.foundation',
|
|
63
|
+
'WorkspaceConnections',
|
|
64
|
+
'o',
|
|
65
|
+
this.origin,
|
|
66
|
+
'config.json'
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
getStoredConfig: {
|
|
71
|
+
type: CapsulePropertyTypes.Function,
|
|
72
|
+
value: async function (this: any): Promise<Record<string, any> | null> {
|
|
73
|
+
const filepath = this.getFilepath()
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const content = await readFile(filepath, 'utf-8')
|
|
77
|
+
const parsed = JSON.parse(content)
|
|
78
|
+
const config = parsed.config || {}
|
|
79
|
+
|
|
80
|
+
// Handle legacy encryptedConfig format (migrate to per-value encryption)
|
|
81
|
+
if (parsed.encryptedConfig) {
|
|
82
|
+
const decrypted = await this.WorkspaceKey.decryptString(parsed.encryptedConfig)
|
|
83
|
+
const legacyConfig = JSON.parse(decrypted)
|
|
84
|
+
// Re-save with per-value encryption
|
|
85
|
+
await this.setStoredConfig(legacyConfig)
|
|
86
|
+
return legacyConfig
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result: Record<string, any> = {}
|
|
90
|
+
let needsMigration = false
|
|
91
|
+
|
|
92
|
+
for (const [key, value] of Object.entries(config)) {
|
|
93
|
+
if (typeof value === 'string' && value.startsWith('aes-256-gcm:')) {
|
|
94
|
+
// Encrypted value: <algo>:<keyName>-<did>:<enc value>
|
|
95
|
+
// Also supports legacy format: <algo>:<keyName>:<enc value>
|
|
96
|
+
// Use lastIndexOf since DID contains colons but base64 does not
|
|
97
|
+
const lastColon = value.lastIndexOf(':')
|
|
98
|
+
if (lastColon > 'aes-256-gcm:'.length) {
|
|
99
|
+
const encryptedValue = value.substring(lastColon + 1)
|
|
100
|
+
const keyIdentifier = value.substring('aes-256-gcm:'.length, lastColon)
|
|
101
|
+
try {
|
|
102
|
+
const decrypted = await this.WorkspaceKey.decryptString(encryptedValue)
|
|
103
|
+
result[key] = JSON.parse(decrypted)
|
|
104
|
+
} catch (decryptErr: any) {
|
|
105
|
+
const chalk = (await import('chalk')).default
|
|
106
|
+
const keyPath = await this.WorkspaceKey.getKeyPath()
|
|
107
|
+
let currentDid: string | null = null
|
|
108
|
+
try { currentDid = await this.WorkspaceKey.getDid() } catch { }
|
|
109
|
+
console.error(chalk.red(`\n\u2717 Decryption failed for '${key}' in ${this.origin} connection config\n`))
|
|
110
|
+
console.error(chalk.red(` Config file: ${filepath}`))
|
|
111
|
+
console.error(chalk.red(` Encrypted with key identifier: ${keyIdentifier}`))
|
|
112
|
+
console.error(chalk.red(` Current workspace key file: ${keyPath}`))
|
|
113
|
+
if (currentDid) {
|
|
114
|
+
console.error(chalk.red(` Current workspace key DID: ${currentDid}`))
|
|
115
|
+
}
|
|
116
|
+
console.error(chalk.red(` Error: ${decryptErr?.message || decryptErr}`))
|
|
117
|
+
console.error(chalk.yellow(`\n The workspace key may have changed since these credentials were saved.`))
|
|
118
|
+
console.error(chalk.yellow(` To fix: restore the original key, or manually delete the config file and re-enter credentials.\n`))
|
|
119
|
+
process.exit(1)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// Plain text value - needs migration
|
|
124
|
+
result[key] = value
|
|
125
|
+
needsMigration = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Auto-migrate plain text values to encrypted
|
|
130
|
+
if (needsMigration) {
|
|
131
|
+
await this.setStoredConfig(result)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return Object.keys(result).length > 0 ? result : null
|
|
135
|
+
} catch (err: any) {
|
|
136
|
+
if (err?.code === 'ENOENT') {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
const chalk = (await import('chalk')).default
|
|
140
|
+
console.error(chalk.red(`\n\u2717 Failed to read connection config for '${this.origin}'\n`))
|
|
141
|
+
console.error(chalk.red(` File: ${this.getFilepath()}`))
|
|
142
|
+
console.error(chalk.red(` Error: ${err?.message || err}\n`))
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
setStoredConfig: {
|
|
148
|
+
type: CapsulePropertyTypes.Function,
|
|
149
|
+
value: async function (this: any, config: Record<string, any>): Promise<void> {
|
|
150
|
+
const filepath = this.getFilepath()
|
|
151
|
+
const dir = join(filepath, '..')
|
|
152
|
+
|
|
153
|
+
await mkdir(dir, { recursive: true })
|
|
154
|
+
|
|
155
|
+
// Ensure workspace key exists and get key name + DID
|
|
156
|
+
const { keyName } = await this.WorkspaceKey.ensureKey()
|
|
157
|
+
const did = await this.WorkspaceKey.getDid()
|
|
158
|
+
|
|
159
|
+
// Encrypt each value separately with prefix format
|
|
160
|
+
const encryptedConfig: Record<string, string> = {}
|
|
161
|
+
for (const [key, value] of Object.entries(config)) {
|
|
162
|
+
const valueJson = JSON.stringify(value)
|
|
163
|
+
const encrypted = await this.WorkspaceKey.encryptString(valueJson)
|
|
164
|
+
// Format: <algo>:<keyName>-<did>:<enc value>
|
|
165
|
+
encryptedConfig[key] = `aes-256-gcm:${keyName}-${did}:${encrypted}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const output = {
|
|
169
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
170
|
+
config: encryptedConfig
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await writeFile(filepath, JSON.stringify(output, null, 4))
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
getConfigValue: {
|
|
177
|
+
type: CapsulePropertyTypes.Function,
|
|
178
|
+
value: async function (this: any, key: string): Promise<any> {
|
|
179
|
+
const storedConfig = await this.getStoredConfig() || {}
|
|
180
|
+
|
|
181
|
+
if (storedConfig[key] !== undefined) {
|
|
182
|
+
return storedConfig[key]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Value not set, need to prompt user
|
|
186
|
+
const propertySchema = this.schema?.properties?.[key]
|
|
187
|
+
if (!propertySchema) {
|
|
188
|
+
throw new Error(`No schema defined for config key "${key}" in ${this.origin} connection config`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create promptFactId for deduplication
|
|
192
|
+
const promptFactId = `${this.capsuleName}:${key}`
|
|
193
|
+
|
|
194
|
+
// Show title once per origin
|
|
195
|
+
const chalk = (await import('chalk')).default
|
|
196
|
+
if (!shownConnectionTitles.has(this.origin)) {
|
|
197
|
+
console.log(chalk.cyan(`\n🔑 ${this.origin} Connection Setup\n`))
|
|
198
|
+
shownConnectionTitles.add(this.origin)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Show description once per promptFactId
|
|
202
|
+
if (propertySchema.description && !shownDescriptions.has(promptFactId)) {
|
|
203
|
+
console.log(chalk.gray(` ${propertySchema.description}\n`))
|
|
204
|
+
shownDescriptions.add(promptFactId)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const value = await this.WorkspacePrompt.input({
|
|
208
|
+
message: `${propertySchema.title || key}:`,
|
|
209
|
+
validate: (input: string) => {
|
|
210
|
+
if (!input || input.trim().length === 0) {
|
|
211
|
+
return `${propertySchema.title || key} cannot be empty`
|
|
212
|
+
}
|
|
213
|
+
// Validate against schema using AJV
|
|
214
|
+
const result = validatePropertyValue(propertySchema, input, key)
|
|
215
|
+
if (!result.valid) {
|
|
216
|
+
return result.error || `Invalid value for ${propertySchema.title || key}`
|
|
217
|
+
}
|
|
218
|
+
return true
|
|
219
|
+
},
|
|
220
|
+
promptFactId
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Store the value
|
|
224
|
+
storedConfig[key] = value
|
|
225
|
+
await this.setStoredConfig(storedConfig)
|
|
226
|
+
|
|
227
|
+
console.log(chalk.green(`\n ✓ ${propertySchema.title || key} saved to connection config\n`))
|
|
228
|
+
|
|
229
|
+
return value
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}, {
|
|
235
|
+
importMeta: import.meta,
|
|
236
|
+
importStack: makeImportStack(),
|
|
237
|
+
capsuleName: capsule['#'],
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
capsule['#'] = 't44/caps/WorkspaceConnection.v0'
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
config: {
|
|
20
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
21
|
+
value: async function (this: any): Promise<void> {
|
|
22
|
+
|
|
23
|
+
const config = await this.WorkspaceConfig.config
|
|
24
|
+
|
|
25
|
+
const configKey = '#' + this.capsuleName
|
|
26
|
+
|
|
27
|
+
const entityConfig = config[configKey]
|
|
28
|
+
if (entityConfig) {
|
|
29
|
+
const now = new Date().toISOString()
|
|
30
|
+
if (!entityConfig.createdAt) {
|
|
31
|
+
await this.WorkspaceConfig.setConfigValue([configKey, 'createdAt'], now)
|
|
32
|
+
}
|
|
33
|
+
if (!entityConfig.updatedAt) {
|
|
34
|
+
await this.WorkspaceConfig.setConfigValue([configKey, 'updatedAt'], now)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return entityConfig || undefined
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
setConfigValue: {
|
|
42
|
+
type: CapsulePropertyTypes.Function,
|
|
43
|
+
value: async function (this: any, path: string[], value: any): Promise<void> {
|
|
44
|
+
|
|
45
|
+
const configKey = '#' + this.capsuleName
|
|
46
|
+
|
|
47
|
+
await this.WorkspaceConfig.setConfigValue([configKey, 'createdAt'], new Date().toISOString(), { ifAbsent: true })
|
|
48
|
+
|
|
49
|
+
const changed = await this.WorkspaceConfig.setConfigValue([configKey, ...path], value)
|
|
50
|
+
|
|
51
|
+
if (changed) {
|
|
52
|
+
await this.WorkspaceConfig.setConfigValue([configKey, 'updatedAt'], new Date().toISOString())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, {
|
|
59
|
+
importMeta: import.meta,
|
|
60
|
+
importStack: makeImportStack(),
|
|
61
|
+
capsuleName: capsule['#'],
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
capsule['#'] = 't44/caps/WorkspaceEntityConfig.v0'
|
|
@@ -0,0 +1,193 @@
|
|
|
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.v0': {},
|
|
18
|
+
'#': {
|
|
19
|
+
WorkspaceConfig: {
|
|
20
|
+
type: CapsulePropertyTypes.Mapping,
|
|
21
|
+
value: 't44/caps/WorkspaceConfig.v0'
|
|
22
|
+
},
|
|
23
|
+
origin: {
|
|
24
|
+
type: CapsulePropertyTypes.Literal,
|
|
25
|
+
value: undefined,
|
|
26
|
+
},
|
|
27
|
+
getFilepath: {
|
|
28
|
+
type: CapsulePropertyTypes.Function,
|
|
29
|
+
value: function (this: any, factType: string, instanceName: string): string {
|
|
30
|
+
return join(
|
|
31
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
32
|
+
this.getRelativeFilepath(factType, instanceName)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
getRelativeFilepath: {
|
|
37
|
+
type: CapsulePropertyTypes.Function,
|
|
38
|
+
value: function (this: any, factType: string, instanceName: string): string {
|
|
39
|
+
return join(
|
|
40
|
+
'.~o',
|
|
41
|
+
'workspace.foundation',
|
|
42
|
+
'WorkspaceEntityFacts',
|
|
43
|
+
'o',
|
|
44
|
+
this.origin,
|
|
45
|
+
factType,
|
|
46
|
+
instanceName + '.json'
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
get: {
|
|
51
|
+
type: CapsulePropertyTypes.Function,
|
|
52
|
+
value: async function (this: any, factType: string, instanceName: string, schemaName: string, rawFilepaths?: string[]): Promise<{ data: any; stale: boolean } | null> {
|
|
53
|
+
const factFilepath = this.getFilepath(factType, instanceName)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const factStat = await stat(factFilepath)
|
|
57
|
+
const factMtime = factStat.mtimeMs
|
|
58
|
+
|
|
59
|
+
// Check if any raw filepaths are newer than our cached fact
|
|
60
|
+
let stale = false
|
|
61
|
+
if (rawFilepaths && rawFilepaths.length > 0) {
|
|
62
|
+
for (const rawPath of rawFilepaths) {
|
|
63
|
+
const fullRawPath = rawPath.startsWith('/')
|
|
64
|
+
? rawPath
|
|
65
|
+
: join(this.WorkspaceConfig.workspaceRootDir, rawPath)
|
|
66
|
+
try {
|
|
67
|
+
const rawStat = await stat(fullRawPath)
|
|
68
|
+
if (rawStat.mtimeMs > factMtime) {
|
|
69
|
+
stale = true
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Raw file doesn't exist, consider stale
|
|
74
|
+
stale = true
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const content = await readFile(factFilepath, 'utf-8')
|
|
81
|
+
const parsed = JSON.parse(content)
|
|
82
|
+
const data = parsed[schemaName]
|
|
83
|
+
|
|
84
|
+
return { data, stale }
|
|
85
|
+
} catch {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
set: {
|
|
91
|
+
type: CapsulePropertyTypes.Function,
|
|
92
|
+
value: async function (this: any, factType: string, instanceName: string, schemaName: string, data: any): Promise<void> {
|
|
93
|
+
|
|
94
|
+
if (this.schema?.definitions) {
|
|
95
|
+
const schemaRef = this.schema.definitions[schemaName]
|
|
96
|
+
if (!schemaRef) {
|
|
97
|
+
throw new Error(`Schema name "${schemaName}" not found in definitions. Available: ${Object.keys(this.schema.definitions).join(', ')}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const factDir = join(
|
|
102
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
103
|
+
'.~o',
|
|
104
|
+
'workspace.foundation',
|
|
105
|
+
'WorkspaceEntityFacts',
|
|
106
|
+
'o',
|
|
107
|
+
this.origin,
|
|
108
|
+
factType
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
await mkdir(factDir, { recursive: true })
|
|
112
|
+
|
|
113
|
+
let validationFeedback = null
|
|
114
|
+
if (this.schema?.validate) {
|
|
115
|
+
validationFeedback = await this.schema.validate(schemaName, data)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let schemaFilePath: string | undefined
|
|
119
|
+
if (this.schema?.definitions && this.capsuleName) {
|
|
120
|
+
const jsonSchemaDir = join(
|
|
121
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
122
|
+
'.~o',
|
|
123
|
+
'workspace.foundation',
|
|
124
|
+
'WorkspaceEntityFacts',
|
|
125
|
+
'JsonSchemas'
|
|
126
|
+
)
|
|
127
|
+
await mkdir(jsonSchemaDir, { recursive: true })
|
|
128
|
+
|
|
129
|
+
const schemaFilename = this.capsuleName.replace(/\//g, '~') + '.json'
|
|
130
|
+
schemaFilePath = join(jsonSchemaDir, schemaFilename)
|
|
131
|
+
|
|
132
|
+
const schemaOutput: Record<string, any> = {
|
|
133
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
134
|
+
$id: this.capsuleName,
|
|
135
|
+
$defs: {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const defName of Object.keys(this.schema.definitions)) {
|
|
139
|
+
const resolved = this.schema.resolveDefinition
|
|
140
|
+
? await this.schema.resolveDefinition(defName)
|
|
141
|
+
: null
|
|
142
|
+
schemaOutput.$defs[defName] = resolved || this.schema.definitions[defName]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await writeFile(schemaFilePath, JSON.stringify(schemaOutput, null, 4))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const output: Record<string, any> = {
|
|
149
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (schemaFilePath && this.capsuleName) {
|
|
153
|
+
output.$defs = {
|
|
154
|
+
[schemaName]: {
|
|
155
|
+
$ref: this.capsuleName + '#/$defs/' + schemaName
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
output[schemaName] = data
|
|
161
|
+
|
|
162
|
+
if (validationFeedback && (validationFeedback.warnings.length > 0 || validationFeedback.errors.length > 0)) {
|
|
163
|
+
output[schemaName + '_ValidationFeedback'] = validationFeedback
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await writeFile(join(factDir, instanceName + '.json'), JSON.stringify(output, null, 4))
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
delete: {
|
|
170
|
+
type: CapsulePropertyTypes.Function,
|
|
171
|
+
value: async function (this: any, factType: string, instanceName: string): Promise<void> {
|
|
172
|
+
const { unlink } = await import('fs/promises')
|
|
173
|
+
const factFilepath = this.getFilepath(factType, instanceName)
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await unlink(factFilepath)
|
|
177
|
+
} catch (error: any) {
|
|
178
|
+
// Ignore if file doesn't exist
|
|
179
|
+
if (error.code !== 'ENOENT') {
|
|
180
|
+
throw error
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}, {
|
|
188
|
+
importMeta: import.meta,
|
|
189
|
+
importStack: makeImportStack(),
|
|
190
|
+
capsuleName: capsule['#'],
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
capsule['#'] = 't44/caps/WorkspaceEntityFact.v0'
|