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.

Files changed (86) hide show
  1. package/LICENSE.md +203 -0
  2. package/README.md +154 -0
  3. package/bin/activate +36 -0
  4. package/bin/activate.ts +30 -0
  5. package/bin/postinstall.sh +19 -0
  6. package/bin/shell +27 -0
  7. package/bin/t44 +27 -0
  8. package/caps/HomeRegistry.v0.ts +298 -0
  9. package/caps/OpenApiSchema.v0.ts +192 -0
  10. package/caps/ProjectDeployment.v0.ts +363 -0
  11. package/caps/ProjectDevelopment.v0.ts +246 -0
  12. package/caps/ProjectPublishing.v0.ts +307 -0
  13. package/caps/ProjectRack.v0.ts +128 -0
  14. package/caps/WorkspaceCli.v0.ts +391 -0
  15. package/caps/WorkspaceConfig.v0.ts +626 -0
  16. package/caps/WorkspaceConfig.yaml +53 -0
  17. package/caps/WorkspaceConnection.v0.ts +240 -0
  18. package/caps/WorkspaceEntityConfig.v0.ts +64 -0
  19. package/caps/WorkspaceEntityFact.v0.ts +193 -0
  20. package/caps/WorkspaceInfo.v0.ts +554 -0
  21. package/caps/WorkspaceInit.v0.ts +30 -0
  22. package/caps/WorkspaceKey.v0.ts +186 -0
  23. package/caps/WorkspaceProjects.v0.ts +455 -0
  24. package/caps/WorkspacePrompt.v0.ts +396 -0
  25. package/caps/WorkspaceShell.sh +39 -0
  26. package/caps/WorkspaceShell.v0.ts +104 -0
  27. package/caps/WorkspaceShell.yaml +65 -0
  28. package/caps/WorkspaceShellCli.v0.ts +109 -0
  29. package/caps/WorkspaceTest.v0.ts +167 -0
  30. package/caps/providers/LICENSE.md +8 -0
  31. package/caps/providers/README.md +2 -0
  32. package/caps/providers/bunny.net/ProjectDeployment.v0.ts +328 -0
  33. package/caps/providers/bunny.net/api-pull.v0.test.ts +319 -0
  34. package/caps/providers/bunny.net/api-pull.v0.ts +161 -0
  35. package/caps/providers/bunny.net/api-storage.v0.test.ts +168 -0
  36. package/caps/providers/bunny.net/api-storage.v0.ts +245 -0
  37. package/caps/providers/bunny.net/api.v0.ts +95 -0
  38. package/caps/providers/dynadot.com/ProjectDeployment.v0.ts +207 -0
  39. package/caps/providers/dynadot.com/api-domains.v0.test.ts +147 -0
  40. package/caps/providers/dynadot.com/api-domains.v0.ts +137 -0
  41. package/caps/providers/dynadot.com/api.v0.ts +88 -0
  42. package/caps/providers/git-scm.com/ProjectPublishing.v0.ts +231 -0
  43. package/caps/providers/github.com/ProjectPublishing.v0.ts +75 -0
  44. package/caps/providers/github.com/api.v0.ts +90 -0
  45. package/caps/providers/npmjs.com/ProjectPublishing.v0.ts +741 -0
  46. package/caps/providers/vercel.com/ProjectDeployment.v0.ts +339 -0
  47. package/caps/providers/vercel.com/api.v0.test.ts +67 -0
  48. package/caps/providers/vercel.com/api.v0.ts +132 -0
  49. package/caps/providers/vercel.com/bun.lock +194 -0
  50. package/caps/providers/vercel.com/package.json +10 -0
  51. package/caps/providers/vercel.com/project.v0.test.ts +108 -0
  52. package/caps/providers/vercel.com/project.v0.ts +150 -0
  53. package/caps/providers/vercel.com/tsconfig.json +28 -0
  54. package/docs/Overview.drawio +189 -0
  55. package/docs/Overview.svg +4 -0
  56. package/lib/crypto.ts +53 -0
  57. package/lib/openapi.ts +132 -0
  58. package/lib/ucan.ts +137 -0
  59. package/package.json +41 -0
  60. package/structs/HomeRegistryConfig.v0.ts +27 -0
  61. package/structs/ProjectDeploymentConfig.v0.ts +27 -0
  62. package/structs/ProjectDeploymentFact.v0.ts +110 -0
  63. package/structs/ProjectPublishingFact.v0.ts +69 -0
  64. package/structs/ProjectRackConfig.v0.ts +27 -0
  65. package/structs/WorkspaceCliConfig.v0.ts +27 -0
  66. package/structs/WorkspaceConfig.v0.ts +27 -0
  67. package/structs/WorkspaceKeyConfig.v0.ts +27 -0
  68. package/structs/WorkspaceMappings.v0.ts +27 -0
  69. package/structs/WorkspaceProjectsConfig.v0.ts +27 -0
  70. package/structs/WorkspaceRepositories.v0.ts +27 -0
  71. package/structs/WorkspaceShellConfig.v0.ts +45 -0
  72. package/structs/providers/LICENSE.md +8 -0
  73. package/structs/providers/README.md +2 -0
  74. package/structs/providers/bunny.net/ProjectDeploymentFact.v0.ts +41 -0
  75. package/structs/providers/bunny.net/WorkspaceConnectionConfig.v0.ts +42 -0
  76. package/structs/providers/dynadot.com/DomainFact.v0.ts +146 -0
  77. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0.ts +41 -0
  78. package/structs/providers/git-scm.com/ProjectPublishingFact.v0.ts +46 -0
  79. package/structs/providers/github.com/ProjectPublishingFact.v0.ts +52 -0
  80. package/structs/providers/github.com/WorkspaceConnectionConfig.v0.ts +42 -0
  81. package/structs/providers/npmjs.com/ProjectPublishingFact.v0.ts +48 -0
  82. package/structs/providers/vercel.com/ProjectDeploymentFact.v0.ts +38 -0
  83. package/structs/providers/vercel.com/WorkspaceConnectionConfig.v0.ts +48 -0
  84. package/tsconfig.json +28 -0
  85. package/workspace-rt.ts +134 -0
  86. 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'