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,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,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'
|