@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,654 @@
1
+
2
+ import { join, resolve } from 'path'
3
+ import { readFile, writeFile } from 'fs/promises'
4
+ import chalk from 'chalk'
5
+ import glob from 'fast-glob'
6
+
7
+ // ── Provider Lifecycle Steps ─────────────────────────────────────────
8
+ // Each provider capsule can implement any subset of these methods.
9
+ // The orchestrator calls them in order for each repository's providers.
10
+ //
11
+ // 1. validateSource — validate source dirs before sync
12
+ // 2. prepareSource — modify source before sync (e.g. npm private field)
13
+ // 3. bump — bump version (e.g. semver RC/release)
14
+ // 4. ensureRemote — ensure remote targets exist (e.g. create GitHub repo)
15
+ // 5. prepare — set up projection/stage dirs, store metadata on ctx
16
+ // 6. tag — tag repos with version
17
+ // 7. push — publish/push to provider
18
+ // 8. afterPush — post-push catalog updates
19
+ //
20
+ // Every method receives { config, ctx } where:
21
+ // config = the provider's own config entry (with .capsule and .config)
22
+ // ctx = shared publishingContext for the current repository
23
+ //
24
+ // ── Provider Tags ────────────────────────────────────────────────────
25
+ // Each provider capsule declares a `tags` property (e.g. ['git'], ['npm'])
26
+ // as part of its capsule definition. Tags classify what kind of publishing
27
+ // a provider performs. The orchestrator queries `provider.tags` after
28
+ // loading the capsule — tags are NOT stored in workspace config because
29
+ // they are an intrinsic property of the capsule itself.
30
+ //
31
+ // When the user runs `t44 push --git` or `t44 push --pkg`, only providers
32
+ // whose tags include the matching value will run. For example:
33
+ // - `--git` runs providers tagged 'git' (git-scm, github, OI, dco)
34
+ // - `--pkg` runs providers tagged 'pkg' (npmjs)
35
+ // - neither runs all providers
36
+ //
37
+ // Providers without tags (e.g. sourcemint license, semver) are skipped
38
+ // when a filter is active, which is correct because --git/--pkg mode only
39
+ // pushes to external targets without re-validating or bumping.
40
+
41
+ export async function capsule({
42
+ encapsulate,
43
+ CapsulePropertyTypes,
44
+ makeImportStack
45
+ }: {
46
+ encapsulate: any
47
+ CapsulePropertyTypes: any
48
+ makeImportStack: any
49
+ }) {
50
+ return encapsulate({
51
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
52
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
53
+ '#@stream44.studio/t44/structs/WorkspaceConfig': {
54
+ as: '$WorkspaceConfig'
55
+ },
56
+ '#@stream44.studio/t44/structs/ProjectPublishingConfig': {
57
+ as: '$WorkspaceRepositories'
58
+ },
59
+ '#@stream44.studio/t44/structs/WorkspaceProjectsConfig': {
60
+ as: '$WorkspaceProjectsConfig'
61
+ },
62
+ '#': {
63
+ WorkspaceConfig: {
64
+ type: CapsulePropertyTypes.Mapping,
65
+ value: '@stream44.studio/t44/caps/WorkspaceConfig'
66
+ },
67
+ WorkspaceProjects: {
68
+ type: CapsulePropertyTypes.Mapping,
69
+ value: '@stream44.studio/t44/caps/WorkspaceProjects'
70
+ },
71
+ ProjectRepository: {
72
+ type: CapsulePropertyTypes.Mapping,
73
+ value: '@stream44.studio/t44/caps/ProjectRepository'
74
+ },
75
+ ProjectRack: {
76
+ type: CapsulePropertyTypes.Mapping,
77
+ value: '@stream44.studio/t44/caps/ProjectRack'
78
+ },
79
+ HomeRegistry: {
80
+ type: CapsulePropertyTypes.Mapping,
81
+ value: '@stream44.studio/t44/caps/HomeRegistry'
82
+ },
83
+ ProjectCatalogs: {
84
+ type: CapsulePropertyTypes.Mapping,
85
+ value: '@stream44.studio/t44/caps/ProjectCatalogs'
86
+ },
87
+ run: {
88
+ type: CapsulePropertyTypes.Function,
89
+ value: async function (this: any, { args }: any): Promise<void> {
90
+
91
+ const { projectSelector, rc, release, bump, git, pkg, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, dangerouslySquashToCommit, branch, yesSignoff } = args
92
+
93
+ // ── Dynamic provider loader ──────────────────────────
94
+ const providerCache = new Map<string, any>()
95
+ const getProvider = async (uri: string) => {
96
+ const cleanUri = uri.startsWith('#') ? uri.substring(1) : uri
97
+ if (!providerCache.has(cleanUri)) {
98
+ const { api } = await this.self.importCapsule({ uri: cleanUri })
99
+ providerCache.set(cleanUri, api)
100
+ }
101
+ return providerCache.get(cleanUri)!
102
+ }
103
+
104
+ // ── Mode flags ───────────────────────────────────────
105
+ const publishFilter = git ? 'git' : pkg ? 'pkg' : null
106
+ const isDryRun = !rc && !release && !bump && !publishFilter
107
+ const shouldBumpVersions = rc || release || bump
108
+
109
+ // ── Provider filter (tag-based + enabled flag) ───────
110
+ // When --git or --pkg is given, only providers whose
111
+ // capsule exposes a matching `tags` property will run.
112
+ // Tags are queried from the loaded capsule, not from config.
113
+ // Additionally, providers with `enabled: false` are always skipped.
114
+ const isProviderIncluded = async (providerConfig: any): Promise<boolean> => {
115
+ // Check enabled flag first - if explicitly false, skip this provider
116
+ if (providerConfig.enabled === false) return false
117
+ if (!publishFilter) return true
118
+ const provider = await getProvider(providerConfig.capsule)
119
+ const tags: string[] | undefined = provider.tags
120
+ if (!tags || tags.length === 0) return false
121
+ return tags.includes(publishFilter)
122
+ }
123
+
124
+ // ── Config helpers ────────────────────────────────────
125
+ const deepMerge = (base: any, override: any): any => {
126
+ if (override === null || override === undefined) return base
127
+ if (base === null || base === undefined) return override
128
+ if (typeof base !== 'object' || typeof override !== 'object') return override
129
+ if (Array.isArray(base) || Array.isArray(override)) return override
130
+ const result: any = { ...base }
131
+ for (const key of Object.keys(override)) {
132
+ result[key] = deepMerge(base[key], override[key])
133
+ }
134
+ return result
135
+ }
136
+
137
+ const resolveRepoProviders = (repoConfig: any, globalProviders: any[]): any[] => {
138
+ const repoProviders: any[] = Array.isArray(repoConfig.providers)
139
+ ? repoConfig.providers
140
+ : repoConfig.provider
141
+ ? [repoConfig.provider]
142
+ : []
143
+
144
+ const globalDefaults = new Map<string, any>(
145
+ globalProviders.map((p: any) => [p.capsule, p])
146
+ )
147
+
148
+ const merged: any[] = []
149
+ const seen = new Set<string>()
150
+
151
+ for (const repoProvider of repoProviders) {
152
+ const capsuleName = repoProvider.capsule
153
+ const globalDefault = globalDefaults.get(capsuleName)
154
+ if (globalDefault) {
155
+ // Merge: repo-level enabled overrides global, config is deep-merged
156
+ const mergedProvider = {
157
+ ...globalDefault,
158
+ ...repoProvider,
159
+ config: deepMerge(globalDefault.config, repoProvider.config),
160
+ }
161
+ // Explicit enabled at repo level takes precedence
162
+ if ('enabled' in repoProvider) {
163
+ mergedProvider.enabled = repoProvider.enabled
164
+ } else if ('enabled' in globalDefault) {
165
+ mergedProvider.enabled = globalDefault.enabled
166
+ }
167
+ merged.push(mergedProvider)
168
+ } else {
169
+ merged.push(repoProvider)
170
+ }
171
+ seen.add(capsuleName)
172
+ }
173
+
174
+ for (const globalProvider of globalProviders) {
175
+ if (!seen.has(globalProvider.capsule)) {
176
+ merged.unshift(globalProvider)
177
+ }
178
+ }
179
+
180
+ return merged
181
+ }
182
+
183
+ // ── Publishing API (passed to providers) ─────────────
184
+ const publishingApi = {
185
+ getProjectionDir: (capsuleName: string) => join(
186
+ this.WorkspaceConfig.workspaceRootDir,
187
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectPublishing',
188
+ capsuleName.replace(/\//g, '~')
189
+ ),
190
+ }
191
+
192
+ // ── Load config ───────────────────────────────────────
193
+ const repositoriesConfig = await this.$WorkspaceRepositories.config
194
+
195
+ if (!repositoriesConfig?.repositories) {
196
+ throw new Error('No repositories configuration found')
197
+ }
198
+
199
+ if (dangerouslyResetMain && !projectSelector) {
200
+ throw new Error('--dangerously-reset-main flag requires a projectSelector or FORCE_FOR_ALL to be specified')
201
+ }
202
+
203
+ let matchingRepositories: Record<string, any>
204
+
205
+ if (!projectSelector || projectSelector === 'FORCE_FOR_ALL') {
206
+ matchingRepositories = repositoriesConfig.repositories
207
+ } else {
208
+ matchingRepositories = await this.WorkspaceProjects.resolveMatchingRepositories({
209
+ workspaceProject: projectSelector,
210
+ repositories: repositoriesConfig.repositories
211
+ })
212
+ }
213
+
214
+ // ── Branch validation ───────────────────────────────
215
+ // --branch flag requires exactly one project to be selected
216
+ if (branch && Object.keys(matchingRepositories).length > 1) {
217
+ throw new Error(
218
+ `--branch flag requires a single project to be selected, but ${Object.keys(matchingRepositories).length} repositories matched.\n` +
219
+ ` Specify a projectSelector to narrow down to one project, then use --branch.`
220
+ )
221
+ }
222
+
223
+ // ── Resolve per-repo effective branch ───────────────
224
+ // Priority: CLI --branch > config.activePublishingBranch > undefined (defaults to 'main' downstream)
225
+ const repoEffectiveBranches = new Map<string, string | undefined>()
226
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
227
+ if (branch) {
228
+ repoEffectiveBranches.set(repoName, branch)
229
+ } else {
230
+ const configBranch = (repoConfig as any).activePublishingBranch
231
+ repoEffectiveBranches.set(repoName, configBranch || undefined)
232
+ }
233
+ }
234
+
235
+ // ── Show mode indicator ──────────────────────────────
236
+ if (isDryRun) {
237
+ console.log('[t44] DRY-RUN MODE: Going through all motions without irreversible operations\n')
238
+ console.log('[t44] Use --rc, --release, or --bump to perform actual operations\n')
239
+ } else if (bump) {
240
+ console.log('[t44] BUMP MODE: Will bump versions but skip tagging and publishing\n')
241
+ } else if (publishFilter) {
242
+ console.log(`[t44] PUBLISH MODE: Pushing current state to '${publishFilter}' providers only (no version bump or tagging)\n`)
243
+ }
244
+
245
+ const globalProviders: any[] = Array.isArray(repositoriesConfig.providers)
246
+ ? repositoriesConfig.providers
247
+ : []
248
+
249
+ // ── Helper: call a lifecycle step on all providers for a repo ──
250
+ const callProvidersForRepo = async (
251
+ step: string,
252
+ repoName: string,
253
+ repoConfig: any,
254
+ repoSourceDir: string,
255
+ ctx: any,
256
+ ) => {
257
+ const providers = resolveRepoProviders(repoConfig, globalProviders)
258
+ ctx.mergedProviders = providers
259
+ for (const providerConfig of providers) {
260
+ if (!await isProviderIncluded(providerConfig)) continue
261
+
262
+ const provider = await getProvider(providerConfig.capsule)
263
+ if (typeof provider[step] !== 'function') continue
264
+
265
+ await provider[step]({
266
+ config: providerConfig,
267
+ ctx,
268
+ })
269
+ }
270
+ }
271
+
272
+ // ══════════════════════════════════════════════════════
273
+ // STEP 1: validateSource — validate source dirs before sync
274
+ // ══════════════════════════════════════════════════════
275
+ console.log('[t44] Validating source directories ...\n')
276
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
277
+ const ctx = {
278
+ repoName,
279
+ repoConfig,
280
+ repoSourceDir: join((repoConfig as any).sourceDir),
281
+ options: { isDryRun, shouldBumpVersions, rc, release, bump, git, pkg, publishFilter, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, dangerouslySquashToCommit, branch: repoEffectiveBranches.get(repoName), yesSignoff },
282
+ metadata: {} as Record<string, any>,
283
+ alwaysIgnore: repositoriesConfig.alwaysIgnore || [],
284
+ publishingApi,
285
+ }
286
+ await callProvidersForRepo('validateSource', repoName, repoConfig, ctx.repoSourceDir, ctx)
287
+ }
288
+
289
+ // ══════════════════════════════════════════════════════
290
+ // STEP 2: prepareSource — modify source before sync
291
+ // ══════════════════════════════════════════════════════
292
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
293
+ const ctx = {
294
+ repoName,
295
+ repoConfig,
296
+ repoSourceDir: join((repoConfig as any).sourceDir),
297
+ options: { isDryRun, shouldBumpVersions, rc, release, bump, git, pkg, publishFilter, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, dangerouslySquashToCommit, branch: repoEffectiveBranches.get(repoName), yesSignoff },
298
+ metadata: {} as Record<string, any>,
299
+ alwaysIgnore: repositoriesConfig.alwaysIgnore || [],
300
+ publishingApi,
301
+ }
302
+ await callProvidersForRepo('prepareSource', repoName, repoConfig, ctx.repoSourceDir, ctx)
303
+ }
304
+
305
+ // ══════════════════════════════════════════════════════
306
+ // INTERNAL: Check for .rej files (unresolved patch conflicts)
307
+ // ══════════════════════════════════════════════════════
308
+ const allRejFiles: string[] = []
309
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
310
+ const projectSourceDir = join((repoConfig as any).sourceDir)
311
+ const rejFiles = await glob('**/*.rej', {
312
+ cwd: projectSourceDir,
313
+ absolute: false,
314
+ onlyFiles: true,
315
+ dot: true,
316
+ ignore: ['**/node_modules/**', '**/.git/**']
317
+ })
318
+ for (const rejFile of rejFiles) {
319
+ allRejFiles.push(join(projectSourceDir, rejFile))
320
+ }
321
+ }
322
+
323
+ if (allRejFiles.length > 0) {
324
+ console.log(chalk.red('\n[t44] ERROR: Found unresolved patch conflict files (.rej)\n'))
325
+ console.log(chalk.red('The following .rej files must be resolved and removed before publishing:\n'))
326
+ for (const rejFile of allRejFiles) {
327
+ console.log(chalk.red(` • ${rejFile}`))
328
+ }
329
+ console.log(chalk.yellow('\nThese files are created when `patch` fails to apply a hunk cleanly.'))
330
+ console.log(chalk.yellow('Review each .rej file, manually apply the changes, then delete the .rej files.\n'))
331
+ process.exit(1)
332
+ }
333
+
334
+ // ══════════════════════════════════════════════════════
335
+ // INTERNAL: Sync source directories to stage repos
336
+ // ══════════════════════════════════════════════════════
337
+ console.log('[t44] Syncing source directories to stage repos ...\n')
338
+ const stageSourceDirs: Map<string, string> = new Map()
339
+
340
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
341
+ const projectSourceDir = join((repoConfig as any).sourceDir)
342
+ const repoSourceDir = await this.ProjectRepository.getStagePath({ repoUri: repoName })
343
+
344
+ await this.ProjectRepository.init({ rootDir: repoSourceDir })
345
+ await this.ProjectRepository.reset({ rootDir: repoSourceDir })
346
+
347
+ const gitignorePath = join(projectSourceDir, '.gitignore')
348
+ await this.ProjectRepository.sync({
349
+ rootDir: repoSourceDir,
350
+ sourceDir: projectSourceDir,
351
+ gitignorePath,
352
+ excludePatterns: repositoriesConfig.alwaysIgnore || []
353
+ })
354
+
355
+ stageSourceDirs.set(repoName, repoSourceDir)
356
+ console.log(`=> Synced '${repoName}' to: ${repoSourceDir}\n`)
357
+ }
358
+
359
+ // ══════════════════════════════════════════════════════
360
+ // STEP 3: bump — bump versions via providers
361
+ // ══════════════════════════════════════════════════════
362
+ const bumpedRepos = new Set<string>()
363
+
364
+ if (shouldBumpVersions) {
365
+ if (rc) console.log('[t44] Release candidate mode enabled\n')
366
+ if (release) console.log('[t44] Release mode enabled\n')
367
+ if (bump) console.log('[t44] Bump mode enabled\n')
368
+
369
+ console.log('[t44] Bumping versions ...\n')
370
+
371
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
372
+ const repoSourceDir = stageSourceDirs.get(repoName)!
373
+
374
+ const hasChanges = await this.ProjectRepository.hasChanges({ rootDir: repoSourceDir })
375
+ if (!hasChanges) {
376
+ console.log(`=> Skipping bump for '${repoName}' (no changes)\n`)
377
+ continue
378
+ }
379
+
380
+ console.log(`=> Bumping version for '${repoName}' ...\n`)
381
+
382
+ const ctx = {
383
+ repoName,
384
+ repoConfig,
385
+ repoSourceDir,
386
+ options: { isDryRun, shouldBumpVersions, rc, release, bump, git, pkg, publishFilter, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, dangerouslySquashToCommit, branch: repoEffectiveBranches.get(repoName), yesSignoff },
387
+ metadata: {} as Record<string, any>,
388
+ bumpedRepos,
389
+ alwaysIgnore: repositoriesConfig.alwaysIgnore || [],
390
+ publishingApi,
391
+ }
392
+ await callProvidersForRepo('bump', repoName, repoConfig, repoSourceDir, ctx)
393
+
394
+ if (ctx.metadata.bumped) {
395
+ bumpedRepos.add(repoName)
396
+ }
397
+ }
398
+
399
+ console.log('[t44] Version bump complete!\n')
400
+ }
401
+
402
+ // ══════════════════════════════════════════════════════
403
+ // INTERNAL: Apply renames and resolve workspace deps
404
+ // (cross-repo operation — not a per-provider lifecycle step)
405
+ // ══════════════════════════════════════════════════════
406
+ const matchingDirs = new Map(
407
+ Object.keys(matchingRepositories)
408
+ .filter(name => stageSourceDirs.has(name))
409
+ .map(name => [name, stageSourceDirs.get(name)!])
410
+ )
411
+ const renameProviders = resolveRepoProviders(
412
+ Object.values(matchingRepositories)[0] || {},
413
+ globalProviders
414
+ )
415
+ for (const providerConfig of renameProviders) {
416
+ const provider = await getProvider(providerConfig.capsule)
417
+ if (typeof provider.rename === 'function') {
418
+ await provider.rename({
419
+ dirs: matchingDirs.values(),
420
+ repos: Object.fromEntries(matchingDirs)
421
+ })
422
+ }
423
+ }
424
+
425
+ // ══════════════════════════════════════════════════════
426
+ // INTERNAL: Commit bumped versions to stage repos
427
+ // ══════════════════════════════════════════════════════
428
+ if (shouldBumpVersions && !bump) {
429
+ for (const [repoName] of Object.entries(matchingRepositories)) {
430
+ const repoSourceDir = stageSourceDirs.get(repoName)!
431
+ await this.ProjectRepository.commit({ rootDir: repoSourceDir, message: 'bump' })
432
+ }
433
+ }
434
+
435
+ // ══════════════════════════════════════════════════════
436
+ // Build per-repo publishing contexts
437
+ // ══════════════════════════════════════════════════════
438
+ const repoContexts = new Map<string, any>()
439
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
440
+ repoContexts.set(repoName, {
441
+ repoName,
442
+ repoConfig,
443
+ repoSourceDir: stageSourceDirs.get(repoName)!,
444
+ options: { isDryRun, shouldBumpVersions, rc, release, bump, git, pkg, publishFilter, dangerouslyResetMain, dangerouslyResetGordianOpenIntegrity, dangerouslySquashToCommit, branch: repoEffectiveBranches.get(repoName), yesSignoff },
445
+ metadata: {} as Record<string, any>,
446
+ bumpedRepos,
447
+ alwaysIgnore: repositoriesConfig.alwaysIgnore || [],
448
+ publishingApi,
449
+ })
450
+ }
451
+
452
+ // ══════════════════════════════════════════════════════
453
+ // STEP 4: ensureRemote — ensure remote targets exist
454
+ // (e.g. create GitHub repos before git-scm tries to clone)
455
+ // ══════════════════════════════════════════════════════
456
+ console.log('[t44] Ensuring remote targets ...\n')
457
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
458
+ const ctx = repoContexts.get(repoName)!
459
+ await callProvidersForRepo('ensureRemote', repoName, repoConfig, ctx.repoSourceDir, ctx)
460
+ }
461
+
462
+ // ══════════════════════════════════════════════════════
463
+ // STEP 5: prepare — set up projection/stage dirs
464
+ // ══════════════════════════════════════════════════════
465
+ console.log('[t44] Preparing providers ...\n')
466
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
467
+ const ctx = repoContexts.get(repoName)!
468
+ await callProvidersForRepo('prepare', repoName, repoConfig, ctx.repoSourceDir, ctx)
469
+ }
470
+
471
+ // ══════════════════════════════════════════════════════
472
+ // STEP 6: tag — tag repos with version
473
+ // ══════════════════════════════════════════════════════
474
+ if ((rc || release) && !isDryRun && !publishFilter) {
475
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
476
+ if (!bumpedRepos.has(repoName)) {
477
+ console.log(` ○ Skipping tag for '${repoName}' (not bumped)\n`)
478
+ continue
479
+ }
480
+ const ctx = repoContexts.get(repoName)!
481
+ await callProvidersForRepo('tag', repoName, repoConfig, ctx.repoSourceDir, ctx)
482
+ }
483
+ }
484
+
485
+ // ══════════════════════════════════════════════════════
486
+ // INTERNAL: Sync to project rack registry
487
+ // ══════════════════════════════════════════════════════
488
+ const rackName = await this.ProjectRack.getRackName()
489
+ if (rackName) {
490
+ const registryRootDir = await this.HomeRegistry.rootDir
491
+ const rackStructDir = '@stream44.studio/t44/structs/ProjectRack'.replace(/\//g, '~')
492
+ const rackCapsuleDir = '@stream44.studio/t44/caps/ProjectRepository'.replace(/\//g, '~')
493
+ const workspaceConfig = await this.$WorkspaceConfig.config
494
+ const workspaceRootDir = workspaceConfig?.rootDir
495
+ const projects = await this.WorkspaceProjects.list
496
+
497
+ const matchingProjectNames = new Set<string>()
498
+ if (workspaceRootDir) {
499
+ const { resolve, relative } = await import('path')
500
+ for (const [, repoConfig] of Object.entries(matchingRepositories)) {
501
+ const typedConfig = repoConfig as any
502
+ if (typedConfig.sourceDir) {
503
+ const resolvedSourceDir = resolve(typedConfig.sourceDir)
504
+ const relPath = relative(workspaceRootDir, resolvedSourceDir)
505
+ const topDir = relPath.split('/')[0]
506
+ matchingProjectNames.add(topDir)
507
+ }
508
+ }
509
+ }
510
+
511
+ console.log(`[t44] Syncing project repos to project rack '${rackName}' ...\n`)
512
+
513
+ for (const [projectName, projectData] of Object.entries(projects)) {
514
+ if (matchingProjectNames.size > 0 && !matchingProjectNames.has(projectName)) {
515
+ continue
516
+ }
517
+
518
+ const project = projectData as any
519
+ const projectDid = project.identifier?.did
520
+ if (!projectDid) {
521
+ console.log(` ○ Skipping '${projectName}' (no project identifier)`)
522
+ continue
523
+ }
524
+
525
+ const projectSourceDir = project.sourceDir
526
+ const rackRepoDir = join(registryRootDir, rackStructDir, rackName, rackCapsuleDir, projectDid)
527
+ try {
528
+ await this.ProjectRepository.initBare({ rootDir: rackRepoDir })
529
+
530
+ const remoteName = '@stream44.studio/t44/caps/ProjectRack'
531
+ const hasRemote = await this.ProjectRepository.hasRemote({ rootDir: projectSourceDir, name: remoteName })
532
+ if (!hasRemote) {
533
+ await this.ProjectRepository.addRemote({ rootDir: projectSourceDir, name: remoteName, url: rackRepoDir })
534
+ } else {
535
+ await this.ProjectRepository.setRemoteUrl({ rootDir: projectSourceDir, name: remoteName, url: rackRepoDir })
536
+ }
537
+
538
+ const branch = await this.ProjectRepository.getBranch({ rootDir: projectSourceDir })
539
+ await this.ProjectRepository.pushToRemote({ rootDir: projectSourceDir, remote: remoteName, branch, force: true })
540
+
541
+ console.log(` ✓ Synced '${projectName}' to rack`)
542
+ } catch (error: any) {
543
+ console.log(chalk.red(`\n ✗ Failed to sync '${projectName}' to project rack '${rackName}'`))
544
+ console.log(chalk.red(` ${error.message || error}`))
545
+ console.log(chalk.red(`[t44] ABORT: Rack sync failed. Not pushing to external providers.\n`))
546
+ return
547
+ }
548
+ }
549
+
550
+ console.log(`[t44] Rack sync complete.\n`)
551
+ }
552
+
553
+ // ══════════════════════════════════════════════════════
554
+ // STEP 7: push — publish/push to providers
555
+ // ══════════════════════════════════════════════════════
556
+ if (isDryRun) {
557
+ console.log('[t44] DRY-RUN: Skipping publishing (would publish packages here)\n')
558
+
559
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
560
+ console.log(`\n=> Processing repository '${repoName}' ...\n`)
561
+ const providers = resolveRepoProviders(repoConfig as any, globalProviders)
562
+ for (const providerConfig of providers) {
563
+ if (!await isProviderIncluded(providerConfig)) continue
564
+ console.log(` -> DRY-RUN: Skipping provider '${providerConfig.capsule}'\n`)
565
+ }
566
+ }
567
+ } else {
568
+ console.log('[t44] Publishing packages ...\n')
569
+
570
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
571
+ console.log(`\n=> Processing repository '${repoName}' ...\n`)
572
+ const ctx = repoContexts.get(repoName)!
573
+ const providers = resolveRepoProviders(repoConfig as any, globalProviders)
574
+
575
+ for (const providerConfig of providers) {
576
+ if (!(await isProviderIncluded(providerConfig))) continue
577
+
578
+ const capsuleName = providerConfig.capsule
579
+ const provider = await getProvider(capsuleName)
580
+ if (typeof provider.push !== 'function') continue
581
+
582
+ console.log(` -> Running provider '${capsuleName}' ...\n`)
583
+ await provider.push({ config: providerConfig, ctx })
584
+ console.log(` <- Provider '${capsuleName}' complete.\n`)
585
+ }
586
+
587
+ console.log(`<= Repository '${repoName}' processing complete.\n`)
588
+ }
589
+ }
590
+
591
+ // ══════════════════════════════════════════════════════
592
+ // INTERNAL: Persist activePublishingBranch to config
593
+ // ══════════════════════════════════════════════════════
594
+ if (!isDryRun) {
595
+ for (const [repoName] of Object.entries(matchingRepositories)) {
596
+ const effectiveBranch = repoEffectiveBranches.get(repoName)
597
+ if (branch && effectiveBranch) {
598
+ // --branch was explicitly used: persist to config
599
+ await this.$WorkspaceRepositories.setConfigValue(
600
+ ['repositories', repoName, 'activePublishingBranch'], effectiveBranch
601
+ )
602
+ console.log(`[t44] Stored activePublishingBranch '${effectiveBranch}' for '${repoName}'\n`)
603
+ }
604
+ }
605
+ }
606
+
607
+ // ══════════════════════════════════════════════════════
608
+ // STEP 8: afterPush — update catalogs
609
+ // ══════════════════════════════════════════════════════
610
+ if (!isDryRun) {
611
+ for (const [repoName, repoConfig] of Object.entries(matchingRepositories)) {
612
+ const ctx = repoContexts.get(repoName)!
613
+
614
+ // Update base catalog entry once per repo
615
+ const repoSourceDir_ = resolve((repoConfig as any).sourceDir)
616
+ const workspaceProjectName = await this.WorkspaceProjects.findProjectForPath({ targetPath: repoSourceDir_ }) || ''
617
+ await this.ProjectCatalogs.updateCatalogRepository({
618
+ repoName,
619
+ providerKey: '#' + capsule['#'],
620
+ providerData: {
621
+ sourceDir: repoSourceDir_,
622
+ workspaceProjectName,
623
+ },
624
+ })
625
+
626
+ // Call afterPush on all providers
627
+ await callProvidersForRepo('afterPush', repoName, repoConfig, ctx.repoSourceDir, ctx)
628
+ }
629
+ }
630
+
631
+ // ══════════════════════════════════════════════════════
632
+ // Done
633
+ // ══════════════════════════════════════════════════════
634
+ if (isDryRun) {
635
+ console.log('[t44] DRY-RUN complete! No irreversible operations were performed.')
636
+ console.log('[t44] To actually publish, use: t44 push --rc (for release candidate) or t44 push --release')
637
+ console.log('[t44] To bump versions only: t44 push --bump')
638
+ } else if (bump) {
639
+ console.log('[t44] Version bump complete! Versions updated in package.json files.')
640
+ console.log('[t44] To tag and publish, use: t44 push --rc or t44 push --release')
641
+ } else {
642
+ console.log('[t44] Project repositories pushed OK!')
643
+ }
644
+ }
645
+ }
646
+ }
647
+ }
648
+ }, {
649
+ importMeta: import.meta,
650
+ importStack: makeImportStack(),
651
+ capsuleName: capsule['#'],
652
+ })
653
+ }
654
+ capsule['#'] = '@stream44.studio/t44/caps/ProjectPublishing'