@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.
Files changed (99) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yaml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yaml +13 -0
  4. package/.github/workflows/test.yaml +31 -0
  5. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  7. package/.o/GordianOpenIntegrity.yaml +21 -0
  8. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  9. package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
  10. package/.repo-identifier +1 -0
  11. package/DCO.md +34 -0
  12. package/LICENSE.txt +186 -0
  13. package/README.md +189 -0
  14. package/bin/activate +36 -0
  15. package/bin/activate.ts +30 -0
  16. package/bin/postinstall.sh +19 -0
  17. package/bin/shell +27 -0
  18. package/bin/t44 +27 -0
  19. package/caps/ConfigSchemaStruct.ts +55 -0
  20. package/caps/Home.ts +57 -0
  21. package/caps/HomeRegistry.ts +319 -0
  22. package/caps/HomeRegistryFile.ts +144 -0
  23. package/caps/JsonSchemas.ts +220 -0
  24. package/caps/OpenApiSchema.ts +67 -0
  25. package/caps/PackageDescriptor.ts +88 -0
  26. package/caps/ProjectCatalogs.ts +153 -0
  27. package/caps/ProjectDeployment.ts +426 -0
  28. package/caps/ProjectDevelopment.ts +257 -0
  29. package/caps/ProjectPublishing.ts +654 -0
  30. package/caps/ProjectPulling.ts +234 -0
  31. package/caps/ProjectRack.ts +155 -0
  32. package/caps/ProjectRepository.ts +332 -0
  33. package/caps/ProjectTest.ts +251 -0
  34. package/caps/ProjectTestLib.ts +257 -0
  35. package/caps/RootKey.ts +219 -0
  36. package/caps/SigningKey.ts +243 -0
  37. package/caps/TaskWorkflow.ts +192 -0
  38. package/caps/WorkspaceCli.ts +448 -0
  39. package/caps/WorkspaceConfig.ts +268 -0
  40. package/caps/WorkspaceConfig.yaml +87 -0
  41. package/caps/WorkspaceConfigFile.ts +902 -0
  42. package/caps/WorkspaceConnection.ts +329 -0
  43. package/caps/WorkspaceEntityConfig.ts +78 -0
  44. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  45. package/caps/WorkspaceEntityFact.ts +218 -0
  46. package/caps/WorkspaceInfo.ts +619 -0
  47. package/caps/WorkspaceInit.ts +30 -0
  48. package/caps/WorkspaceKey.ts +338 -0
  49. package/caps/WorkspaceModel.ts +373 -0
  50. package/caps/WorkspaceProjects.ts +636 -0
  51. package/caps/WorkspacePrompt.ts +430 -0
  52. package/caps/WorkspaceShell.sh +39 -0
  53. package/caps/WorkspaceShell.ts +104 -0
  54. package/caps/WorkspaceShell.yaml +64 -0
  55. package/caps/WorkspaceShellCli.ts +109 -0
  56. package/caps/patterns/README.md +2 -0
  57. package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
  58. package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
  59. package/docs/Overview.drawio +248 -0
  60. package/docs/Overview.svg +4 -0
  61. package/examples/01-Lifecycle/main.test.ts +223 -0
  62. package/lib/crypto.ts +53 -0
  63. package/lib/key.ts +381 -0
  64. package/lib/schema-console-renderer.ts +181 -0
  65. package/lib/schema-resolver.ts +349 -0
  66. package/lib/ucan.ts +137 -0
  67. package/package.json +91 -0
  68. package/standalone-rt.test.ts +150 -0
  69. package/standalone-rt.ts +140 -0
  70. package/structs/HomeRegistry.ts +55 -0
  71. package/structs/HomeRegistryConfig.ts +60 -0
  72. package/structs/ProjectCatalogsConfig.ts +53 -0
  73. package/structs/ProjectDeploymentConfig.ts +56 -0
  74. package/structs/ProjectDeploymentFact.ts +106 -0
  75. package/structs/ProjectPublishingConfig.ts +78 -0
  76. package/structs/ProjectPublishingFact.ts +68 -0
  77. package/structs/ProjectPullingConfig.ts +52 -0
  78. package/structs/ProjectRack.ts +51 -0
  79. package/structs/ProjectRackConfig.ts +56 -0
  80. package/structs/RepositoryOriginDescriptor.ts +51 -0
  81. package/structs/RootKeyConfig.ts +64 -0
  82. package/structs/SigningKeyConfig.ts +64 -0
  83. package/structs/Workspace.ts +56 -0
  84. package/structs/WorkspaceCatalogs.ts +56 -0
  85. package/structs/WorkspaceCliConfig.ts +53 -0
  86. package/structs/WorkspaceConfig.ts +64 -0
  87. package/structs/WorkspaceConfigFile.ts +50 -0
  88. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  89. package/structs/WorkspaceKey.ts +55 -0
  90. package/structs/WorkspaceKeyConfig.ts +56 -0
  91. package/structs/WorkspaceMappingsConfig.ts +56 -0
  92. package/structs/WorkspaceProject.ts +104 -0
  93. package/structs/WorkspaceProjectsConfig.ts +67 -0
  94. package/structs/WorkspaceShellConfig.ts +83 -0
  95. package/structs/patterns/README.md +2 -0
  96. package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
  97. package/tsconfig.json +33 -0
  98. package/workspace-rt.ts +152 -0
  99. 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'