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.
Files changed (125) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yml +12 -0
  3. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  4. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity.yaml +25 -0
  6. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  7. package/DCO.md +34 -0
  8. package/LICENSE.md +203 -0
  9. package/README.md +183 -0
  10. package/bin/activate +36 -0
  11. package/bin/activate.ts +30 -0
  12. package/bin/postinstall.sh +19 -0
  13. package/bin/shell +27 -0
  14. package/bin/t44 +27 -0
  15. package/caps/ConfigSchemaStruct.ts +55 -0
  16. package/caps/Home.ts +51 -0
  17. package/caps/HomeRegistry.ts +313 -0
  18. package/caps/HomeRegistryFile.ts +144 -0
  19. package/caps/JsonSchemas.ts +220 -0
  20. package/caps/OpenApiSchema.ts +67 -0
  21. package/caps/PackageDescriptor.ts +88 -0
  22. package/caps/ProjectCatalogs.ts +153 -0
  23. package/caps/ProjectDeployment.ts +363 -0
  24. package/caps/ProjectDevelopment.ts +257 -0
  25. package/caps/ProjectPublishing.ts +522 -0
  26. package/caps/ProjectRack.ts +155 -0
  27. package/caps/ProjectRepository.ts +322 -0
  28. package/caps/RootKey.ts +219 -0
  29. package/caps/SigningKey.ts +243 -0
  30. package/caps/WorkspaceCli.ts +442 -0
  31. package/caps/WorkspaceConfig.ts +268 -0
  32. package/caps/WorkspaceConfig.yaml +71 -0
  33. package/caps/WorkspaceConfigFile.ts +799 -0
  34. package/caps/WorkspaceConnection.ts +249 -0
  35. package/caps/WorkspaceEntityConfig.ts +78 -0
  36. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  37. package/caps/WorkspaceEntityFact.ts +218 -0
  38. package/caps/WorkspaceInfo.ts +595 -0
  39. package/caps/WorkspaceInit.ts +30 -0
  40. package/caps/WorkspaceKey.ts +338 -0
  41. package/caps/WorkspaceModel.ts +373 -0
  42. package/caps/WorkspaceProjects.ts +636 -0
  43. package/caps/WorkspacePrompt.ts +406 -0
  44. package/caps/WorkspaceShell.sh +39 -0
  45. package/caps/WorkspaceShell.ts +104 -0
  46. package/caps/WorkspaceShell.yaml +64 -0
  47. package/caps/WorkspaceShellCli.ts +109 -0
  48. package/caps/WorkspaceTest.ts +167 -0
  49. package/caps/providers/README.md +2 -0
  50. package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
  51. package/caps/providers/bunny.net/api-pull.test.ts +319 -0
  52. package/caps/providers/bunny.net/api-pull.ts +164 -0
  53. package/caps/providers/bunny.net/api-storage.test.ts +168 -0
  54. package/caps/providers/bunny.net/api-storage.ts +248 -0
  55. package/caps/providers/bunny.net/api.ts +95 -0
  56. package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
  57. package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
  58. package/caps/providers/dynadot.com/api-domains.ts +169 -0
  59. package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
  60. package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
  61. package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
  62. package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
  63. package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
  64. package/caps/providers/github.com/ProjectPublishing.ts +118 -0
  65. package/caps/providers/github.com/api.ts +115 -0
  66. package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
  67. package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
  68. package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
  69. package/caps/providers/vercel.com/api.test.ts +67 -0
  70. package/caps/providers/vercel.com/api.ts +132 -0
  71. package/caps/providers/vercel.com/bun.lock +194 -0
  72. package/caps/providers/vercel.com/package.json +10 -0
  73. package/caps/providers/vercel.com/project.test.ts +108 -0
  74. package/caps/providers/vercel.com/project.ts +150 -0
  75. package/caps/providers/vercel.com/tsconfig.json +28 -0
  76. package/docs/Overview.drawio +248 -0
  77. package/docs/Overview.svg +4 -0
  78. package/lib/crypto.ts +53 -0
  79. package/lib/key.ts +365 -0
  80. package/lib/schema-console-renderer.ts +181 -0
  81. package/lib/schema-resolver.ts +349 -0
  82. package/lib/ucan.ts +137 -0
  83. package/package.json +101 -0
  84. package/structs/HomeRegistry.ts +55 -0
  85. package/structs/HomeRegistryConfig.ts +56 -0
  86. package/structs/ProjectCatalogsConfig.ts +53 -0
  87. package/structs/ProjectDeploymentConfig.ts +56 -0
  88. package/structs/ProjectDeploymentFact.ts +106 -0
  89. package/structs/ProjectPublishingFact.ts +68 -0
  90. package/structs/ProjectRack.ts +51 -0
  91. package/structs/ProjectRackConfig.ts +56 -0
  92. package/structs/RepositoryOriginDescriptor.ts +51 -0
  93. package/structs/RootKeyConfig.ts +64 -0
  94. package/structs/SigningKeyConfig.ts +64 -0
  95. package/structs/Workspace.ts +56 -0
  96. package/structs/WorkspaceCatalogs.ts +56 -0
  97. package/structs/WorkspaceCliConfig.ts +53 -0
  98. package/structs/WorkspaceConfig.ts +64 -0
  99. package/structs/WorkspaceConfigFile.ts +50 -0
  100. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  101. package/structs/WorkspaceKey.ts +55 -0
  102. package/structs/WorkspaceKeyConfig.ts +56 -0
  103. package/structs/WorkspaceMappingsConfig.ts +56 -0
  104. package/structs/WorkspaceProject.ts +104 -0
  105. package/structs/WorkspaceProjectsConfig.ts +67 -0
  106. package/structs/WorkspacePublishingConfig.ts +65 -0
  107. package/structs/WorkspaceShellConfig.ts +83 -0
  108. package/structs/providers/README.md +2 -0
  109. package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
  110. package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
  111. package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
  112. package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
  113. package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
  114. package/structs/providers/dynadot.com/DomainFact.ts +46 -0
  115. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
  116. package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
  117. package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
  118. package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
  119. package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
  120. package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
  121. package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
  122. package/tests/01-Lifecycle/main.test.ts +173 -0
  123. package/tsconfig.json +28 -0
  124. package/workspace-rt.ts +134 -0
  125. package/workspace.yaml +3 -0
@@ -0,0 +1,522 @@
1
+
2
+ import { join, resolve } 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': {},
20
+ '#t44/structs/WorkspaceConfig': {
21
+ as: '$WorkspaceConfig'
22
+ },
23
+ '#t44/structs/WorkspacePublishingConfig': {
24
+ as: '$WorkspaceRepositories'
25
+ },
26
+ '#t44/structs/WorkspaceProjectsConfig': {
27
+ as: '$WorkspaceProjectsConfig'
28
+ },
29
+ '#': {
30
+ WorkspaceConfig: {
31
+ type: CapsulePropertyTypes.Mapping,
32
+ value: 't44/caps/WorkspaceConfig'
33
+ },
34
+ WorkspaceProjects: {
35
+ type: CapsulePropertyTypes.Mapping,
36
+ value: 't44/caps/WorkspaceProjects'
37
+ },
38
+ ProjectRepository: {
39
+ type: CapsulePropertyTypes.Mapping,
40
+ value: 't44/caps/ProjectRepository'
41
+ },
42
+ SemverProvider: {
43
+ type: CapsulePropertyTypes.Mapping,
44
+ value: 't44/caps/providers/semver.org/ProjectPublishing'
45
+ },
46
+ GitRepository: {
47
+ type: CapsulePropertyTypes.Mapping,
48
+ value: 't44/caps/providers/git-scm.com/ProjectPublishing'
49
+ },
50
+ NpmRegistry: {
51
+ type: CapsulePropertyTypes.Mapping,
52
+ value: 't44/caps/providers/npmjs.com/ProjectPublishing'
53
+ },
54
+ GitHubRepository: {
55
+ type: CapsulePropertyTypes.Mapping,
56
+ value: 't44/caps/providers/github.com/ProjectPublishing'
57
+ },
58
+ ProjectRack: {
59
+ type: CapsulePropertyTypes.Mapping,
60
+ value: 't44/caps/ProjectRack'
61
+ },
62
+ HomeRegistry: {
63
+ type: CapsulePropertyTypes.Mapping,
64
+ value: 't44/caps/HomeRegistry'
65
+ },
66
+ ProjectCatalogs: {
67
+ type: CapsulePropertyTypes.Mapping,
68
+ value: 't44/caps/ProjectCatalogs'
69
+ },
70
+ run: {
71
+ type: CapsulePropertyTypes.Function,
72
+ value: async function (this: any, { args }: any): Promise<void> {
73
+
74
+ const { projectSelector, rc, release, bump, publish, dangerouslyResetMain, yesSignoff } = args
75
+
76
+ // Determine if this is a dry-run (default) or actual publish
77
+ const isDryRun = !rc && !release && !bump && !publish
78
+ const shouldBumpVersions = rc || release || bump
79
+
80
+ // Provider filter: when --publish <filter> is given, only matching providers run
81
+ const publishFilter = typeof publish === 'string' ? publish : null
82
+ const PROVIDER_FILTERS: Record<string, string[]> = {
83
+ git: [
84
+ 't44/caps/providers/git-scm.com/ProjectPublishing',
85
+ 't44/caps/providers/github.com/ProjectPublishing',
86
+ ],
87
+ }
88
+ const isProviderIncluded = (capsuleName: string): boolean => {
89
+ if (!publishFilter) return true
90
+ const allowed = PROVIDER_FILTERS[publishFilter]
91
+ if (!allowed) {
92
+ console.log(`[t44] WARNING: Unknown provider filter '${publishFilter}', running all providers\n`)
93
+ return true
94
+ }
95
+ return allowed.includes(capsuleName)
96
+ }
97
+
98
+ const repositoriesConfig = await this.$WorkspaceRepositories.config
99
+
100
+ if (!repositoriesConfig?.repositories) {
101
+ throw new Error('No repositories configuration found')
102
+ }
103
+
104
+ if (dangerouslyResetMain && !projectSelector) {
105
+ throw new Error('--dangerously-reset-main flag requires a projectSelector or FORCE_FOR_ALL to be specified')
106
+ }
107
+
108
+ let matchingRepositories: Record<string, any>
109
+
110
+ if (!projectSelector || projectSelector === 'FORCE_FOR_ALL') {
111
+ matchingRepositories = repositoriesConfig.repositories
112
+ } else {
113
+ matchingRepositories = await this.WorkspaceProjects.resolveMatchingRepositories({
114
+ workspaceProject: projectSelector,
115
+ repositories: repositoriesConfig.repositories
116
+ })
117
+ }
118
+
119
+ // Show mode indicator
120
+ if (isDryRun) {
121
+ console.log('[t44] DRY-RUN MODE: Going through all motions without irreversible operations\n')
122
+ console.log('[t44] Use --rc, --release, or --bump to perform actual operations\n')
123
+ } else if (bump) {
124
+ console.log('[t44] BUMP MODE: Will bump versions but skip tagging and publishing\n')
125
+ } else if (publish) {
126
+ if (publishFilter) {
127
+ console.log(`[t44] PUBLISH MODE: Pushing current state to '${publishFilter}' providers only (no version bump or tagging)\n`)
128
+ } else {
129
+ console.log('[t44] PUBLISH MODE: Pushing current state to all providers (no version bump or tagging)\n')
130
+ }
131
+ }
132
+
133
+ // Phase 1: Copy source directories to stage location (git-tracked)
134
+ console.log('[t44] Syncing source directories to stage repos ...\n')
135
+ const stageSourceDirs: Map<string, string> = new Map()
136
+
137
+ const syncToStage = async (repoName: string, repoConfig: any) => {
138
+ const projectSourceDir = join((repoConfig as any).sourceDir)
139
+ const repoSourceDir = await this.ProjectRepository.getStagePath({ repoUri: repoName })
140
+
141
+ // Init git repo if not already
142
+ await this.ProjectRepository.init({ rootDir: repoSourceDir })
143
+
144
+ // Reset working tree to last commit before copying
145
+ await this.ProjectRepository.reset({ rootDir: repoSourceDir })
146
+
147
+ // Sync files from source to stage repo
148
+ const gitignorePath = join(projectSourceDir, '.gitignore')
149
+ await this.ProjectRepository.sync({
150
+ rootDir: repoSourceDir,
151
+ sourceDir: projectSourceDir,
152
+ gitignorePath
153
+ })
154
+
155
+ stageSourceDirs.set(repoName, repoSourceDir)
156
+ return repoSourceDir
157
+ }
158
+
159
+ // Update source package.json private field based on npm provider public config
160
+ for (const [, repoConfig] of Object.entries(matchingRepositories)) {
161
+ const providers = Array.isArray((repoConfig as any).providers)
162
+ ? (repoConfig as any).providers
163
+ : (repoConfig as any).provider
164
+ ? [(repoConfig as any).provider]
165
+ : []
166
+
167
+ for (const providerConfig of providers) {
168
+ if (providerConfig.capsule === 't44/caps/providers/npmjs.com/ProjectPublishing') {
169
+ const publicSetting = providerConfig.config?.PackageSettings?.public
170
+ if (publicSetting !== undefined) {
171
+ const projectSourceDir = join((repoConfig as any).sourceDir)
172
+ const sourcePackageJsonPath = join(projectSourceDir, 'package.json')
173
+ try {
174
+ const content = await readFile(sourcePackageJsonPath, 'utf-8')
175
+ const packageJson = JSON.parse(content)
176
+ const desiredPrivate = !publicSetting
177
+ if (packageJson.private !== desiredPrivate) {
178
+ packageJson.private = desiredPrivate
179
+ const indent = content.match(/^\{\s*\n([ \t]+)/)
180
+ const indentSize = indent ? indent[1].length : 2
181
+ await writeFile(sourcePackageJsonPath, JSON.stringify(packageJson, null, indentSize) + '\n', 'utf-8')
182
+ console.log(` ✓ Updated ${sourcePackageJsonPath} private: ${desiredPrivate}`)
183
+ }
184
+ } catch { }
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
191
+ console.log(`=> Syncing '${repoName}' ...`)
192
+ const repoSourceDir = await syncToStage(repoName, repoConfig)
193
+ console.log(` Synced to: ${repoSourceDir}\n`)
194
+ }
195
+
196
+ // Helper to apply renames and resolve workspace dependencies on stage source dirs
197
+ const applyRenamesAndFinalize = async () => {
198
+ const matchingDirs = new Map(
199
+ Object.keys(matchingRepositories)
200
+ .filter(name => stageSourceDirs.has(name))
201
+ .map(name => [name, stageSourceDirs.get(name)!])
202
+ )
203
+ await this.SemverProvider.rename({
204
+ dirs: matchingDirs.values(),
205
+ repos: Object.fromEntries(matchingDirs)
206
+ })
207
+ }
208
+
209
+ // Phase 2: Detect source changes and bump versions
210
+ const bumpedRepos = new Set<string>()
211
+
212
+ if (shouldBumpVersions) {
213
+ if (rc) console.log('[t44] Release candidate mode enabled\n')
214
+ if (release) console.log('[t44] Release mode enabled\n')
215
+ if (bump) console.log('[t44] Bump mode enabled\n')
216
+
217
+ console.log('[t44] Bumping versions ...\n')
218
+
219
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
220
+ const repoSourceDir = stageSourceDirs.get(repoName)!
221
+
222
+ // Check if there are changes since last committed state
223
+ const hasChanges = await this.ProjectRepository.hasChanges({ rootDir: repoSourceDir })
224
+
225
+ if (!hasChanges) {
226
+ console.log(`=> Skipping bump for '${repoName}' (no changes)\n`)
227
+ continue
228
+ }
229
+
230
+ console.log(`=> Bumping version for '${repoName}' ...\n`)
231
+
232
+ const result = await this.SemverProvider.bump({
233
+ config: repoConfig,
234
+ options: { rc, release, bump }
235
+ })
236
+
237
+ if (result?.newVersion) {
238
+ bumpedRepos.add(repoName)
239
+
240
+ // Update version in stage repo's package.json too
241
+ const stagePackageJsonPath = join(repoSourceDir, 'package.json')
242
+ const stageContent = await readFile(stagePackageJsonPath, 'utf-8')
243
+ const stagePackageJson = JSON.parse(stageContent)
244
+ stagePackageJson.version = result.newVersion
245
+ const indent = stageContent.match(/^\{\s*\n([ \t]+)/)
246
+ const indentSize = indent ? indent[1].length : 2
247
+ await writeFile(stagePackageJsonPath, JSON.stringify(stagePackageJson, null, indentSize) + '\n', 'utf-8')
248
+ }
249
+ }
250
+
251
+ console.log('[t44] Version bump complete!\n')
252
+ }
253
+
254
+ // Phase 3: Apply renames and resolve workspace dependencies
255
+ await applyRenamesAndFinalize()
256
+
257
+ // Phase 4: Commit the final state for all repos that have changes
258
+ if (shouldBumpVersions && !bump) {
259
+ for (const [repoName] of Object.entries(matchingRepositories)) {
260
+ const repoSourceDir = stageSourceDirs.get(repoName)!
261
+ await this.ProjectRepository.commit({ rootDir: repoSourceDir, message: 'bump' })
262
+ }
263
+ }
264
+
265
+ // Helper to iterate providers with custom callback
266
+ const forEachProvider = async (callback: (params: {
267
+ repoName: string,
268
+ repoConfig: any,
269
+ providerConfig: any,
270
+ capsuleName: string,
271
+ repoSourceDir: string
272
+ }) => Promise<void>) => {
273
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
274
+ const providers = Array.isArray((repoConfig as any).providers)
275
+ ? (repoConfig as any).providers
276
+ : (repoConfig as any).provider
277
+ ? [(repoConfig as any).provider]
278
+ : []
279
+
280
+ const repoSourceDir = stageSourceDirs.get(repoName)!
281
+ for (const providerConfig of providers) {
282
+ const capsuleName = providerConfig.capsule
283
+ await callback({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir })
284
+ }
285
+ }
286
+ }
287
+
288
+ // Phase 4: Prepare all providers (copy from stage source to projection dirs)
289
+ console.log('[t44] Preparing providers ...\n')
290
+ const packageMetadata: Map<string, any> = new Map()
291
+ const gitMetadata: Map<string, any> = new Map()
292
+
293
+ await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
294
+ if (!isProviderIncluded(capsuleName)) return
295
+ if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing') {
296
+ const metadata = await this.NpmRegistry.prepare({
297
+ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
298
+ projectionDir: join(
299
+ this.WorkspaceConfig.workspaceRootDir,
300
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectPublishing/@t44.sh~t44~caps~providers~npmjs.com~ProjectPublishing'
301
+ ),
302
+ repoSourceDir
303
+ })
304
+ packageMetadata.set(repoName, metadata)
305
+ } else if (capsuleName === 't44/caps/providers/github.com/ProjectPublishing' && !isDryRun) {
306
+ // Ensure GitHub repo exists before git-scm.com tries to clone from it
307
+ await this.GitHubRepository.push({ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir } })
308
+ } else if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing') {
309
+ const metadata = await this.GitRepository.prepare({
310
+ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
311
+ projectionDir: join(
312
+ this.WorkspaceConfig.workspaceRootDir,
313
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectPublishing/@t44.sh~t44~caps~providers~git-scm.com~ProjectPublishing'
314
+ )
315
+ })
316
+ gitMetadata.set(repoName, metadata)
317
+ }
318
+ })
319
+
320
+ // Phase 5: Tag git repos with version (only bumped repos)
321
+ if ((rc || release) && !isDryRun && !publish) {
322
+ const taggedRepos = new Set<string>()
323
+ await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
324
+ if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing' && !taggedRepos.has(repoName)) {
325
+ if (!bumpedRepos.has(repoName)) {
326
+ console.log(` ○ Skipping tag for '${repoName}' (not bumped)\n`)
327
+ taggedRepos.add(repoName)
328
+ return
329
+ }
330
+ const metadata = gitMetadata.get(repoName)
331
+ if (!metadata?.stageDir) return
332
+
333
+ await this.GitRepository.tag({ metadata, repoSourceDir })
334
+ taggedRepos.add(repoName)
335
+ }
336
+ })
337
+ }
338
+
339
+ // Phase 5.5: Sync selected project repos to project rack registry
340
+ const rackName = await this.ProjectRack.getRackName()
341
+ if (rackName) {
342
+ const registryRootDir = await this.HomeRegistry.rootDir
343
+ const rackStructDir = 't44/structs/ProjectRack'.replace(/\//g, '~')
344
+ const rackCapsuleDir = 't44/caps/ProjectRepository'.replace(/\//g, '~')
345
+ const workspaceConfig = await this.$WorkspaceConfig.config
346
+ const workspaceRootDir = workspaceConfig?.rootDir
347
+ const projects = await this.WorkspaceProjects.list
348
+
349
+ // Determine which projects have matching repositories
350
+ const matchingProjectNames = new Set<string>()
351
+ if (workspaceRootDir) {
352
+ const { resolve, relative } = await import('path')
353
+ for (const [, repoConfig] of Object.entries(matchingRepositories)) {
354
+ const typedConfig = repoConfig as any
355
+ if (typedConfig.sourceDir) {
356
+ const resolvedSourceDir = resolve(typedConfig.sourceDir)
357
+ const relPath = relative(workspaceRootDir, resolvedSourceDir)
358
+ const topDir = relPath.split('/')[0]
359
+ matchingProjectNames.add(topDir)
360
+ }
361
+ }
362
+ }
363
+
364
+ console.log(`[t44] Syncing project repos to project rack '${rackName}' ...\n`)
365
+
366
+ for (const [projectName, projectData] of Object.entries(projects)) {
367
+ if (matchingProjectNames.size > 0 && !matchingProjectNames.has(projectName)) {
368
+ continue
369
+ }
370
+
371
+ const project = projectData as any
372
+ const projectDid = project.identifier?.did
373
+ if (!projectDid) {
374
+ console.log(` ○ Skipping '${projectName}' (no project identifier)`)
375
+ continue
376
+ }
377
+
378
+ const projectSourceDir = project.sourceDir
379
+ const rackRepoDir = join(registryRootDir, rackStructDir, rackName, rackCapsuleDir, projectDid)
380
+ try {
381
+ // Init bare repo in rack registry if needed
382
+ await this.ProjectRepository.initBare({ rootDir: rackRepoDir })
383
+
384
+ // Add remote to source repo if not present, or update URL
385
+ const remoteName = 't44/caps/ProjectRack'
386
+ const hasRemote = await this.ProjectRepository.hasRemote({ rootDir: projectSourceDir, name: remoteName })
387
+ if (!hasRemote) {
388
+ await this.ProjectRepository.addRemote({ rootDir: projectSourceDir, name: remoteName, url: rackRepoDir })
389
+ } else {
390
+ await this.ProjectRepository.setRemoteUrl({ rootDir: projectSourceDir, name: remoteName, url: rackRepoDir })
391
+ }
392
+
393
+ // Push source repo to rack registry
394
+ const branch = await this.ProjectRepository.getBranch({ rootDir: projectSourceDir })
395
+ await this.ProjectRepository.pushToRemote({ rootDir: projectSourceDir, remote: remoteName, branch, force: true })
396
+
397
+ console.log(` ✓ Synced '${projectName}' to rack`)
398
+ } catch (error: any) {
399
+ const chalk = (await import('chalk')).default
400
+ console.log(chalk.red(`\n ✗ Failed to sync '${projectName}' to project rack '${rackName}'`))
401
+ console.log(chalk.red(` ${error.message || error}`))
402
+ console.log(chalk.red(`[t44] ABORT: Rack sync failed. Not pushing to external providers.\n`))
403
+ return
404
+ }
405
+ }
406
+
407
+ console.log(`[t44] Rack sync complete.\n`)
408
+ }
409
+
410
+ // Phase 6a: Push all providers
411
+ if (isDryRun) {
412
+ console.log('[t44] DRY-RUN: Skipping publishing (would publish packages here)\n')
413
+ } else {
414
+ console.log('[t44] Publishing packages ...\n')
415
+ }
416
+ const processedRepos = new Set<string>()
417
+ await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName, repoSourceDir }) => {
418
+ if (!isProviderIncluded(capsuleName)) return
419
+ if (!processedRepos.has(repoName)) {
420
+ console.log(`\n=> Processing repository '${repoName}' ...\n`)
421
+ processedRepos.add(repoName)
422
+ }
423
+
424
+ if (isDryRun) {
425
+ console.log(` -> DRY-RUN: Skipping provider '${capsuleName}'\n`)
426
+ } else {
427
+ console.log(` -> Running provider '${capsuleName}' ...\n`)
428
+ }
429
+
430
+ if (capsuleName === 't44/caps/providers/github.com/ProjectPublishing' && !isDryRun) {
431
+ await this.GitHubRepository.push({
432
+ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir }
433
+ })
434
+ } else if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing' && !isDryRun) {
435
+ await this.GitRepository.push({
436
+ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
437
+ dangerouslyResetMain,
438
+ yesSignoff,
439
+ metadata: gitMetadata.get(repoName),
440
+ projectSourceDir: (repoConfig as any).sourceDir
441
+ })
442
+ } else if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing' && !isDryRun) {
443
+ await this.NpmRegistry.push({
444
+ config: { ...repoConfig, provider: providerConfig, sourceDir: repoSourceDir },
445
+ projectionDir: join(
446
+ this.WorkspaceConfig.workspaceRootDir,
447
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectPublishing/@t44.sh~t44~caps~providers~npmjs.com~ProjectPublishing'
448
+ ),
449
+ metadata: packageMetadata.get(repoName)
450
+ })
451
+ }
452
+
453
+ if (!isDryRun) {
454
+ console.log(` <- Provider '${capsuleName}' complete.\n`)
455
+ }
456
+ })
457
+
458
+ for (const repoName of processedRepos) {
459
+ console.log(`<= Repository '${repoName}' processing complete.\n`)
460
+ }
461
+
462
+ // Phase 6b: Update catalogs after all pushes complete
463
+ if (!isDryRun) {
464
+ const catalogRepos = new Set<string>()
465
+ await forEachProvider(async ({ repoName, repoConfig, providerConfig, capsuleName }) => {
466
+ if (!isProviderIncluded(capsuleName)) return
467
+
468
+ if (!catalogRepos.has(repoName)) {
469
+ catalogRepos.add(repoName)
470
+ const repoSourceDir_ = resolve((repoConfig as any).sourceDir)
471
+ const workspaceProjectName = await this.WorkspaceProjects.findProjectForPath({ targetPath: repoSourceDir_ }) || ''
472
+ await this.ProjectCatalogs.updateCatalogRepository({
473
+ repoName,
474
+ providerKey: '#' + capsule['#'],
475
+ providerData: {
476
+ sourceDir: repoSourceDir_,
477
+ workspaceProjectName,
478
+ },
479
+ })
480
+ }
481
+
482
+ if (capsuleName === 't44/caps/providers/github.com/ProjectPublishing') {
483
+ await this.GitHubRepository.afterPush({
484
+ repoName,
485
+ config: { ...repoConfig, provider: providerConfig, sourceDir: (repoConfig as any).sourceDir },
486
+ })
487
+ } else if (capsuleName === 't44/caps/providers/git-scm.com/ProjectPublishing') {
488
+ await this.GitRepository.afterPush({
489
+ repoName,
490
+ config: { ...repoConfig, provider: providerConfig, sourceDir: (repoConfig as any).sourceDir },
491
+ metadata: gitMetadata.get(repoName),
492
+ })
493
+ } else if (capsuleName === 't44/caps/providers/npmjs.com/ProjectPublishing') {
494
+ await this.NpmRegistry.afterPush({
495
+ repoName,
496
+ metadata: packageMetadata.get(repoName),
497
+ })
498
+ }
499
+ })
500
+ }
501
+
502
+ if (isDryRun) {
503
+ console.log('[t44] DRY-RUN complete! No irreversible operations were performed.')
504
+ console.log('[t44] To actually publish, use: t44 push --rc (for release candidate) or t44 push --release')
505
+ console.log('[t44] To bump versions only: t44 push --bump')
506
+ } else if (bump) {
507
+ console.log('[t44] Version bump complete! Versions updated in package.json files.')
508
+ console.log('[t44] To tag and publish, use: t44 push --rc or t44 push --release')
509
+ } else {
510
+ console.log('[t44] Project repositories pushed OK!')
511
+ }
512
+ }
513
+ }
514
+ }
515
+ }
516
+ }, {
517
+ importMeta: import.meta,
518
+ importStack: makeImportStack(),
519
+ capsuleName: capsule['#'],
520
+ })
521
+ }
522
+ capsule['#'] = 't44/caps/ProjectPublishing'