@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,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'
|