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,307 @@
|
|
|
1
|
+
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import { mkdir, access, readFile, writeFile } from 'fs/promises'
|
|
5
|
+
import { constants } from 'fs'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
export async function capsule({
|
|
9
|
+
encapsulate,
|
|
10
|
+
CapsulePropertyTypes,
|
|
11
|
+
makeImportStack
|
|
12
|
+
}: {
|
|
13
|
+
encapsulate: any
|
|
14
|
+
CapsulePropertyTypes: any
|
|
15
|
+
makeImportStack: any
|
|
16
|
+
}) {
|
|
17
|
+
return encapsulate({
|
|
18
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
19
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
20
|
+
'#t44/structs/WorkspaceConfig.v0': {
|
|
21
|
+
as: '$WorkspaceConfig'
|
|
22
|
+
},
|
|
23
|
+
'#t44/structs/WorkspaceRepositories.v0': {
|
|
24
|
+
as: '$WorkspaceRepositories'
|
|
25
|
+
},
|
|
26
|
+
'#': {
|
|
27
|
+
WorkspaceConfig: {
|
|
28
|
+
type: CapsulePropertyTypes.Mapping,
|
|
29
|
+
value: 't44/caps/WorkspaceConfig.v0'
|
|
30
|
+
},
|
|
31
|
+
WorkspaceProjects: {
|
|
32
|
+
type: CapsulePropertyTypes.Mapping,
|
|
33
|
+
value: 't44/caps/WorkspaceProjects.v0'
|
|
34
|
+
},
|
|
35
|
+
GitRepository: {
|
|
36
|
+
type: CapsulePropertyTypes.Mapping,
|
|
37
|
+
value: 't44/caps/providers/git-scm.com/ProjectPublishing.v0'
|
|
38
|
+
},
|
|
39
|
+
NpmRegistry: {
|
|
40
|
+
type: CapsulePropertyTypes.Mapping,
|
|
41
|
+
value: 't44/caps/providers/npmjs.com/ProjectPublishing.v0'
|
|
42
|
+
},
|
|
43
|
+
GitHubRepository: {
|
|
44
|
+
type: CapsulePropertyTypes.Mapping,
|
|
45
|
+
value: 't44/caps/providers/github.com/ProjectPublishing.v0'
|
|
46
|
+
},
|
|
47
|
+
run: {
|
|
48
|
+
type: CapsulePropertyTypes.Function,
|
|
49
|
+
value: async function (this: any, { args }: any): Promise<void> {
|
|
50
|
+
|
|
51
|
+
const { projectSelector, rc, release, dangerouslyResetMain } = args
|
|
52
|
+
|
|
53
|
+
const repositoriesConfig = await this.$WorkspaceRepositories.config
|
|
54
|
+
|
|
55
|
+
if (!repositoriesConfig?.repositories) {
|
|
56
|
+
throw new Error('No repositories configuration found')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (dangerouslyResetMain && !projectSelector) {
|
|
60
|
+
throw new Error('--dangerously-reset-main flag requires a projectSelector to be specified')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let matchingRepositories: Record<string, any>
|
|
64
|
+
|
|
65
|
+
if (!projectSelector) {
|
|
66
|
+
matchingRepositories = repositoriesConfig.repositories
|
|
67
|
+
} else {
|
|
68
|
+
matchingRepositories = await this.WorkspaceProjects.resolveMatchingRepositories({
|
|
69
|
+
workspaceProject: projectSelector,
|
|
70
|
+
repositories: repositoriesConfig.repositories
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Phase 0: Copy source directories to central location
|
|
75
|
+
console.log('[t44] Copying source directories to central location ...\n')
|
|
76
|
+
const centralSourceDirs: Map<string, string> = new Map()
|
|
77
|
+
|
|
78
|
+
for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
|
|
79
|
+
const projectSourceDir = join((repoConfig as any).sourceDir)
|
|
80
|
+
const repoSourceDir = join(
|
|
81
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
82
|
+
'.~o/workspace.foundation/ProjectPublishing/repos',
|
|
83
|
+
repoName
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
console.log(`=> Syncing '${repoName}' to central location ...`)
|
|
87
|
+
|
|
88
|
+
await mkdir(repoSourceDir, { recursive: true })
|
|
89
|
+
|
|
90
|
+
const gitignorePath = join(projectSourceDir, '.gitignore')
|
|
91
|
+
let gitignoreExists = false
|
|
92
|
+
try {
|
|
93
|
+
await access(gitignorePath, constants.F_OK)
|
|
94
|
+
gitignoreExists = true
|
|
95
|
+
} catch { }
|
|
96
|
+
|
|
97
|
+
const rsyncArgs = ['rsync', '-a', '--delete', '--exclude', '.git']
|
|
98
|
+
if (gitignoreExists) rsyncArgs.push('--exclude-from=' + gitignorePath)
|
|
99
|
+
rsyncArgs.push(projectSourceDir + '/', repoSourceDir + '/')
|
|
100
|
+
await $`${rsyncArgs}`
|
|
101
|
+
|
|
102
|
+
centralSourceDirs.set(repoName, repoSourceDir)
|
|
103
|
+
console.log(` Synced to: ${repoSourceDir}\n`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper to iterate providers with custom callback
|
|
107
|
+
const forEachProvider = async (callback: (params: {
|
|
108
|
+
repoName: string,
|
|
109
|
+
repoConfig: any,
|
|
110
|
+
providerConfig: any,
|
|
111
|
+
capsuleName: string,
|
|
112
|
+
repoSourceDir: string
|
|
113
|
+
}) => Promise<void>) => {
|
|
114
|
+
for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
|
|
115
|
+
const providers = Array.isArray((repoConfig as any).providers)
|
|
116
|
+
? (repoConfig as any).providers
|
|
117
|
+
: (repoConfig as any).provider
|
|
118
|
+
? [(repoConfig as any).provider]
|
|
119
|
+
: []
|
|
120
|
+
|
|
121
|
+
const repoSourceDir = centralSourceDirs.get(repoName)!
|
|
122
|
+
for (const providerConfig of providers) {
|
|
123
|
+
const capsuleName = providerConfig.capsule
|
|
124
|
+
await callback({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Phase 1: Prepare - analyze all packages and collect metadata
|
|
130
|
+
console.log('[t44] Analyzing packages ...\n')
|
|
131
|
+
const packageMetadata: Map<string, any> = new Map()
|
|
132
|
+
|
|
133
|
+
await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
|
|
134
|
+
if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing.v0') {
|
|
135
|
+
const metadata = await this.NpmRegistry.prepare({
|
|
136
|
+
config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
|
|
137
|
+
projectionDir: join(
|
|
138
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
139
|
+
'.~o/workspace.foundation/o/npmjs.com'
|
|
140
|
+
),
|
|
141
|
+
repoSourceDir
|
|
142
|
+
})
|
|
143
|
+
packageMetadata.set(repoName, metadata)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Phase 2: Bump - only bump packages that have changes
|
|
148
|
+
if (rc || release) {
|
|
149
|
+
if (rc) console.log('[t44] Release candidate mode enabled\n')
|
|
150
|
+
if (release) console.log('[t44] Release mode enabled\n')
|
|
151
|
+
|
|
152
|
+
console.log('[t44] Bumping versions for changed packages ...\n')
|
|
153
|
+
|
|
154
|
+
const bumpedRepos = new Set<string>()
|
|
155
|
+
await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
|
|
156
|
+
if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing.v0') {
|
|
157
|
+
const metadata = packageMetadata.get(repoName)
|
|
158
|
+
|
|
159
|
+
if (metadata && metadata.hasChanges) {
|
|
160
|
+
if (!bumpedRepos.has(repoName)) {
|
|
161
|
+
console.log(`\n=> Bumping version for '${repoName}' ...\n`)
|
|
162
|
+
bumpedRepos.add(repoName)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await this.NpmRegistry.bump({
|
|
166
|
+
config: { ...repoConfig, provider: providerConfig },
|
|
167
|
+
options: { rc, release },
|
|
168
|
+
repoSourceDir,
|
|
169
|
+
metadata
|
|
170
|
+
})
|
|
171
|
+
} else if (metadata && !bumpedRepos.has(repoName)) {
|
|
172
|
+
console.log(`\n=> Skipping '${repoName}' (no changes)\n`)
|
|
173
|
+
bumpedRepos.add(repoName)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
console.log('[t44] Version bump complete!\n')
|
|
179
|
+
|
|
180
|
+
// Tag git-scm repos with the bumped version
|
|
181
|
+
const taggedRepos = new Set<string>()
|
|
182
|
+
await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
|
|
183
|
+
if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing.v0' && !taggedRepos.has(repoName)) {
|
|
184
|
+
// Read the bumped version from the source package.json
|
|
185
|
+
const packageJsonPath = join(repoSourceDir, 'package.json')
|
|
186
|
+
try {
|
|
187
|
+
const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
|
|
188
|
+
const packageJson = JSON.parse(packageJsonContent)
|
|
189
|
+
const version = packageJson.version
|
|
190
|
+
const tag = `v${version}`
|
|
191
|
+
|
|
192
|
+
const originUri = providerConfig.config.RepositorySettings.origin
|
|
193
|
+
const projectionDir = join(
|
|
194
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
195
|
+
'.~o/workspace.foundation/o/git-scm.com'
|
|
196
|
+
)
|
|
197
|
+
const projectProjectionDir = join(projectionDir, 'repos', originUri.replace(/[@:\/]/g, '~'))
|
|
198
|
+
|
|
199
|
+
// Check if the projection repo exists
|
|
200
|
+
try {
|
|
201
|
+
await access(projectProjectionDir, constants.F_OK)
|
|
202
|
+
} catch {
|
|
203
|
+
// Repo not cloned yet, tagging will happen after push
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if tag already exists locally or on remote
|
|
208
|
+
const localTagCheck = await $`git tag -l ${tag}`.cwd(projectProjectionDir).quiet().nothrow()
|
|
209
|
+
if (localTagCheck.text().trim() === tag) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Git tag '${tag}' already exists in repository '${repoName}'.\n` +
|
|
212
|
+
` Please bump to a different version before pushing.`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const remoteTagCheck = await $`git ls-remote --tags origin ${tag}`.cwd(projectProjectionDir).quiet().nothrow()
|
|
217
|
+
if (remoteTagCheck.text().trim().length > 0) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Git tag '${tag}' already exists on remote for repository '${repoName}'.\n` +
|
|
220
|
+
` Please bump to a different version before pushing.`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await $`git tag ${tag}`.cwd(projectProjectionDir)
|
|
225
|
+
console.log(chalk.green(` ✓ Tagged repository '${repoName}' with ${tag}\n`))
|
|
226
|
+
taggedRepos.add(repoName)
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
if (error.message?.includes('already exists')) {
|
|
229
|
+
throw error
|
|
230
|
+
}
|
|
231
|
+
// Skip if package.json can't be read
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
|
|
237
|
+
if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing.v0') {
|
|
238
|
+
const metadata = await this.NpmRegistry.prepare({
|
|
239
|
+
config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
|
|
240
|
+
projectionDir: join(
|
|
241
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
242
|
+
'.~o/workspace.foundation/o/npmjs.com'
|
|
243
|
+
),
|
|
244
|
+
repoSourceDir
|
|
245
|
+
})
|
|
246
|
+
packageMetadata.set(repoName, metadata)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Phase 3: Push - publish packages
|
|
252
|
+
console.log('[t44] Publishing packages ...\n')
|
|
253
|
+
|
|
254
|
+
const processedRepos = new Set<string>()
|
|
255
|
+
await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
|
|
256
|
+
if (!processedRepos.has(repoName)) {
|
|
257
|
+
console.log(`\n=> Processing repository '${repoName}' ...\n`)
|
|
258
|
+
processedRepos.add(repoName)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(` -> Running provider '${capsuleName}' ...\n`)
|
|
262
|
+
|
|
263
|
+
const metadata = packageMetadata.get(repoName)
|
|
264
|
+
|
|
265
|
+
if (capsuleName === 't44/caps/providers/github.com/ProjectPublishing.v0') {
|
|
266
|
+
await this.GitHubRepository.push({
|
|
267
|
+
config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir }
|
|
268
|
+
})
|
|
269
|
+
} else if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing.v0') {
|
|
270
|
+
await this.GitRepository.push({
|
|
271
|
+
config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
|
|
272
|
+
projectionDir: join(
|
|
273
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
274
|
+
'.~o/workspace.foundation/o/git-scm.com'
|
|
275
|
+
),
|
|
276
|
+
dangerouslyResetMain
|
|
277
|
+
})
|
|
278
|
+
} else if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing.v0') {
|
|
279
|
+
await this.NpmRegistry.push({
|
|
280
|
+
config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
|
|
281
|
+
projectionDir: join(
|
|
282
|
+
this.WorkspaceConfig.workspaceRootDir,
|
|
283
|
+
'.~o/workspace.foundation/o/npmjs.com'
|
|
284
|
+
),
|
|
285
|
+
metadata
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(` <- Provider '${capsuleName}' complete.\n`)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
for (const repoName of processedRepos) {
|
|
293
|
+
console.log(`<= Repository '${repoName}' processing complete.\n`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('[t44] Project repositories pushed OK!')
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}, {
|
|
302
|
+
importMeta: import.meta,
|
|
303
|
+
importStack: makeImportStack(),
|
|
304
|
+
capsuleName: capsule['#'],
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
capsule['#'] = 't44/caps/ProjectPublishing.v0'
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export async function capsule({
|
|
2
|
+
encapsulate,
|
|
3
|
+
CapsulePropertyTypes,
|
|
4
|
+
makeImportStack
|
|
5
|
+
}: {
|
|
6
|
+
encapsulate: any
|
|
7
|
+
CapsulePropertyTypes: any
|
|
8
|
+
makeImportStack: any
|
|
9
|
+
}) {
|
|
10
|
+
return encapsulate({
|
|
11
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
12
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
13
|
+
'#t44/structs/WorkspaceConfig.v0': {
|
|
14
|
+
as: '$WorkspaceConfig'
|
|
15
|
+
},
|
|
16
|
+
'#t44/structs/ProjectRackConfig.v0': {
|
|
17
|
+
as: '$ProjectRackConfig'
|
|
18
|
+
},
|
|
19
|
+
'#': {
|
|
20
|
+
WorkspacePrompt: {
|
|
21
|
+
type: CapsulePropertyTypes.Mapping,
|
|
22
|
+
value: 't44/caps/WorkspacePrompt.v0'
|
|
23
|
+
},
|
|
24
|
+
HomeRegistry: {
|
|
25
|
+
type: CapsulePropertyTypes.Mapping,
|
|
26
|
+
value: 't44/caps/HomeRegistry.v0'
|
|
27
|
+
},
|
|
28
|
+
ensureRack: {
|
|
29
|
+
type: CapsulePropertyTypes.Function,
|
|
30
|
+
value: async function (this: any): Promise<{ rackName: string }> {
|
|
31
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
32
|
+
const rackConfig = await this.$ProjectRackConfig.config
|
|
33
|
+
|
|
34
|
+
// Check if projectRack is already set in config (object format: { name, identifier })
|
|
35
|
+
if (rackConfig?.name && rackConfig?.identifier) {
|
|
36
|
+
return { rackName: rackConfig.name }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let rackName: string
|
|
40
|
+
|
|
41
|
+
const rackConfigStructKey = '#t44/structs/ProjectRackConfig.v0'
|
|
42
|
+
if (!rackConfig?.name) {
|
|
43
|
+
rackName = await this.WorkspacePrompt.setupPrompt({
|
|
44
|
+
title: '📦 Project Rack Setup',
|
|
45
|
+
description: [
|
|
46
|
+
`Workspace: ${workspaceConfig?.name || 'unknown'}`,
|
|
47
|
+
`Root: ${workspaceConfig?.rootDir || 'unknown'}`,
|
|
48
|
+
'',
|
|
49
|
+
'The project rack holds an integrated set of projects which can be',
|
|
50
|
+
'pulled into one or more workspaces.',
|
|
51
|
+
'A workspace attached to a rack has access to all projects in the rack',
|
|
52
|
+
'and is able to add more projects to the rack.',
|
|
53
|
+
'All workspaces attached to a rack automatically sync their projects',
|
|
54
|
+
'to the rack.',
|
|
55
|
+
'',
|
|
56
|
+
],
|
|
57
|
+
message: 'Enter a name for the project rack:',
|
|
58
|
+
defaultValue: 'genesis',
|
|
59
|
+
validate: (input: string) => {
|
|
60
|
+
if (!input || input.trim().length === 0) {
|
|
61
|
+
return 'Project rack name cannot be empty'
|
|
62
|
+
}
|
|
63
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
|
|
64
|
+
return 'Project rack name can only contain letters, numbers, underscores, and hyphens'
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
67
|
+
},
|
|
68
|
+
configPath: [rackConfigStructKey],
|
|
69
|
+
onSuccess: async () => {
|
|
70
|
+
// Don't write to config yet — we write the full object after rack registration
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
} else {
|
|
74
|
+
rackName = rackConfig.name
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if rack already exists in registry
|
|
78
|
+
let rackData = await this.HomeRegistry.getRack(rackName)
|
|
79
|
+
|
|
80
|
+
if (!rackData) {
|
|
81
|
+
const chalk = (await import('chalk')).default
|
|
82
|
+
console.log(chalk.cyan(`\n Registering project rack '${rackName}'...\n`))
|
|
83
|
+
|
|
84
|
+
const { generateKeypair } = await import('../lib/ucan.js')
|
|
85
|
+
const { did, privateKey } = await generateKeypair()
|
|
86
|
+
|
|
87
|
+
rackData = {
|
|
88
|
+
did,
|
|
89
|
+
privateKey,
|
|
90
|
+
createdAt: new Date().toISOString()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const rackPath = await this.HomeRegistry.setRack(rackName, rackData)
|
|
94
|
+
|
|
95
|
+
console.log(chalk.green(` ✓ Project rack registered at:`))
|
|
96
|
+
console.log(chalk.green(` ${rackPath}`))
|
|
97
|
+
console.log(chalk.green(` ✓ DID: ${rackData.did}\n`))
|
|
98
|
+
} else {
|
|
99
|
+
const chalk = (await import('chalk')).default
|
|
100
|
+
const rackPath = await this.HomeRegistry.getRackPath(rackName)
|
|
101
|
+
console.log(chalk.green(`\n ✓ Using existing project rack at:`))
|
|
102
|
+
console.log(chalk.green(` ${rackPath}`))
|
|
103
|
+
console.log(chalk.green(` ✓ DID: ${rackData.did}\n`))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Store rack as object { name, identifier } in rack config struct
|
|
107
|
+
await this.$ProjectRackConfig.setConfigValue(['name'], rackName)
|
|
108
|
+
await this.$ProjectRackConfig.setConfigValue(['identifier'], rackData.did)
|
|
109
|
+
|
|
110
|
+
return { rackName }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
getRackName: {
|
|
114
|
+
type: CapsulePropertyTypes.Function,
|
|
115
|
+
value: async function (this: any): Promise<string | null> {
|
|
116
|
+
const rackConfig = await this.$ProjectRackConfig.config
|
|
117
|
+
return rackConfig?.name || null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, {
|
|
123
|
+
importMeta: import.meta,
|
|
124
|
+
importStack: makeImportStack(),
|
|
125
|
+
capsuleName: capsule['#'],
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
capsule['#'] = 't44/caps/ProjectRack.v0'
|