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.
Files changed (127) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yaml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yaml +13 -0
  4. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity.yaml +25 -0
  7. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  8. package/DCO.md +34 -0
  9. package/LICENSE.md +203 -0
  10. package/README.md +185 -0
  11. package/bin/activate +36 -0
  12. package/bin/activate.ts +30 -0
  13. package/bin/postinstall.sh +19 -0
  14. package/bin/shell +27 -0
  15. package/bin/t44 +27 -0
  16. package/caps/ConfigSchemaStruct.ts +55 -0
  17. package/caps/Home.ts +57 -0
  18. package/caps/HomeRegistry.ts +319 -0
  19. package/caps/HomeRegistryFile.ts +144 -0
  20. package/caps/JsonSchemas.ts +220 -0
  21. package/caps/OpenApiSchema.ts +67 -0
  22. package/caps/PackageDescriptor.ts +88 -0
  23. package/caps/ProjectCatalogs.ts +153 -0
  24. package/caps/ProjectDeployment.ts +363 -0
  25. package/caps/ProjectDevelopment.ts +257 -0
  26. package/caps/ProjectPublishing.ts +522 -0
  27. package/caps/ProjectRack.ts +155 -0
  28. package/caps/ProjectRepository.ts +322 -0
  29. package/caps/RootKey.ts +219 -0
  30. package/caps/SigningKey.ts +243 -0
  31. package/caps/WorkspaceCli.ts +442 -0
  32. package/caps/WorkspaceConfig.ts +268 -0
  33. package/caps/WorkspaceConfig.yaml +71 -0
  34. package/caps/WorkspaceConfigFile.ts +807 -0
  35. package/caps/WorkspaceConnection.ts +256 -0
  36. package/caps/WorkspaceEntityConfig.ts +78 -0
  37. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  38. package/caps/WorkspaceEntityFact.ts +218 -0
  39. package/caps/WorkspaceInfo.ts +595 -0
  40. package/caps/WorkspaceInit.ts +30 -0
  41. package/caps/WorkspaceKey.ts +338 -0
  42. package/caps/WorkspaceModel.ts +373 -0
  43. package/caps/WorkspaceProjects.ts +636 -0
  44. package/caps/WorkspacePrompt.ts +406 -0
  45. package/caps/WorkspaceShell.sh +39 -0
  46. package/caps/WorkspaceShell.ts +104 -0
  47. package/caps/WorkspaceShell.yaml +64 -0
  48. package/caps/WorkspaceShellCli.ts +109 -0
  49. package/caps/WorkspaceTest.ts +167 -0
  50. package/caps/providers/README.md +2 -0
  51. package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
  52. package/caps/providers/bunny.net/api-pull.test.ts +319 -0
  53. package/caps/providers/bunny.net/api-pull.ts +164 -0
  54. package/caps/providers/bunny.net/api-storage.test.ts +168 -0
  55. package/caps/providers/bunny.net/api-storage.ts +248 -0
  56. package/caps/providers/bunny.net/api.ts +95 -0
  57. package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
  58. package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
  59. package/caps/providers/dynadot.com/api-domains.ts +169 -0
  60. package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
  61. package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
  62. package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
  63. package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
  64. package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
  65. package/caps/providers/github.com/ProjectPublishing.ts +133 -0
  66. package/caps/providers/github.com/api.ts +130 -0
  67. package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
  68. package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
  69. package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
  70. package/caps/providers/vercel.com/api.test.ts +67 -0
  71. package/caps/providers/vercel.com/api.ts +132 -0
  72. package/caps/providers/vercel.com/bun.lock +194 -0
  73. package/caps/providers/vercel.com/package.json +10 -0
  74. package/caps/providers/vercel.com/project.test.ts +108 -0
  75. package/caps/providers/vercel.com/project.ts +150 -0
  76. package/caps/providers/vercel.com/tsconfig.json +28 -0
  77. package/docs/Overview.drawio +248 -0
  78. package/docs/Overview.svg +4 -0
  79. package/examples/01-Lifecycle/main.test.ts +228 -0
  80. package/lib/crypto.ts +53 -0
  81. package/lib/key.ts +369 -0
  82. package/lib/schema-console-renderer.ts +181 -0
  83. package/lib/schema-resolver.ts +349 -0
  84. package/lib/ucan.ts +137 -0
  85. package/package.json +102 -0
  86. package/standalone-rt.ts +121 -0
  87. package/structs/HomeRegistry.ts +55 -0
  88. package/structs/HomeRegistryConfig.ts +60 -0
  89. package/structs/ProjectCatalogsConfig.ts +53 -0
  90. package/structs/ProjectDeploymentConfig.ts +56 -0
  91. package/structs/ProjectDeploymentFact.ts +106 -0
  92. package/structs/ProjectPublishingFact.ts +68 -0
  93. package/structs/ProjectRack.ts +51 -0
  94. package/structs/ProjectRackConfig.ts +56 -0
  95. package/structs/RepositoryOriginDescriptor.ts +51 -0
  96. package/structs/RootKeyConfig.ts +64 -0
  97. package/structs/SigningKeyConfig.ts +64 -0
  98. package/structs/Workspace.ts +56 -0
  99. package/structs/WorkspaceCatalogs.ts +56 -0
  100. package/structs/WorkspaceCliConfig.ts +53 -0
  101. package/structs/WorkspaceConfig.ts +64 -0
  102. package/structs/WorkspaceConfigFile.ts +50 -0
  103. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  104. package/structs/WorkspaceKey.ts +55 -0
  105. package/structs/WorkspaceKeyConfig.ts +56 -0
  106. package/structs/WorkspaceMappingsConfig.ts +56 -0
  107. package/structs/WorkspaceProject.ts +104 -0
  108. package/structs/WorkspaceProjectsConfig.ts +67 -0
  109. package/structs/WorkspacePublishingConfig.ts +65 -0
  110. package/structs/WorkspaceShellConfig.ts +83 -0
  111. package/structs/providers/README.md +2 -0
  112. package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
  113. package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
  114. package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
  115. package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
  116. package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
  117. package/structs/providers/dynadot.com/DomainFact.ts +46 -0
  118. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
  119. package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
  120. package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
  121. package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
  122. package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
  123. package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
  124. package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
  125. package/tsconfig.json +28 -0
  126. package/workspace-rt.ts +134 -0
  127. package/workspace.yaml +3 -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': {},
17
+ '#t44/structs/WorkspaceCliConfig': {
18
+ as: '$WorkspaceCliConfig'
19
+ },
20
+ '#': {
21
+ WorkspaceConfig: {
22
+ type: CapsulePropertyTypes.Mapping,
23
+ value: 't44/caps/WorkspaceConfig'
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'
@@ -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': {},
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'
@@ -0,0 +1,2 @@
1
+
2
+ **NOTE: Code in this directory will be relocated to a different project in future.**
@@ -0,0 +1,327 @@
1
+
2
+ export async function capsule({
3
+ encapsulate,
4
+ CapsulePropertyTypes,
5
+ makeImportStack
6
+ }: {
7
+ encapsulate: any
8
+ CapsulePropertyTypes: any
9
+ makeImportStack: any
10
+ }) {
11
+ return encapsulate({
12
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
13
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
14
+ '#t44/structs/ProjectDeploymentFact': {
15
+ as: '$StatusFact'
16
+ },
17
+ '#t44/structs/providers/bunny.net/StorageZoneFact': {
18
+ as: '$StorageZoneFact'
19
+ },
20
+ '#t44/structs/providers/bunny.net/PullZoneFact': {
21
+ as: '$PullZoneFact'
22
+ },
23
+ '#t44/structs/ProjectDeploymentConfig': {
24
+ as: '$ProjectDeploymentConfig'
25
+ },
26
+ '#': {
27
+ WorkspacePrompt: {
28
+ type: CapsulePropertyTypes.Mapping,
29
+ value: 't44/caps/WorkspacePrompt'
30
+ },
31
+ storage: {
32
+ type: CapsulePropertyTypes.Mapping,
33
+ value: './api-storage'
34
+ },
35
+ pull: {
36
+ type: CapsulePropertyTypes.Mapping,
37
+ value: './api-pull'
38
+ },
39
+ deploy: {
40
+ type: CapsulePropertyTypes.Function,
41
+ value: async function (this: any, { projectionDir, alias, config, workspaceProjectName }: { projectionDir: string, alias: string, config: any, workspaceProjectName?: string }) {
42
+ let projectName = config.provider.config.ProjectSettings.name
43
+ const region = config.provider.config.ProjectSettings.region || 'LA'
44
+
45
+ console.log(`Deploying '${projectName}' to Bunny.net CDN ...`)
46
+
47
+ console.log(`Ensuring storage zone '${projectName}' exists ...`)
48
+
49
+ let storageZone: any
50
+ let retryCount = 0
51
+ const maxRetries = 3
52
+
53
+ while (retryCount < maxRetries) {
54
+ try {
55
+ storageZone = await this.storage.ensureZone({
56
+ name: projectName,
57
+ region: region
58
+ })
59
+ break
60
+ } catch (error: any) {
61
+ const errorMessage = error.message || ''
62
+
63
+ // Check if it's a zone name conflict error
64
+ if (errorMessage.includes('storagezone.name_taken') ||
65
+ errorMessage.includes('storage zone is currently being deleted')) {
66
+
67
+ const chalk = (await import('chalk')).default
68
+
69
+ console.log(chalk.yellow(`\n⚠️ WARNING: Storage zone name '${projectName}' is already taken.\n`))
70
+ console.log(chalk.gray(` Deleted zones may remain permanently reserved.`))
71
+ console.log(chalk.gray(` Please choose a different project name.\n`))
72
+
73
+ try {
74
+ const newProjectName = await this.WorkspacePrompt.input({
75
+ message: 'Enter a new project name:',
76
+ defaultValue: `${projectName}-${Date.now()}`,
77
+ validate: (input: string) => {
78
+ if (!input || input.trim().length === 0) {
79
+ return 'Project name cannot be empty'
80
+ }
81
+ if (!/^[a-z0-9-]+$/.test(input)) {
82
+ return 'Project name must contain only lowercase letters, numbers, and hyphens'
83
+ }
84
+ return true
85
+ }
86
+ })
87
+
88
+ // Update the project name in config
89
+ projectName = newProjectName
90
+ config.provider.config.ProjectSettings.name = newProjectName
91
+
92
+ // Save the updated config back to the workspace config
93
+ if (workspaceProjectName) {
94
+ const configPath = ['deployments', workspaceProjectName, alias, 'provider', 'config', 'ProjectSettings', 'name']
95
+ await this.$ProjectDeploymentConfig.setConfigValue(configPath, newProjectName)
96
+ }
97
+
98
+ console.log(chalk.green(`\n✓ Updated project name to: ${newProjectName}\n`))
99
+
100
+ retryCount++
101
+ continue
102
+
103
+ } catch (promptError: any) {
104
+ if (promptError.message?.includes('SIGINT') || promptError.message?.includes('force closed')) {
105
+ console.log(chalk.red('\nABORTED\n'))
106
+ throw new Error('Deployment aborted by user')
107
+ }
108
+ throw promptError
109
+ }
110
+ } else {
111
+ throw error
112
+ }
113
+ }
114
+ }
115
+
116
+ if (!storageZone) {
117
+ throw new Error('Failed to create storage zone after multiple attempts')
118
+ }
119
+
120
+ console.log(`Storage Zone ID: ${storageZone.Id}`)
121
+
122
+ console.log(`Ensuring pull zone '${projectName}' exists ...`)
123
+ const pullZone = await this.pull.ensureZone({
124
+ name: projectName,
125
+ originUrl: `https://${storageZone.StorageHostname}/${projectName}`,
126
+ storageZoneId: storageZone.Id
127
+ })
128
+ console.log(`Pull Zone ID: ${pullZone.Id}`)
129
+
130
+ const publicUrl = pullZone.Hostnames?.[0]?.Value
131
+ ? `https://${pullZone.Hostnames[0].Value}`
132
+ : `https://${projectName}.b-cdn.net`
133
+ console.log(`Public URL: ${publicUrl}`)
134
+
135
+ // Derive upload region from the actual StorageHostname returned by the API.
136
+ // Edge SSD zones use 'storage.bunnycdn.com' (no region prefix).
137
+ // Standard zones use '<region>.storage.bunnycdn.com'.
138
+ const storageHostname: string = storageZone.StorageHostname || ''
139
+ const hostnameMatch = storageHostname.match(/^([^.]+)\.storage\.bunnycdn\.com$/)
140
+ const uploadRegion = (hostnameMatch && hostnameMatch[1] !== 'storage') ? hostnameMatch[1] : undefined
141
+
142
+ console.log(`Uploading files from ${config.sourceDir} ...`)
143
+ await this.storage.uploadDirectory({
144
+ sourceDirectory: config.sourceDir,
145
+ destinationDirectory: '',
146
+ storageZoneName: projectName,
147
+ password: storageZone.Password,
148
+ region: uploadRegion,
149
+ cleanDestination: 'avoid-deletes'
150
+ })
151
+ console.log(`Files uploaded successfully`)
152
+
153
+ console.log(`Purging CDN cache ...`)
154
+ await this.pull.purgeZone(pullZone.Id)
155
+ console.log(`Cache purged`)
156
+
157
+ console.log(`Deployment complete: ${publicUrl}`)
158
+
159
+ const deploymentName = workspaceProjectName || projectName
160
+ const statusResult = {
161
+ projectName: deploymentName,
162
+ provider: 'bunny.net',
163
+ status: 'READY',
164
+ publicUrl
165
+ }
166
+ await this.$StatusFact.set(deploymentName, statusResult)
167
+ }
168
+ },
169
+ deprovision: {
170
+ type: CapsulePropertyTypes.Function,
171
+ value: async function (this: any, { config }: { config: any }) {
172
+ const projectName = config.provider.config.ProjectSettings.name
173
+
174
+ console.log(`Deprovisioning '${projectName}' from Bunny.net ...`)
175
+
176
+ try {
177
+ const pullZones = await this.pull.listZones({ search: projectName })
178
+ const zones = pullZones.Items || pullZones
179
+ const pullZone = zones.find((zone: any) => zone.Name === projectName)
180
+
181
+ if (pullZone) {
182
+ console.log(`Deleting pull zone '${projectName}' (ID: ${pullZone.Id}) ...`)
183
+ await this.pull.deleteZone(pullZone.Id)
184
+ console.log(`Pull zone deleted`)
185
+ } else {
186
+ console.log(`Pull zone '${projectName}' not found`)
187
+ }
188
+ } catch (error: any) {
189
+ console.log(`Error deleting pull zone: ${error.message}`)
190
+ }
191
+
192
+ try {
193
+ const storageZones = await this.storage.listZones({ search: projectName })
194
+ const storageZone = storageZones.find((zone: any) => zone.Name === projectName)
195
+
196
+ if (storageZone) {
197
+ console.log(`Deleting storage zone '${projectName}' (ID: ${storageZone.Id}) ...`)
198
+ await this.storage.deleteZone(storageZone.Id)
199
+ console.log(`Storage zone deleted`)
200
+ } else {
201
+ console.log(`Storage zone '${projectName}' not found`)
202
+ }
203
+ } catch (error: any) {
204
+ console.log(`Error deleting storage zone: ${error.message}`)
205
+ }
206
+
207
+ // Delete fact files
208
+ console.log(`Deleting fact files ...`)
209
+ try {
210
+ await this.$StorageZoneFact.delete(projectName)
211
+ await this.$PullZoneFact.delete(projectName)
212
+ await this.$StatusFact.delete(projectName)
213
+ console.log(`Fact files deleted`)
214
+ } catch (error: any) {
215
+ console.log(`Error deleting fact files: ${error.message}`)
216
+ }
217
+
218
+ console.log(`Deprovision complete`)
219
+ }
220
+ },
221
+ status: {
222
+ type: CapsulePropertyTypes.Function,
223
+ value: async function (this: any, { config, now, passive, deploymentName }: { config: any; now?: boolean; passive?: boolean; deploymentName?: string }) {
224
+ const projectName = config.provider.config.ProjectSettings.name
225
+ const factName = deploymentName || projectName
226
+
227
+ if (!projectName) {
228
+ return {
229
+ projectName: factName || 'unknown',
230
+ provider: 'bunny.net',
231
+ error: 'No project name configured',
232
+ rawDefinitionFilepaths: []
233
+ }
234
+ }
235
+
236
+ // Raw fact filepaths that this status depends on (specific zone files)
237
+ const rawFilepaths = [
238
+ this.$StorageZoneFact.getRelativeFilepath(projectName),
239
+ this.$PullZoneFact.getRelativeFilepath(projectName)
240
+ ]
241
+
242
+ // Try to get cached status if not forcing refresh
243
+ if (!now) {
244
+ const cached = await this.$StatusFact.get(factName, rawFilepaths)
245
+ if (cached) {
246
+ return cached.data
247
+ }
248
+ }
249
+
250
+ // In passive mode, don't call the provider if no cache exists
251
+ if (passive) {
252
+ return null
253
+ }
254
+
255
+ const storageZones = await this.storage.listZones({ search: projectName })
256
+ const storageZoneMatch = storageZones.find((zone: any) => zone.Name === projectName)
257
+
258
+ if (!storageZoneMatch) {
259
+ // Write placeholder fact files so cache can be hit on subsequent calls
260
+ await this.$StorageZoneFact.set(projectName, { notFound: true, name: projectName })
261
+ await this.$PullZoneFact.set(projectName, { notFound: true, name: projectName })
262
+ const errorResult = {
263
+ projectName: factName,
264
+ provider: 'bunny.net',
265
+ error: 'Storage zone not found',
266
+ rawDefinitionFilepaths: rawFilepaths
267
+ }
268
+ await this.$StatusFact.set(factName, errorResult)
269
+ return errorResult
270
+ }
271
+
272
+ const pullZones = await this.pull.listZones({ search: projectName })
273
+ const zones = pullZones.Items || pullZones
274
+ const pullZoneMatch = zones.find((zone: any) => zone.Name === projectName)
275
+
276
+ if (!pullZoneMatch) {
277
+ // Write placeholder fact file so cache can be hit on subsequent calls
278
+ await this.$PullZoneFact.set(projectName, { notFound: true, name: projectName })
279
+ const errorResult = {
280
+ projectName: factName,
281
+ provider: 'bunny.net',
282
+ error: 'Pull zone not found',
283
+ rawDefinitionFilepaths: rawFilepaths
284
+ }
285
+ await this.$StatusFact.set(factName, errorResult)
286
+ return errorResult
287
+ }
288
+
289
+ // Fetch full zone details (this also saves the individual fact files)
290
+ const storageZone = await this.storage.getZone(storageZoneMatch.Id)
291
+ const pullZone = await this.pull.getZone(pullZoneMatch.Id)
292
+
293
+ const publicUrl = pullZone.Hostnames?.[0]?.Value
294
+ ? `https://${pullZone.Hostnames[0].Value}`
295
+ : `https://${projectName}.b-cdn.net`
296
+
297
+ const result: Record<string, any> = {
298
+ projectName: factName,
299
+ provider: 'bunny.net',
300
+ status: pullZone.Enabled ? 'READY' : 'DISABLED',
301
+ publicUrl: publicUrl,
302
+ providerProjectId: `storage:${storageZone.Id}|pull:${pullZone.Id}`,
303
+ providerPortalUrl: `https://dash.bunny.net/cdn/${pullZone.Id}/general/hostnames`,
304
+ updatedAt: storageZone.DateModified ? new Date(storageZone.DateModified + 'Z').toISOString() : undefined,
305
+ usage: {
306
+ storageBytes: storageZone.StorageUsed,
307
+ filesCount: storageZone.FilesStored,
308
+ bandwidthBytes: pullZone.MonthlyBandwidthUsed,
309
+ charges: pullZone.MonthlyCharges
310
+ },
311
+ rawDefinitionFilepaths: rawFilepaths
312
+ }
313
+
314
+ await this.$StatusFact.set(factName, result)
315
+
316
+ return result
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }, {
322
+ importMeta: import.meta,
323
+ importStack: makeImportStack(),
324
+ capsuleName: capsule['#'],
325
+ })
326
+ }
327
+ capsule['#'] = 't44/caps/providers/bunny.net/ProjectDeployment'