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,109 @@
1
+
2
+ import { Command } from 'commander'
3
+ import { $ } from 'bun'
4
+
5
+ export async function capsule({
6
+ encapsulate,
7
+ CapsulePropertyTypes,
8
+ makeImportStack
9
+ }: {
10
+ encapsulate: any
11
+ CapsulePropertyTypes: any
12
+ makeImportStack: any
13
+ }) {
14
+ return encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
17
+ '#t44/structs/WorkspaceCliConfig.v0': {
18
+ as: '$WorkspaceCliConfig'
19
+ },
20
+ '#': {
21
+ WorkspaceConfig: {
22
+ type: CapsulePropertyTypes.Mapping,
23
+ value: 't44/caps/WorkspaceConfig.v0'
24
+ },
25
+ shellCommands: {
26
+ type: CapsulePropertyTypes.GetterFunction,
27
+ value: async function (this: any): Promise<object> {
28
+
29
+ const config = await this.WorkspaceConfig.config as any
30
+ const self = this
31
+
32
+ const commands: Record<string, (commandArgs?: any) => Promise<void>> = {}
33
+ for (const commandName in config.shell.commands) {
34
+ const commandConfig = config.shell.commands[commandName]
35
+
36
+ commands[commandName] = async function () {
37
+ throw new Error(`Shell commands cannot be run directly! They must be sourced into the shell.`)
38
+ }
39
+ }
40
+ return commands
41
+ }
42
+ },
43
+ runCli: {
44
+ type: CapsulePropertyTypes.Function,
45
+ value: async function (this: any, argv: string[]): Promise<void> {
46
+
47
+ const config = await this.WorkspaceConfig.config as any
48
+ const cliConfig = await this.$WorkspaceCliConfig.config
49
+ const shellCommands = await this.shellCommands as Record<string, (args?: any) => Promise<void>>
50
+
51
+ const program = new Command()
52
+ .option('--yes', 'Confirm all questions with default values.')
53
+
54
+ for (const commandName in config.shell.commands) {
55
+ const commandConfig = config.shell.commands[commandName]
56
+
57
+ // If this is a cliCommand reference, pull description and arguments from CLI command
58
+ let description = commandConfig.description || ''
59
+ let commandArgs = commandConfig.arguments
60
+ let commandOptions = commandConfig.options
61
+
62
+ if (commandConfig.cliCommand) {
63
+ const cliCommandName = commandConfig.cliCommand
64
+ const cliCommand = cliConfig?.cli?.commands?.[cliCommandName]
65
+ if (cliCommand) {
66
+ description = cliCommand.description || description
67
+ commandArgs = cliCommand.arguments || commandArgs
68
+ commandOptions = cliCommand.options || commandOptions
69
+ }
70
+ }
71
+
72
+ const cmd = program
73
+ .command(commandName)
74
+ .description(description)
75
+
76
+ // Add arguments if defined
77
+ if (commandArgs) {
78
+ for (const argName in commandArgs) {
79
+ const argConfig = commandArgs[argName]
80
+ const argSyntax = argConfig.optional ? `[${argName}]` : `<${argName}>`
81
+ cmd.argument(argSyntax, argConfig.description || '')
82
+ }
83
+ }
84
+
85
+ // Add options if defined
86
+ if (commandOptions) {
87
+ for (const optionName in commandOptions) {
88
+ const optionConfig = commandOptions[optionName]
89
+ cmd.option(`--${optionName}`, optionConfig.description || '')
90
+ }
91
+ }
92
+
93
+ cmd.action(async function (...actionArgs) {
94
+ throw new Error(`Shell commands cannot be run directly! They must be sourced into the shell.`)
95
+ })
96
+ }
97
+
98
+ await program.parseAsync(argv)
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }, {
104
+ importMeta: import.meta,
105
+ importStack: makeImportStack(),
106
+ capsuleName: capsule['#'],
107
+ })
108
+ }
109
+ capsule['#'] = 't44/caps/WorkspaceShellCli.v0'
@@ -0,0 +1,167 @@
1
+
2
+ import type * as BunTest from 'bun:test'
3
+ import { config as loadDotenv } from 'dotenv'
4
+ import { join } from 'path'
5
+
6
+ // Global cache for loaded env files (this is fine as a cache)
7
+ const loadedEnvFiles = new Set<string>()
8
+
9
+ export async function capsule({
10
+ encapsulate,
11
+ CapsulePropertyTypes,
12
+ makeImportStack
13
+ }: {
14
+ encapsulate: any
15
+ CapsulePropertyTypes: any
16
+ makeImportStack: any
17
+ }) {
18
+ return encapsulate({
19
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
20
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
21
+ '#': {
22
+ bunTest: {
23
+ type: CapsulePropertyTypes.Literal,
24
+ value: undefined as any as typeof BunTest,
25
+ },
26
+ env: {
27
+ type: CapsulePropertyTypes.Literal,
28
+ value: undefined
29
+ },
30
+ testRootDir: {
31
+ type: CapsulePropertyTypes.Literal,
32
+ value: undefined as string | undefined,
33
+ },
34
+ _envLoaded: {
35
+ type: CapsulePropertyTypes.Literal,
36
+ value: false,
37
+ },
38
+ loadEnvFiles: {
39
+ type: CapsulePropertyTypes.Function,
40
+ value: function (this: any, cwd: string): void {
41
+ if (this._envLoaded) return
42
+
43
+ // Load .env file if it exists
44
+ const envPath = join(cwd, '.env')
45
+ if (!loadedEnvFiles.has(envPath)) {
46
+ loadDotenv({ path: envPath, quiet: true })
47
+ loadedEnvFiles.add(envPath)
48
+ }
49
+
50
+ // Load .env.dev file if it exists
51
+ const envDevPath = join(cwd, '.env.dev')
52
+ if (!loadedEnvFiles.has(envDevPath)) {
53
+ loadDotenv({ path: envDevPath, quiet: true })
54
+ loadedEnvFiles.add(envDevPath)
55
+ }
56
+
57
+ this._envLoaded = true
58
+ }
59
+ },
60
+ getEnvValue: {
61
+ type: CapsulePropertyTypes.Function,
62
+ value: function (this: any, envVarName: string): string | undefined {
63
+ // Auto-load env files from testRootDir if available
64
+ if (this.testRootDir) {
65
+ this.loadEnvFiles(this.testRootDir)
66
+ }
67
+ return process.env[envVarName]
68
+ }
69
+ },
70
+ describe: {
71
+ type: CapsulePropertyTypes.GetterFunction,
72
+ value: function (this: any) {
73
+ const bunTestModule = this.bunTest
74
+ const describeMethod = (name: string, fn: () => void) => {
75
+ return bunTestModule.describe(name, async () => {
76
+ await fn()
77
+ })
78
+ }
79
+ describeMethod.skip = (name: string, fn: () => void) => {
80
+ return bunTestModule.describe.skip(name, async () => {
81
+ await fn()
82
+ })
83
+ }
84
+ return describeMethod
85
+ }
86
+ },
87
+ it: {
88
+ type: CapsulePropertyTypes.GetterFunction,
89
+ value: function (this: any) {
90
+ const bunTestModule = this.bunTest
91
+ const itMethod = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
92
+ return bunTestModule.it(name, async () => {
93
+ await fn()
94
+ }, options)
95
+ }
96
+ itMethod.skip = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
97
+ return bunTestModule.it.skip(name, async () => {
98
+ await fn()
99
+ }, options)
100
+ }
101
+ return itMethod
102
+ }
103
+ },
104
+ test: {
105
+ type: CapsulePropertyTypes.GetterFunction,
106
+ value: function (this: any) {
107
+ const bunTestModule = this.bunTest
108
+ const testMethod = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
109
+ return bunTestModule.test(name, async () => {
110
+ await fn()
111
+ }, options)
112
+ }
113
+ testMethod.skip = (name: string, fn: () => void | Promise<void>, options?: number | BunTest.TestOptions) => {
114
+ return bunTestModule.test.skip(name, async () => {
115
+ await fn()
116
+ }, options)
117
+ }
118
+ return testMethod
119
+ }
120
+ },
121
+ expect: {
122
+ type: CapsulePropertyTypes.GetterFunction,
123
+ value: function (this: any): typeof BunTest.expect {
124
+ return this.bunTest.expect
125
+ }
126
+ },
127
+ beforeAll: {
128
+ type: CapsulePropertyTypes.Function,
129
+ value: function (this: any, fn: () => void | Promise<void>) {
130
+ return this.bunTest.beforeAll(async () => {
131
+ await fn()
132
+ })
133
+ }
134
+ },
135
+ afterAll: {
136
+ type: CapsulePropertyTypes.Function,
137
+ value: function (this: any, fn: () => void | Promise<void>) {
138
+ return this.bunTest.afterAll(async () => {
139
+ await fn()
140
+ })
141
+ }
142
+ },
143
+ beforeEach: {
144
+ type: CapsulePropertyTypes.Function,
145
+ value: function (this: any, fn: () => void | Promise<void>) {
146
+ return this.bunTest.beforeEach(async () => {
147
+ await fn()
148
+ })
149
+ }
150
+ },
151
+ afterEach: {
152
+ type: CapsulePropertyTypes.Function,
153
+ value: function (this: any, fn: () => void | Promise<void>) {
154
+ return this.bunTest.afterEach(async () => {
155
+ await fn()
156
+ })
157
+ }
158
+ },
159
+ }
160
+ }
161
+ }, {
162
+ importMeta: import.meta,
163
+ importStack: makeImportStack(),
164
+ capsuleName: capsule['#'],
165
+ })
166
+ }
167
+ capsule['#'] = 't44/caps/WorkspaceTest.v0'
@@ -0,0 +1,8 @@
1
+
2
+ Copyright 2026 Christoph.diy
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,2 @@
1
+
2
+ **NOTE: Code in this directory will be relocated to a different project under the same `MIT` license in future.**
@@ -0,0 +1,328 @@
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
+ '#t44/structs/ProjectDeploymentFact.v0': {
15
+ as: '$StatusFact'
16
+ },
17
+ '#t44/structs/providers/bunny.net/ProjectDeploymentFact.v0': {
18
+ as: '$BunnyFact'
19
+ },
20
+ '#t44/structs/ProjectDeploymentConfig.v0': {
21
+ as: '$ProjectDeploymentConfig'
22
+ },
23
+ '#': {
24
+ WorkspacePrompt: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: 't44/caps/WorkspacePrompt.v0'
27
+ },
28
+ storage: {
29
+ type: CapsulePropertyTypes.Mapping,
30
+ value: './api-storage.v0'
31
+ },
32
+ pull: {
33
+ type: CapsulePropertyTypes.Mapping,
34
+ value: './api-pull.v0'
35
+ },
36
+ deploy: {
37
+ type: CapsulePropertyTypes.Function,
38
+ value: async function (this: any, { projectionDir, alias, config, workspaceProjectName }: { projectionDir: string, alias: string, config: any, workspaceProjectName?: string }) {
39
+ let projectName = config.provider.config.ProjectSettings.name
40
+ const region = config.provider.config.ProjectSettings.region || 'LA'
41
+
42
+ console.log(`Deploying '${projectName}' to Bunny.net CDN ...`)
43
+
44
+ console.log(`Ensuring storage zone '${projectName}' exists ...`)
45
+
46
+ let storageZone: any
47
+ let retryCount = 0
48
+ const maxRetries = 3
49
+
50
+ while (retryCount < maxRetries) {
51
+ try {
52
+ storageZone = await this.storage.ensureZone({
53
+ name: projectName,
54
+ region: region
55
+ })
56
+ break
57
+ } catch (error: any) {
58
+ const errorMessage = error.message || ''
59
+
60
+ // Check if it's a zone name conflict error
61
+ if (errorMessage.includes('storagezone.name_taken') ||
62
+ errorMessage.includes('storage zone is currently being deleted')) {
63
+
64
+ const chalk = (await import('chalk')).default
65
+
66
+ console.log(chalk.yellow(`\n⚠️ WARNING: Storage zone name '${projectName}' is already taken.\n`))
67
+ console.log(chalk.gray(` Deleted zones may remain permanently reserved.`))
68
+ console.log(chalk.gray(` Please choose a different project name.\n`))
69
+
70
+ try {
71
+ const newProjectName = await this.WorkspacePrompt.input({
72
+ message: 'Enter a new project name:',
73
+ defaultValue: `${projectName}-${Date.now()}`,
74
+ validate: (input: string) => {
75
+ if (!input || input.trim().length === 0) {
76
+ return 'Project name cannot be empty'
77
+ }
78
+ if (!/^[a-z0-9-]+$/.test(input)) {
79
+ return 'Project name must contain only lowercase letters, numbers, and hyphens'
80
+ }
81
+ return true
82
+ }
83
+ })
84
+
85
+ // Update the project name in config
86
+ projectName = newProjectName
87
+ config.provider.config.ProjectSettings.name = newProjectName
88
+
89
+ // Save the updated config back to the workspace config
90
+ if (workspaceProjectName) {
91
+ const configPath = ['deployments', workspaceProjectName, alias, 'provider', 'config', 'ProjectSettings', 'name']
92
+ await this.$ProjectDeploymentConfig.setConfigValue(configPath, newProjectName)
93
+ }
94
+
95
+ console.log(chalk.green(`\n✓ Updated project name to: ${newProjectName}\n`))
96
+
97
+ retryCount++
98
+ continue
99
+
100
+ } catch (promptError: any) {
101
+ if (promptError.message?.includes('SIGINT') || promptError.message?.includes('force closed')) {
102
+ console.log(chalk.red('\nABORTED\n'))
103
+ throw new Error('Deployment aborted by user')
104
+ }
105
+ throw promptError
106
+ }
107
+ } else {
108
+ throw error
109
+ }
110
+ }
111
+ }
112
+
113
+ if (!storageZone) {
114
+ throw new Error('Failed to create storage zone after multiple attempts')
115
+ }
116
+
117
+ console.log(`Storage Zone ID: ${storageZone.Id}`)
118
+
119
+ console.log(`Ensuring pull zone '${projectName}' exists ...`)
120
+ const pullZone = await this.pull.ensureZone({
121
+ name: projectName,
122
+ originUrl: `https://${storageZone.StorageHostname}/${projectName}`,
123
+ storageZoneId: storageZone.Id
124
+ })
125
+ console.log(`Pull Zone ID: ${pullZone.Id}`)
126
+
127
+ const publicUrl = pullZone.Hostnames?.[0]?.Value
128
+ ? `https://${pullZone.Hostnames[0].Value}`
129
+ : `https://${projectName}.b-cdn.net`
130
+ console.log(`Public URL: ${publicUrl}`)
131
+
132
+ console.log(`Uploading files from ${config.sourceDir} ...`)
133
+ await this.storage.uploadDirectory({
134
+ sourceDirectory: config.sourceDir,
135
+ destinationDirectory: '',
136
+ storageZoneName: projectName,
137
+ password: storageZone.Password,
138
+ region: region.toLowerCase(),
139
+ cleanDestination: 'avoid-deletes'
140
+ })
141
+ console.log(`Files uploaded successfully`)
142
+
143
+ console.log(`Purging CDN cache ...`)
144
+ await this.pull.purgeZone(pullZone.Id)
145
+ console.log(`Cache purged`)
146
+
147
+ console.log(`Deployment complete: ${publicUrl}`)
148
+
149
+ // Write deployment status with updatedAt
150
+ const statusResult = {
151
+ projectName,
152
+ provider: 'bunny.net',
153
+ status: 'READY',
154
+ publicUrl,
155
+ '#t44/structs/ProjectDeploymentConfig.v0': {
156
+ updatedAt: new Date().toISOString()
157
+ }
158
+ }
159
+ await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', statusResult)
160
+ }
161
+ },
162
+ deprovision: {
163
+ type: CapsulePropertyTypes.Function,
164
+ value: async function (this: any, { config }: { config: any }) {
165
+ const projectName = config.provider.config.ProjectSettings.name
166
+
167
+ console.log(`Deprovisioning '${projectName}' from Bunny.net ...`)
168
+
169
+ try {
170
+ const pullZones = await this.pull.listZones({ search: projectName })
171
+ const zones = pullZones.Items || pullZones
172
+ const pullZone = zones.find((zone: any) => zone.Name === projectName)
173
+
174
+ if (pullZone) {
175
+ console.log(`Deleting pull zone '${projectName}' (ID: ${pullZone.Id}) ...`)
176
+ await this.pull.deleteZone(pullZone.Id)
177
+ console.log(`Pull zone deleted`)
178
+ } else {
179
+ console.log(`Pull zone '${projectName}' not found`)
180
+ }
181
+ } catch (error: any) {
182
+ console.log(`Error deleting pull zone: ${error.message}`)
183
+ }
184
+
185
+ try {
186
+ const storageZones = await this.storage.listZones({ search: projectName })
187
+ const storageZone = storageZones.find((zone: any) => zone.Name === projectName)
188
+
189
+ if (storageZone) {
190
+ console.log(`Deleting storage zone '${projectName}' (ID: ${storageZone.Id}) ...`)
191
+ await this.storage.deleteZone(storageZone.Id)
192
+ console.log(`Storage zone deleted`)
193
+ } else {
194
+ console.log(`Storage zone '${projectName}' not found`)
195
+ }
196
+ } catch (error: any) {
197
+ console.log(`Error deleting storage zone: ${error.message}`)
198
+ }
199
+
200
+ // Delete fact files
201
+ console.log(`Deleting fact files ...`)
202
+ try {
203
+ await this.$BunnyFact.delete('storage-zones', projectName)
204
+ await this.$BunnyFact.delete('pull-zones', projectName)
205
+ await this.$StatusFact.delete('ProjectDeploymentStatus', projectName)
206
+ console.log(`Fact files deleted`)
207
+ } catch (error: any) {
208
+ console.log(`Error deleting fact files: ${error.message}`)
209
+ }
210
+
211
+ console.log(`Deprovision complete`)
212
+ }
213
+ },
214
+ status: {
215
+ type: CapsulePropertyTypes.Function,
216
+ value: async function (this: any, { config, now, passive }: { config: any; now?: boolean; passive?: boolean }) {
217
+ const projectName = config.provider.config.ProjectSettings.name
218
+
219
+ if (!projectName) {
220
+ return {
221
+ projectName: projectName || 'unknown',
222
+ provider: 'bunny.net',
223
+ error: 'No project name configured',
224
+ rawDefinitionFilepaths: []
225
+ }
226
+ }
227
+
228
+ // Raw fact filepaths that this status depends on (specific zone files)
229
+ const rawFilepaths = [
230
+ this.$BunnyFact.getRelativeFilepath('storage-zones', projectName),
231
+ this.$BunnyFact.getRelativeFilepath('pull-zones', projectName)
232
+ ]
233
+
234
+ // Try to get cached status if not forcing refresh
235
+ if (!now) {
236
+ const cached = await this.$StatusFact.get('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', rawFilepaths)
237
+ if (cached) {
238
+ return cached.data
239
+ }
240
+ }
241
+
242
+ // In passive mode, don't call the provider if no cache exists
243
+ if (passive) {
244
+ return null
245
+ }
246
+
247
+ const storageZones = await this.storage.listZones({ search: projectName })
248
+ const storageZoneMatch = storageZones.find((zone: any) => zone.Name === projectName)
249
+
250
+ if (!storageZoneMatch) {
251
+ // Write placeholder fact files so cache can be hit on subsequent calls
252
+ await this.$BunnyFact.set('storage-zones', projectName, 'StorageZoneModel', { notFound: true, name: projectName })
253
+ await this.$BunnyFact.set('pull-zones', projectName, 'PullZoneModel', { notFound: true, name: projectName })
254
+ const errorResult = {
255
+ projectName,
256
+ provider: 'bunny.net',
257
+ error: 'Storage zone not found',
258
+ rawDefinitionFilepaths: rawFilepaths
259
+ }
260
+ await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', errorResult)
261
+ return errorResult
262
+ }
263
+
264
+ const pullZones = await this.pull.listZones({ search: projectName })
265
+ const zones = pullZones.Items || pullZones
266
+ const pullZoneMatch = zones.find((zone: any) => zone.Name === projectName)
267
+
268
+ if (!pullZoneMatch) {
269
+ // Write placeholder fact file so cache can be hit on subsequent calls
270
+ await this.$BunnyFact.set('pull-zones', projectName, 'PullZoneModel', { notFound: true, name: projectName })
271
+ const errorResult = {
272
+ projectName,
273
+ provider: 'bunny.net',
274
+ error: 'Pull zone not found',
275
+ rawDefinitionFilepaths: rawFilepaths
276
+ }
277
+ await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', errorResult)
278
+ return errorResult
279
+ }
280
+
281
+ // Fetch full zone details (this also saves the individual fact files)
282
+ const storageZone = await this.storage.getZone(storageZoneMatch.Id)
283
+ const pullZone = await this.pull.getZone(pullZoneMatch.Id)
284
+
285
+ const publicUrl = pullZone.Hostnames?.[0]?.Value
286
+ ? `https://${pullZone.Hostnames[0].Value}`
287
+ : `https://${projectName}.b-cdn.net`
288
+
289
+ // Preserve updatedAt from existing cached status
290
+ const existingStatus = await this.$StatusFact.get('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus')
291
+ const existingMeta = existingStatus?.data?.['#t44/structs/ProjectDeploymentConfig.v0']
292
+
293
+ const result: Record<string, any> = {
294
+ projectName: projectName,
295
+ provider: 'bunny.net',
296
+ status: pullZone.Enabled ? 'READY' : 'DISABLED',
297
+ publicUrl: publicUrl,
298
+ providerProjectId: `storage:${storageZone.Id}|pull:${pullZone.Id}`,
299
+ providerPortalUrl: `https://dash.bunny.net/cdn/${pullZone.Id}/general/hostnames`,
300
+ usage: {
301
+ storageBytes: storageZone.StorageUsed,
302
+ filesCount: storageZone.FilesStored,
303
+ bandwidthBytes: pullZone.MonthlyBandwidthUsed,
304
+ charges: pullZone.MonthlyCharges
305
+ },
306
+ rawDefinitionFilepaths: rawFilepaths
307
+ }
308
+
309
+ if (existingMeta?.updatedAt) {
310
+ result['#t44/structs/ProjectDeploymentConfig.v0'] = {
311
+ updatedAt: existingMeta.updatedAt
312
+ }
313
+ }
314
+
315
+ await this.$StatusFact.set('ProjectDeploymentStatus', projectName, 'ProjectDeploymentStatus', result)
316
+
317
+ return result
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }, {
323
+ importMeta: import.meta,
324
+ importStack: makeImportStack(),
325
+ capsuleName: capsule['#'],
326
+ })
327
+ }
328
+ capsule['#'] = 't44/caps/providers/bunny.net/ProjectDeployment.v0'