@stream44.studio/t44 0.4.0-rc.24
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.yaml +12 -0
- package/.github/workflows/gordian-open-integrity.yaml +13 -0
- package/.github/workflows/test.yaml +31 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +21 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
- package/.repo-identifier +1 -0
- package/DCO.md +34 -0
- package/LICENSE.txt +186 -0
- package/README.md +189 -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 +57 -0
- package/caps/HomeRegistry.ts +319 -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 +426 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +654 -0
- package/caps/ProjectPulling.ts +234 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +332 -0
- package/caps/ProjectTest.ts +251 -0
- package/caps/ProjectTestLib.ts +257 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/TaskWorkflow.ts +192 -0
- package/caps/WorkspaceCli.ts +448 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +87 -0
- package/caps/WorkspaceConfigFile.ts +902 -0
- package/caps/WorkspaceConnection.ts +329 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +619 -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 +430 -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/patterns/README.md +2 -0
- package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
- package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/examples/01-Lifecycle/main.test.ts +223 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +381 -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 +91 -0
- package/standalone-rt.test.ts +150 -0
- package/standalone-rt.ts +140 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +60 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingConfig.ts +78 -0
- package/structs/ProjectPublishingFact.ts +68 -0
- package/structs/ProjectPullingConfig.ts +52 -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/WorkspaceShellConfig.ts +83 -0
- package/structs/patterns/README.md +2 -0
- package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/tsconfig.json +33 -0
- package/workspace-rt.ts +152 -0
- package/workspace.yaml +3 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
|
|
2
|
+
import { join, resolve, relative } from 'path'
|
|
3
|
+
import { readdir, stat } from 'fs/promises'
|
|
4
|
+
import { $ } from 'bun'
|
|
5
|
+
|
|
6
|
+
export async function capsule({
|
|
7
|
+
encapsulate,
|
|
8
|
+
CapsulePropertyTypes,
|
|
9
|
+
makeImportStack
|
|
10
|
+
}: {
|
|
11
|
+
encapsulate: any
|
|
12
|
+
CapsulePropertyTypes: any
|
|
13
|
+
makeImportStack: any
|
|
14
|
+
}) {
|
|
15
|
+
return encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
18
|
+
'#@stream44.studio/t44/structs/WorkspaceConfig': {
|
|
19
|
+
as: '$WorkspaceConfig'
|
|
20
|
+
},
|
|
21
|
+
'#@stream44.studio/t44/structs/WorkspaceProjectsConfig': {
|
|
22
|
+
as: '$WorkspaceProjectsConfig',
|
|
23
|
+
},
|
|
24
|
+
'#@stream44.studio/t44/structs/ProjectDeploymentConfig': {
|
|
25
|
+
as: '$ProjectDeploymentConfig',
|
|
26
|
+
},
|
|
27
|
+
'#@stream44.studio/t44/structs/ProjectPublishingConfig': {
|
|
28
|
+
as: '$WorkspaceRepositories'
|
|
29
|
+
},
|
|
30
|
+
'#@stream44.studio/t44/structs/WorkspaceProject': {
|
|
31
|
+
as: '$WorkspaceProject'
|
|
32
|
+
},
|
|
33
|
+
'#@stream44.studio/t44/structs/RepositoryOriginDescriptor': {
|
|
34
|
+
as: '$RepositoryOriginDescriptor'
|
|
35
|
+
},
|
|
36
|
+
'#': {
|
|
37
|
+
list: {
|
|
38
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
39
|
+
value: async function (this: any): Promise<Record<string, { sourceDir: string, deployments: Record<string, any>, repositories: Record<string, any> }>> {
|
|
40
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
41
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
42
|
+
|
|
43
|
+
if (!workspaceRootDir) {
|
|
44
|
+
throw new Error('Workspace root directory not configured')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const configFilepath = join(workspaceRootDir, '.workspace/workspace.yaml')
|
|
48
|
+
|
|
49
|
+
// Read existing projects config
|
|
50
|
+
const projectsConfig = await this.$WorkspaceProjectsConfig.config
|
|
51
|
+
const configuredProjects: Record<string, { sourceDir: string }> = projectsConfig?.projects || {}
|
|
52
|
+
|
|
53
|
+
// Scan workspace root for project directories
|
|
54
|
+
const entries = await readdir(workspaceRootDir, { withFileTypes: true })
|
|
55
|
+
const scannedDirs: string[] = []
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (!entry.isDirectory()) continue
|
|
59
|
+
if (entry.name.startsWith('.')) continue
|
|
60
|
+
if (entry.name === 'node_modules') continue
|
|
61
|
+
if (entry.name === '___') continue
|
|
62
|
+
scannedDirs.push(entry.name)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Pre-fill config with scanned projects that are not yet configured
|
|
66
|
+
for (const dirName of scannedDirs) {
|
|
67
|
+
if (!configuredProjects[dirName]) {
|
|
68
|
+
const sourceDir = `resolve('\${__dirname}/../${dirName}')`
|
|
69
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', dirName, 'sourceDir'], sourceDir)
|
|
70
|
+
configuredProjects[dirName] = { sourceDir: join(workspaceRootDir, dirName) }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build projects from config values, validating each
|
|
75
|
+
const projects: Record<string, { sourceDir: string, git: any, identifier: any, missing: boolean, deployments: Record<string, any>, repositories: Record<string, any> }> = {}
|
|
76
|
+
|
|
77
|
+
const sortedProjectEntries = Object.entries(configuredProjects).sort(([a], [b]) => a.localeCompare(b))
|
|
78
|
+
for (const [projectName, projectConfig] of sortedProjectEntries) {
|
|
79
|
+
const typedConfig = projectConfig as any
|
|
80
|
+
|
|
81
|
+
if (!typedConfig.sourceDir) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Project '${projectName}' has no sourceDir configured.\n` +
|
|
84
|
+
` Fix in: ${configFilepath}\n` +
|
|
85
|
+
` Under: '#@stream44.studio/t44/structs/WorkspaceProjectsConfig' → projects → ${projectName}`
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const resolvedSourceDir = resolve(typedConfig.sourceDir)
|
|
90
|
+
|
|
91
|
+
let missing = false
|
|
92
|
+
try {
|
|
93
|
+
const dirStat = await stat(resolvedSourceDir)
|
|
94
|
+
if (!dirStat.isDirectory()) {
|
|
95
|
+
missing = true
|
|
96
|
+
}
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
if (err.code === 'ENOENT') {
|
|
99
|
+
missing = true
|
|
100
|
+
} else {
|
|
101
|
+
throw err
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Read identifier from package.json descriptor
|
|
106
|
+
let identifier: any = undefined
|
|
107
|
+
if (!missing) {
|
|
108
|
+
try {
|
|
109
|
+
const pkgJsonPath = join(resolvedSourceDir, 'package.json')
|
|
110
|
+
identifier = await this.$RepositoryOriginDescriptor.get(pkgJsonPath)
|
|
111
|
+
} catch { }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
projects[projectName] = {
|
|
115
|
+
sourceDir: resolvedSourceDir,
|
|
116
|
+
git: typedConfig.git !== undefined ? typedConfig.git : undefined,
|
|
117
|
+
identifier: identifier || undefined,
|
|
118
|
+
missing,
|
|
119
|
+
deployments: {},
|
|
120
|
+
repositories: {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Map deployments to projects
|
|
125
|
+
const deploymentConfig = await this.$ProjectDeploymentConfig.config
|
|
126
|
+
if (deploymentConfig?.deployments) {
|
|
127
|
+
for (const [deploymentName, deploymentAliases] of Object.entries(deploymentConfig.deployments)) {
|
|
128
|
+
const aliases = deploymentAliases as Record<string, any>
|
|
129
|
+
// Find the project by checking sourceDir of any alias
|
|
130
|
+
let mappedProject: string | null = null
|
|
131
|
+
for (const [aliasName, aliasConfig] of Object.entries(aliases)) {
|
|
132
|
+
if (aliasConfig.sourceDir) {
|
|
133
|
+
const resolvedSourceDir = resolve(aliasConfig.sourceDir)
|
|
134
|
+
const relPath = relative(workspaceRootDir, resolvedSourceDir)
|
|
135
|
+
const topDir = relPath.split('/')[0]
|
|
136
|
+
if (projects[topDir]) {
|
|
137
|
+
mappedProject = topDir
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!mappedProject) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Deployment '${deploymentName}' does not map to any workspace project.\n` +
|
|
146
|
+
` Ensure at least one alias has a valid sourceDir pointing to a project directory.\n` +
|
|
147
|
+
` Known projects: ${Object.keys(projects).join(', ')}\n` +
|
|
148
|
+
` Fix in: ${configFilepath}`
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
projects[mappedProject].deployments[deploymentName] = aliases
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Map repositories to projects
|
|
157
|
+
const repositoriesConfig = await this.$WorkspaceRepositories.config
|
|
158
|
+
if (repositoriesConfig?.repositories) {
|
|
159
|
+
for (const [repoName, repoConfig] of Object.entries(repositoriesConfig.repositories)) {
|
|
160
|
+
const typedConfig = repoConfig as any
|
|
161
|
+
if (typedConfig.sourceDir) {
|
|
162
|
+
const resolvedSourceDir = resolve(typedConfig.sourceDir)
|
|
163
|
+
const relPath = relative(workspaceRootDir, resolvedSourceDir)
|
|
164
|
+
const topDir = relPath.split('/')[0]
|
|
165
|
+
if (!projects[topDir]) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Repository '${repoName}' sourceDir '${typedConfig.sourceDir}' does not map to any workspace project '${topDir}'.\n` +
|
|
168
|
+
` Known projects: ${Object.keys(projects).join(', ')}\n` +
|
|
169
|
+
` Fix in: ${configFilepath}`
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
projects[topDir].repositories[repoName] = typedConfig
|
|
173
|
+
} else {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Repository '${repoName}' has no sourceDir configured.\n` +
|
|
176
|
+
` Fix in: ${configFilepath}`
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return projects
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
gatherGitInfo: {
|
|
186
|
+
type: CapsulePropertyTypes.Function,
|
|
187
|
+
value: async function (this: any, { now }: { now?: boolean } = {}): Promise<void> {
|
|
188
|
+
const projects = await this.list
|
|
189
|
+
|
|
190
|
+
for (const [projectName, projectInfo] of Object.entries(projects)) {
|
|
191
|
+
const project = projectInfo as any
|
|
192
|
+
const sourceDir = project.sourceDir
|
|
193
|
+
|
|
194
|
+
// If git info is already in config and --now is not passed, skip
|
|
195
|
+
if (project.git !== undefined && !now) {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check if this is a git repo
|
|
200
|
+
try {
|
|
201
|
+
const gitDirCheck = await $`git -C ${sourceDir} rev-parse --git-dir`.quiet().nothrow()
|
|
202
|
+
if (gitDirCheck.exitCode !== 0) {
|
|
203
|
+
// Not a git repo
|
|
204
|
+
if (project.git === undefined) {
|
|
205
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], false)
|
|
206
|
+
}
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get first commit hash
|
|
211
|
+
const firstCommitResult = await $`git -C ${sourceDir} rev-list --max-parents=0 HEAD`.quiet().nothrow()
|
|
212
|
+
if (firstCommitResult.exitCode !== 0) {
|
|
213
|
+
// No commits yet
|
|
214
|
+
if (project.git === undefined) {
|
|
215
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], false)
|
|
216
|
+
}
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const firstCommitHash = firstCommitResult.text().trim().split('\n')[0]
|
|
221
|
+
|
|
222
|
+
// Get first commit date (createdAt)
|
|
223
|
+
const createdAtResult = await $`git -C ${sourceDir} show -s --format=%aI ${firstCommitHash}`.quiet().nothrow()
|
|
224
|
+
const createdAt = createdAtResult.exitCode === 0 ? createdAtResult.text().trim() : null
|
|
225
|
+
|
|
226
|
+
// Get first commit author details
|
|
227
|
+
const authorNameResult = await $`git -C ${sourceDir} show -s --format=%an ${firstCommitHash}`.quiet().nothrow()
|
|
228
|
+
const authorEmailResult = await $`git -C ${sourceDir} show -s --format=%ae ${firstCommitHash}`.quiet().nothrow()
|
|
229
|
+
const firstCommitAuthor: Record<string, string> = {}
|
|
230
|
+
if (authorNameResult.exitCode === 0) {
|
|
231
|
+
firstCommitAuthor.name = authorNameResult.text().trim()
|
|
232
|
+
}
|
|
233
|
+
if (authorEmailResult.exitCode === 0) {
|
|
234
|
+
firstCommitAuthor.email = authorEmailResult.text().trim()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Get remotes
|
|
238
|
+
const remotesResult = await $`git -C ${sourceDir} remote -v`.quiet().nothrow()
|
|
239
|
+
const remotes: Record<string, string> = {}
|
|
240
|
+
if (remotesResult.exitCode === 0) {
|
|
241
|
+
const lines = remotesResult.text().trim().split('\n').filter(Boolean)
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/)
|
|
244
|
+
if (match) {
|
|
245
|
+
remotes[match[1]] = match[2]
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If --now is passed, sync remotes between config and git repo
|
|
251
|
+
if (now && project.git && typeof project.git === 'object' && project.git.remotes) {
|
|
252
|
+
for (const [remoteName, remoteUri] of Object.entries(project.git.remotes)) {
|
|
253
|
+
if (!remotes[remoteName]) {
|
|
254
|
+
// Remote exists in config but not in git repo — add it
|
|
255
|
+
const addResult = await $`git -C ${sourceDir} remote add ${remoteName} ${remoteUri as string}`.quiet().nothrow()
|
|
256
|
+
if (addResult.exitCode === 0) {
|
|
257
|
+
remotes[remoteName] = remoteUri as string
|
|
258
|
+
}
|
|
259
|
+
} else if (remotes[remoteName] !== remoteUri) {
|
|
260
|
+
// Remote URL in config differs from git — update git to match config
|
|
261
|
+
const setUrlResult = await $`git -C ${sourceDir} remote set-url ${remoteName} ${remoteUri as string}`.quiet().nothrow()
|
|
262
|
+
if (setUrlResult.exitCode === 0) {
|
|
263
|
+
remotes[remoteName] = remoteUri as string
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const gitInfo: Record<string, any> = {
|
|
270
|
+
firstCommitHash,
|
|
271
|
+
createdAt,
|
|
272
|
+
firstCommitAuthor,
|
|
273
|
+
remotes
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], gitInfo)
|
|
277
|
+
} catch (err: any) {
|
|
278
|
+
// If git commands fail entirely, mark as false
|
|
279
|
+
if (project.git === undefined) {
|
|
280
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], false)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Write per-project JSON files as cached data
|
|
286
|
+
await this.writeProjectFiles()
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
writeProjectFiles: {
|
|
290
|
+
type: CapsulePropertyTypes.Function,
|
|
291
|
+
value: async function (this: any): Promise<void> {
|
|
292
|
+
const projects = await this.list
|
|
293
|
+
|
|
294
|
+
for (const [projectName, projectData] of Object.entries(projects)) {
|
|
295
|
+
const project = projectData as any
|
|
296
|
+
const projectFile = {
|
|
297
|
+
name: projectName,
|
|
298
|
+
sourceDir: project.sourceDir,
|
|
299
|
+
git: project.git,
|
|
300
|
+
identifier: project.identifier || undefined,
|
|
301
|
+
deployments: project.deployments || {},
|
|
302
|
+
repositories: project.repositories || {},
|
|
303
|
+
updatedAt: new Date().toISOString()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await this.$WorkspaceProject.set(projectName, projectFile)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
resolveMatchingRepositories: {
|
|
311
|
+
type: CapsulePropertyTypes.Function,
|
|
312
|
+
value: async function (this: any, { workspaceProject, repositories }: {
|
|
313
|
+
workspaceProject: string
|
|
314
|
+
repositories: Record<string, any>
|
|
315
|
+
}): Promise<Record<string, any>> {
|
|
316
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
317
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
318
|
+
const currentDir = process.cwd()
|
|
319
|
+
|
|
320
|
+
let matchingRepositories: Record<string, any> = {}
|
|
321
|
+
|
|
322
|
+
// Strategy 1: Try prefix matching on repository names
|
|
323
|
+
const prefixMatches: string[] = []
|
|
324
|
+
for (const repoName of Object.keys(repositories)) {
|
|
325
|
+
if (repoName.startsWith(workspaceProject)) {
|
|
326
|
+
prefixMatches.push(repoName)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (prefixMatches.length > 1) {
|
|
331
|
+
const exactMatch = prefixMatches.find(m => m === workspaceProject)
|
|
332
|
+
if (exactMatch) {
|
|
333
|
+
matchingRepositories[exactMatch] = repositories[exactMatch]
|
|
334
|
+
return matchingRepositories
|
|
335
|
+
}
|
|
336
|
+
const chalk = (await import('chalk')).default
|
|
337
|
+
console.log(chalk.red(`\nError: Multiple repositories match prefix '${workspaceProject}':\n`))
|
|
338
|
+
for (const match of prefixMatches) {
|
|
339
|
+
console.log(chalk.gray(` - ${match}`))
|
|
340
|
+
}
|
|
341
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
342
|
+
throw new Error(`Multiple repositories match prefix: ${workspaceProject}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (prefixMatches.length === 1) {
|
|
346
|
+
matchingRepositories[prefixMatches[0]] = repositories[prefixMatches[0]]
|
|
347
|
+
return matchingRepositories
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Strategy 2: Try path matching (absolute or relative from current directory)
|
|
351
|
+
let targetPath: string
|
|
352
|
+
if (workspaceProject.startsWith('/')) {
|
|
353
|
+
targetPath = workspaceProject
|
|
354
|
+
} else {
|
|
355
|
+
targetPath = resolve(currentDir, workspaceProject)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const [repoName, repoConfig] of Object.entries(repositories)) {
|
|
359
|
+
if ((repoConfig as any).sourceDir) {
|
|
360
|
+
const sourceDirPath = resolve((repoConfig as any).sourceDir)
|
|
361
|
+
const rel = relative(targetPath, sourceDirPath)
|
|
362
|
+
|
|
363
|
+
const isWithinOrEqual = rel === '' || !rel.startsWith('..')
|
|
364
|
+
|
|
365
|
+
if (isWithinOrEqual) {
|
|
366
|
+
matchingRepositories[repoName] = repoConfig
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Strategy 3: Try suffix matching on the last segment of repository names
|
|
372
|
+
if (Object.keys(matchingRepositories).length === 0) {
|
|
373
|
+
const suffixMatches: string[] = []
|
|
374
|
+
for (const repoName of Object.keys(repositories)) {
|
|
375
|
+
const lastSegment = repoName.split('/').pop() || ''
|
|
376
|
+
if (lastSegment === workspaceProject || lastSegment.startsWith(workspaceProject)) {
|
|
377
|
+
suffixMatches.push(repoName)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (suffixMatches.length > 1) {
|
|
382
|
+
const chalk = (await import('chalk')).default
|
|
383
|
+
console.log(chalk.red(`\nError: Multiple repositories match '${workspaceProject}':\n`))
|
|
384
|
+
for (const match of suffixMatches) {
|
|
385
|
+
console.log(chalk.gray(` - ${match}`))
|
|
386
|
+
}
|
|
387
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
388
|
+
throw new Error(`Multiple repositories match: ${workspaceProject}`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (suffixMatches.length === 1) {
|
|
392
|
+
matchingRepositories[suffixMatches[0]] = repositories[suffixMatches[0]]
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Strategy 4: Try substring matching on repository names
|
|
397
|
+
if (Object.keys(matchingRepositories).length === 0) {
|
|
398
|
+
const substringMatches: string[] = []
|
|
399
|
+
for (const repoName of Object.keys(repositories)) {
|
|
400
|
+
if (repoName.includes(workspaceProject)) {
|
|
401
|
+
substringMatches.push(repoName)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (substringMatches.length > 1) {
|
|
406
|
+
const chalk = (await import('chalk')).default
|
|
407
|
+
console.log(chalk.red(`\nError: Multiple repositories contain '${workspaceProject}':\n`))
|
|
408
|
+
for (const match of substringMatches) {
|
|
409
|
+
console.log(chalk.gray(` - ${match}`))
|
|
410
|
+
}
|
|
411
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
412
|
+
throw new Error(`Multiple repositories contain: ${workspaceProject}`)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (substringMatches.length === 1) {
|
|
416
|
+
matchingRepositories[substringMatches[0]] = repositories[substringMatches[0]]
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (Object.keys(matchingRepositories).length === 0) {
|
|
421
|
+
const chalk = (await import('chalk')).default
|
|
422
|
+
console.log(chalk.red(`\nError: No repositories found matching '${workspaceProject}'.\n`))
|
|
423
|
+
console.log(chalk.gray('Available repositories:'))
|
|
424
|
+
for (const repoName of Object.keys(repositories)) {
|
|
425
|
+
console.log(chalk.gray(` - ${repoName}`))
|
|
426
|
+
}
|
|
427
|
+
console.log('')
|
|
428
|
+
throw new Error(`No repositories found matching: ${workspaceProject}`)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return matchingRepositories
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
resolveMatchingDeployments: {
|
|
435
|
+
type: CapsulePropertyTypes.Function,
|
|
436
|
+
value: async function (this: any, { workspaceProject, deployments }: {
|
|
437
|
+
workspaceProject: string
|
|
438
|
+
deployments: Record<string, any>
|
|
439
|
+
}): Promise<Record<string, any>> {
|
|
440
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
441
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
442
|
+
const currentDir = process.cwd()
|
|
443
|
+
|
|
444
|
+
let matchingDeployments: Record<string, any> = {}
|
|
445
|
+
|
|
446
|
+
// Strategy 1: Try prefix matching on project names
|
|
447
|
+
const prefixMatches: string[] = []
|
|
448
|
+
for (const projectName of Object.keys(deployments)) {
|
|
449
|
+
if (projectName.startsWith(workspaceProject)) {
|
|
450
|
+
prefixMatches.push(projectName)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (prefixMatches.length > 1) {
|
|
455
|
+
const chalk = (await import('chalk')).default
|
|
456
|
+
console.log(chalk.red(`\nError: Multiple projects match prefix '${workspaceProject}':\n`))
|
|
457
|
+
for (const match of prefixMatches) {
|
|
458
|
+
console.log(chalk.gray(` - ${match}`))
|
|
459
|
+
}
|
|
460
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
461
|
+
throw new Error(`Multiple projects match prefix: ${workspaceProject}`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (prefixMatches.length === 1) {
|
|
465
|
+
matchingDeployments[prefixMatches[0]] = deployments[prefixMatches[0]]
|
|
466
|
+
return matchingDeployments
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Strategy 2: Try path matching (absolute or relative from current directory)
|
|
470
|
+
let targetPath: string
|
|
471
|
+
if (workspaceProject.startsWith('/')) {
|
|
472
|
+
targetPath = workspaceProject
|
|
473
|
+
} else {
|
|
474
|
+
targetPath = resolve(currentDir, workspaceProject)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for (const [projectName, projectAliases] of Object.entries(deployments)) {
|
|
478
|
+
for (const [alias, aliasConfig] of Object.entries(projectAliases as Record<string, any>)) {
|
|
479
|
+
if (aliasConfig.sourceDir) {
|
|
480
|
+
const sourceDirPath = resolve(aliasConfig.sourceDir)
|
|
481
|
+
const rel = relative(targetPath, sourceDirPath)
|
|
482
|
+
|
|
483
|
+
const isWithinOrEqual = rel === '' || !rel.startsWith('..')
|
|
484
|
+
|
|
485
|
+
if (isWithinOrEqual) {
|
|
486
|
+
if (!matchingDeployments[projectName]) {
|
|
487
|
+
matchingDeployments[projectName] = {}
|
|
488
|
+
}
|
|
489
|
+
matchingDeployments[projectName][alias] = aliasConfig
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (Object.keys(matchingDeployments).length > 1) {
|
|
496
|
+
const chalk = (await import('chalk')).default
|
|
497
|
+
const pathMatches = Object.keys(matchingDeployments)
|
|
498
|
+
console.log(chalk.red(`\nError: Multiple projects match path '${workspaceProject}':\n`))
|
|
499
|
+
for (const match of pathMatches) {
|
|
500
|
+
console.log(chalk.gray(` - ${match}`))
|
|
501
|
+
}
|
|
502
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
503
|
+
throw new Error(`Multiple projects match path: ${workspaceProject}`)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Strategy 3: Try suffix matching on the last segment of project names
|
|
507
|
+
if (Object.keys(matchingDeployments).length === 0) {
|
|
508
|
+
const suffixMatches: string[] = []
|
|
509
|
+
for (const projectName of Object.keys(deployments)) {
|
|
510
|
+
const lastSegment = projectName.split('/').pop() || ''
|
|
511
|
+
if (lastSegment === workspaceProject || lastSegment.startsWith(workspaceProject)) {
|
|
512
|
+
suffixMatches.push(projectName)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (suffixMatches.length > 1) {
|
|
517
|
+
const chalk = (await import('chalk')).default
|
|
518
|
+
console.log(chalk.red(`\nError: Multiple projects match '${workspaceProject}':\n`))
|
|
519
|
+
for (const match of suffixMatches) {
|
|
520
|
+
console.log(chalk.gray(` - ${match}`))
|
|
521
|
+
}
|
|
522
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
523
|
+
throw new Error(`Multiple projects match: ${workspaceProject}`)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (suffixMatches.length === 1) {
|
|
527
|
+
matchingDeployments[suffixMatches[0]] = deployments[suffixMatches[0]]
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Strategy 4: Try substring matching on project names
|
|
532
|
+
if (Object.keys(matchingDeployments).length === 0) {
|
|
533
|
+
const substringMatches: string[] = []
|
|
534
|
+
for (const projectName of Object.keys(deployments)) {
|
|
535
|
+
if (projectName.includes(workspaceProject)) {
|
|
536
|
+
substringMatches.push(projectName)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (substringMatches.length > 1) {
|
|
541
|
+
const chalk = (await import('chalk')).default
|
|
542
|
+
console.log(chalk.red(`\nError: Multiple projects contain '${workspaceProject}':\n`))
|
|
543
|
+
for (const match of substringMatches) {
|
|
544
|
+
console.log(chalk.gray(` - ${match}`))
|
|
545
|
+
}
|
|
546
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
547
|
+
throw new Error(`Multiple projects contain: ${workspaceProject}`)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (substringMatches.length === 1) {
|
|
551
|
+
matchingDeployments[substringMatches[0]] = deployments[substringMatches[0]]
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (Object.keys(matchingDeployments).length === 0) {
|
|
556
|
+
const chalk = (await import('chalk')).default
|
|
557
|
+
console.log(chalk.red(`\nError: No deployments found matching '${workspaceProject}'.\n`))
|
|
558
|
+
console.log(chalk.gray('Available projects:'))
|
|
559
|
+
for (const projectName of Object.keys(deployments)) {
|
|
560
|
+
console.log(chalk.gray(` - ${projectName}`))
|
|
561
|
+
}
|
|
562
|
+
console.log('')
|
|
563
|
+
throw new Error(`No deployments found matching: ${workspaceProject}`)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return matchingDeployments
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
ensureIdentifiers: {
|
|
570
|
+
type: CapsulePropertyTypes.Function,
|
|
571
|
+
value: async function (this: any): Promise<void> {
|
|
572
|
+
const projects = await this.list
|
|
573
|
+
|
|
574
|
+
for (const [projectName, projectInfo] of Object.entries(projects)) {
|
|
575
|
+
const project = projectInfo as any
|
|
576
|
+
if (project.missing) continue
|
|
577
|
+
|
|
578
|
+
const sourceDir = project.sourceDir
|
|
579
|
+
const packageJsonPath = join(sourceDir, 'package.json')
|
|
580
|
+
|
|
581
|
+
// Check if package.json exists
|
|
582
|
+
try {
|
|
583
|
+
await stat(packageJsonPath)
|
|
584
|
+
} catch {
|
|
585
|
+
continue
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Check if descriptor already exists
|
|
589
|
+
const existing = await this.$RepositoryOriginDescriptor.get(packageJsonPath)
|
|
590
|
+
if (existing?.did && existing?.privateKey && existing?.createdAt) {
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Generate a new DID keypair
|
|
595
|
+
const { generateKeypair } = await import('../lib/ucan.js')
|
|
596
|
+
const { did, privateKey } = await generateKeypair()
|
|
597
|
+
|
|
598
|
+
const descriptor = {
|
|
599
|
+
did,
|
|
600
|
+
privateKey,
|
|
601
|
+
createdAt: new Date().toISOString()
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
await this.$RepositoryOriginDescriptor.set(packageJsonPath, descriptor)
|
|
605
|
+
|
|
606
|
+
const chalk = (await import('chalk')).default
|
|
607
|
+
console.log(chalk.green(` ✓ Created identifier for '${projectName}'`))
|
|
608
|
+
console.log(chalk.green(` DID: ${did}\n`))
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
findProjectForPath: {
|
|
613
|
+
type: CapsulePropertyTypes.Function,
|
|
614
|
+
value: async function (this: any, { targetPath }: { targetPath: string }): Promise<string | null> {
|
|
615
|
+
const projects = await this.list
|
|
616
|
+
const resolvedTarget = resolve(targetPath)
|
|
617
|
+
|
|
618
|
+
for (const [projectName, projectInfo] of Object.entries(projects)) {
|
|
619
|
+
const projectDir = (projectInfo as any).sourceDir
|
|
620
|
+
if (resolvedTarget === projectDir || resolvedTarget.startsWith(projectDir + '/')) {
|
|
621
|
+
return projectName
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return null
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}, {
|
|
631
|
+
importMeta: import.meta,
|
|
632
|
+
importStack: makeImportStack(),
|
|
633
|
+
capsuleName: capsule['#'],
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
capsule['#'] = '@stream44.studio/t44/caps/WorkspaceProjects'
|