t44 0.4.0-rc.3
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.
- package/.dco-signatures +9 -0
- package/.github/workflows/dco.yml +12 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +203 -0
- package/README.md +183 -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/ConfigSchemaStruct.ts +55 -0
- package/caps/Home.ts +51 -0
- package/caps/HomeRegistry.ts +313 -0
- package/caps/HomeRegistryFile.ts +144 -0
- package/caps/JsonSchemas.ts +220 -0
- package/caps/OpenApiSchema.ts +67 -0
- package/caps/PackageDescriptor.ts +88 -0
- package/caps/ProjectCatalogs.ts +153 -0
- package/caps/ProjectDeployment.ts +363 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +522 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +322 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/WorkspaceCli.ts +442 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +71 -0
- package/caps/WorkspaceConfigFile.ts +799 -0
- package/caps/WorkspaceConnection.ts +249 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +595 -0
- package/caps/WorkspaceInit.ts +30 -0
- package/caps/WorkspaceKey.ts +338 -0
- package/caps/WorkspaceModel.ts +373 -0
- package/caps/WorkspaceProjects.ts +636 -0
- package/caps/WorkspacePrompt.ts +406 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.ts +104 -0
- package/caps/WorkspaceShell.yaml +64 -0
- package/caps/WorkspaceShellCli.ts +109 -0
- package/caps/WorkspaceTest.ts +167 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
- package/caps/providers/bunny.net/api-pull.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.ts +164 -0
- package/caps/providers/bunny.net/api-storage.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.ts +248 -0
- package/caps/providers/bunny.net/api.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
- package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
- package/caps/providers/dynadot.com/api-domains.ts +169 -0
- package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
- package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
- package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
- package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
- package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
- package/caps/providers/github.com/ProjectPublishing.ts +118 -0
- package/caps/providers/github.com/api.ts +115 -0
- package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
- package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
- package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
- package/caps/providers/vercel.com/api.test.ts +67 -0
- package/caps/providers/vercel.com/api.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.test.ts +108 -0
- package/caps/providers/vercel.com/project.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +365 -0
- package/lib/schema-console-renderer.ts +181 -0
- package/lib/schema-resolver.ts +349 -0
- package/lib/ucan.ts +137 -0
- package/package.json +101 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +56 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectRack.ts +51 -0
- package/structs/ProjectRackConfig.ts +56 -0
- package/structs/RepositoryOriginDescriptor.ts +51 -0
- package/structs/RootKeyConfig.ts +64 -0
- package/structs/SigningKeyConfig.ts +64 -0
- package/structs/Workspace.ts +56 -0
- package/structs/WorkspaceCatalogs.ts +56 -0
- package/structs/WorkspaceCliConfig.ts +53 -0
- package/structs/WorkspaceConfig.ts +64 -0
- package/structs/WorkspaceConfigFile.ts +50 -0
- package/structs/WorkspaceConfigFileMeta.ts +70 -0
- package/structs/WorkspaceKey.ts +55 -0
- package/structs/WorkspaceKeyConfig.ts +56 -0
- package/structs/WorkspaceMappingsConfig.ts +56 -0
- package/structs/WorkspaceProject.ts +104 -0
- package/structs/WorkspaceProjectsConfig.ts +67 -0
- package/structs/WorkspacePublishingConfig.ts +65 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
- package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/dynadot.com/DomainFact.ts +46 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
- package/tests/01-Lifecycle/main.test.ts +173 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- 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,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'
|