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,654 @@
1
+
2
+ import { join } from 'path'
3
+ import { mkdir, access, readFile, writeFile, copyFile, rm, cp } from 'fs/promises'
4
+ import { constants } from 'fs'
5
+ import { $ } from 'bun'
6
+ import chalk from 'chalk'
7
+
8
+ const OI_REGISTRY_CAPSULE = '@t44.sh~t44~caps~providers~blockchaincommons.com~GordianOpenIntegrity'
9
+ const GENERATOR_FILE = '.git/o/GordianOpenIntegrity-generator.yaml'
10
+
11
+ export async function capsule({
12
+ encapsulate,
13
+ CapsulePropertyTypes,
14
+ makeImportStack
15
+ }: {
16
+ encapsulate: any
17
+ CapsulePropertyTypes: any
18
+ makeImportStack: any
19
+ }) {
20
+ // High level API that deals with everything concerning a git repository.
21
+ return encapsulate({
22
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
23
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
24
+ '#t44/structs/providers/git-scm.com/ProjectPublishingFact': {
25
+ as: '$GitFact'
26
+ },
27
+ '#t44/structs/ProjectPublishingFact': {
28
+ as: '$StatusFact'
29
+ },
30
+ '#': {
31
+ WorkspacePrompt: {
32
+ type: CapsulePropertyTypes.Mapping,
33
+ value: 't44/caps/WorkspacePrompt'
34
+ },
35
+ ProjectRepository: {
36
+ type: CapsulePropertyTypes.Mapping,
37
+ value: 't44/caps/ProjectRepository'
38
+ },
39
+ GordianOpenIntegrity: {
40
+ type: CapsulePropertyTypes.Mapping,
41
+ value: '@stream44.studio/t44-blockchaincommons.com/caps/GordianOpenIntegrity'
42
+ },
43
+ HomeRegistry: {
44
+ type: CapsulePropertyTypes.Mapping,
45
+ value: 't44/caps/HomeRegistry'
46
+ },
47
+ ProjectCatalogs: {
48
+ type: CapsulePropertyTypes.Mapping,
49
+ value: 't44/caps/ProjectCatalogs'
50
+ },
51
+ Dco: {
52
+ type: CapsulePropertyTypes.Mapping,
53
+ value: '@stream44.studio/dco/caps/Dco'
54
+ },
55
+ SigningKey: {
56
+ type: CapsulePropertyTypes.Mapping,
57
+ value: 't44/caps/SigningKey'
58
+ },
59
+ prepare: {
60
+ type: CapsulePropertyTypes.Function,
61
+ value: async function (this: any, { projectionDir, config }: { projectionDir: string, config: any }) {
62
+
63
+ const originUri = config.provider.config.RepositorySettings.origin
64
+
65
+ console.log(`Preparing git repo '${originUri}' from source '${config.sourceDir}' ...`)
66
+
67
+ const projectSourceDir = join(config.sourceDir)
68
+ const stageDir = join(projectionDir, 'stage', originUri.replace(/[\/]/g, '~'))
69
+
70
+ // Clone if repository doesn't exist yet
71
+ let isNewEmptyRepo = false
72
+ const repoExists = await this.ProjectRepository.exists({ rootDir: stageDir })
73
+ if (!repoExists) {
74
+ console.log(`Cloning repository from '${originUri}' ...`)
75
+ const result = await this.ProjectRepository.clone({ originUri, targetDir: stageDir })
76
+ isNewEmptyRepo = result.isNewEmptyRepo
77
+ }
78
+
79
+ // Set local git author from RepositorySettings config
80
+ const authorConfig = config.provider?.config?.RepositorySettings?.author
81
+ if (authorConfig?.name) {
82
+ await $`git config user.name ${authorConfig.name}`.cwd(stageDir).quiet()
83
+ }
84
+ if (authorConfig?.email) {
85
+ await $`git config user.email ${authorConfig.email}`.cwd(stageDir).quiet()
86
+ }
87
+
88
+ // Sync files using rsync with gitignore support and delete removed files
89
+ const gitignorePath = join(projectSourceDir, '.gitignore')
90
+ await this.ProjectRepository.sync({
91
+ rootDir: stageDir,
92
+ sourceDir: projectSourceDir,
93
+ gitignorePath
94
+ })
95
+
96
+ // Generate files from config properties starting with '/'
97
+ // This happens AFTER rsync so generated files are not overwritten
98
+ if (config.provider.config) {
99
+ for (const [key, value] of Object.entries(config.provider.config)) {
100
+ if (key.startsWith('/')) {
101
+ const targetPath = join(stageDir, key)
102
+ const targetDir = join(targetPath, '..')
103
+
104
+ // Check if file already exists
105
+ let fileExists = false
106
+ try {
107
+ await access(targetPath, constants.F_OK)
108
+ fileExists = true
109
+ } catch {
110
+ fileExists = false
111
+ }
112
+
113
+ if (fileExists) {
114
+ console.log(`Overwriting file '${key}' in repository ...`)
115
+ } else {
116
+ console.log(`Creating file '${key}' in repository ...`)
117
+ }
118
+
119
+ // Ensure directory exists
120
+ await mkdir(targetDir, { recursive: true })
121
+
122
+ // Write file content
123
+ const content = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
124
+ await writeFile(targetPath, content, 'utf-8')
125
+ }
126
+ }
127
+ }
128
+
129
+ return {
130
+ originUri,
131
+ projectSourceDir,
132
+ stageDir,
133
+ isNewEmptyRepo
134
+ }
135
+ }
136
+ },
137
+ tag: {
138
+ type: CapsulePropertyTypes.Function,
139
+ value: async function (this: any, { metadata, repoSourceDir }: { metadata: any, repoSourceDir: string }) {
140
+
141
+ const { stageDir } = metadata
142
+
143
+ const packageJsonPath = join(repoSourceDir, 'package.json')
144
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
145
+ const packageJson = JSON.parse(packageJsonContent)
146
+ const version = packageJson.version
147
+ const tag = `v${version}`
148
+
149
+ const headCommit = await this.ProjectRepository.getHeadCommit({ rootDir: stageDir })
150
+
151
+ if (!headCommit) {
152
+ console.log(chalk.gray(` ○ Empty repository, skipping tag (will tag after first commit)\n`))
153
+ return
154
+ }
155
+
156
+ // Check if tag already exists locally
157
+ const localTag = await this.ProjectRepository.hasTag({ rootDir: stageDir, tag })
158
+ if (localTag.exists) {
159
+ if (localTag.commit === headCommit) {
160
+ console.log(chalk.gray(` ○ Tag ${tag} already exists at current commit, skipping\n`))
161
+ return
162
+ }
163
+ console.log(chalk.yellow(`\n Tag ${tag} exists at ${localTag.commit!.slice(0, 8)} but HEAD is ${headCommit.slice(0, 8)}\n`))
164
+ const diffText = await this.ProjectRepository.diff({ rootDir: stageDir, from: tag })
165
+ if (diffText.length > 0) {
166
+ console.log(diffText)
167
+ }
168
+ throw new Error(
169
+ `Git tag '${tag}' already exists but points to a different commit.\n` +
170
+ ` Please bump to a different version before pushing.`
171
+ )
172
+ }
173
+
174
+ // Check if tag already exists on remote
175
+ const remoteTag = await this.ProjectRepository.hasRemoteTag({ rootDir: stageDir, tag })
176
+ if (remoteTag.exists) {
177
+ if (remoteTag.commit === headCommit) {
178
+ console.log(chalk.gray(` ○ Tag ${tag} already exists on remote at current commit, skipping\n`))
179
+ return
180
+ }
181
+ console.log(chalk.yellow(`\n Tag ${tag} exists on remote at ${remoteTag.commit!.slice(0, 8)} but HEAD is ${headCommit.slice(0, 8)}\n`))
182
+ const diffText = await this.ProjectRepository.diff({ rootDir: stageDir, from: remoteTag.commit! })
183
+ if (diffText.length > 0) {
184
+ console.log(diffText)
185
+ }
186
+ throw new Error(
187
+ `Git tag '${tag}' already exists on remote but points to a different commit.\n` +
188
+ ` Please bump to a different version before pushing.`
189
+ )
190
+ }
191
+
192
+ await this.ProjectRepository.tag({ rootDir: stageDir, tag })
193
+ console.log(chalk.green(` ✓ Tagged with ${tag}\n`))
194
+ }
195
+ },
196
+ push: {
197
+ type: CapsulePropertyTypes.Function,
198
+ value: async function (this: any, { config, dangerouslyResetMain, yesSignoff, metadata, projectSourceDir }: { config: any, dangerouslyResetMain?: boolean, yesSignoff?: boolean, metadata: any, projectSourceDir?: string }) {
199
+
200
+ const {
201
+ originUri,
202
+ stageDir,
203
+ isNewEmptyRepo
204
+ } = metadata
205
+
206
+ // Check if GordianOpenIntegrity is enabled for this provider
207
+ const oiConfig = config.provider?.config?.['#@stream44.studio/t44-blockchaincommons.com']
208
+ const oiEnabled = oiConfig?.GordianOpenIntegrity === true
209
+
210
+ if (dangerouslyResetMain) {
211
+ if (oiEnabled) {
212
+ console.log(`Reset mode enabled with GordianOpenIntegrity - will create a fresh open integrity repo`)
213
+ } else {
214
+ console.log(`Reset mode enabled - will reset repository to initial commit`)
215
+ }
216
+ }
217
+
218
+ // Git add and check for changes
219
+ console.log(`Committing changes ...`)
220
+ const hasNewChanges = await this.ProjectRepository.addAll({ rootDir: stageDir })
221
+
222
+ // Handle reset (works on existing commits, regardless of new changes)
223
+ let shouldReset = false
224
+ if (dangerouslyResetMain) {
225
+
226
+ // Check if the repo already has commits
227
+ const headCommit = await this.ProjectRepository.getHeadCommit({ rootDir: stageDir })
228
+ const hasExistingCommits = !!headCommit
229
+
230
+ if (hasExistingCommits) {
231
+ // Repo has commits — warn user and require confirmation
232
+ const descriptionLines = oiEnabled
233
+ ? [
234
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
235
+ 'This will create a fresh GordianOpenIntegrity repository.',
236
+ '',
237
+ 'What this means:',
238
+ ' • The existing local stage repo will be deleted entirely',
239
+ ' • A new repo will be created with a cryptographic inception commit',
240
+ ' • An XID identity and SSH signing key will be generated',
241
+ ' • All source files will be added as a signed commit',
242
+ ' • The new repo will be force pushed, destroying all remote history',
243
+ ' • All existing commit history, tags, and signatures will be lost',
244
+ '',
245
+ 'This cannot be undone once pushed to remote.',
246
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
247
+ ]
248
+ : [
249
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
250
+ 'Resetting will:',
251
+ ' • Destroy all commit history in the local repository',
252
+ ' • Destroy all commit history on GitHub when force pushed',
253
+ ' • Cannot be undone once pushed to remote',
254
+ '',
255
+ 'This should ONLY be done at the very beginning of a project.',
256
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
257
+ ]
258
+
259
+ shouldReset = await this.WorkspacePrompt.confirm({
260
+ title: '⚠️ WARNING: DESTRUCTIVE OPERATION ⚠️',
261
+ description: descriptionLines,
262
+ message: oiEnabled
263
+ ? 'Are you absolutely sure you want to recreate this as a GordianOpenIntegrity repo?'
264
+ : 'Are you absolutely sure you want to reset all commits and destroy the history?',
265
+ defaultValue: false,
266
+ onSuccess: async (confirmed: boolean) => {
267
+ if (confirmed) {
268
+ const chalk = (await import('chalk')).default
269
+ if (oiEnabled) {
270
+ console.log(chalk.cyan(`\nCreating fresh GordianOpenIntegrity repository ...`))
271
+ } else {
272
+ console.log(chalk.cyan(`\nResetting all commits to initial commit ...`))
273
+ }
274
+ } else {
275
+ console.log('\nReset operation cancelled. Pushing without resetting...\n')
276
+ }
277
+ }
278
+ })
279
+ } else {
280
+ // No existing commits — safe to proceed without confirmation
281
+ shouldReset = true
282
+ }
283
+
284
+ if (shouldReset && oiEnabled) {
285
+ // GordianOpenIntegrity reset: create a fresh OI repo
286
+
287
+ // Get author info from workspace.yaml config
288
+ const authorConfig = config.provider?.config?.RepositorySettings?.author
289
+ if (!authorConfig?.name || !authorConfig?.email) {
290
+ throw new Error('GordianOpenIntegrity requires author.name and author.email in RepositorySettings config')
291
+ }
292
+ const authorName = authorConfig.name
293
+ const authorEmail = authorConfig.email
294
+
295
+ // Resolve the workspace signing key
296
+ const signingKeyPath = await this.SigningKey.getKeyPath()
297
+ const signingPublicKey = await this.SigningKey.getPublicKey()
298
+ const signingFingerprint = await this.SigningKey.getFingerprint()
299
+ const signingKeyName = await this.SigningKey.getKeyName()
300
+ if (!signingKeyPath || !signingPublicKey || !signingFingerprint) {
301
+ throw new Error('Signing key not configured. Run SigningKey.ensureKey() first.')
302
+ }
303
+ console.log(chalk.gray(` Signing key: ${signingKeyName} (${signingKeyPath})`))
304
+
305
+ // Delete existing OI registry data for the previous repo DID
306
+ const registryRootDir_ = await this.HomeRegistry.rootDir
307
+ try {
308
+ const existingOiYaml = await readFile(join(stageDir, '.o', 'GordianOpenIntegrity.yaml'), 'utf-8')
309
+ const existingDidMatch = existingOiYaml.match(/^#\s*Repository DID:\s*(.+)$/m)
310
+ if (existingDidMatch) {
311
+ const existingDid = existingDidMatch[1].trim()
312
+ const existingOiRegistryDir = join(registryRootDir_, OI_REGISTRY_CAPSULE, existingDid)
313
+ try {
314
+ await access(existingOiRegistryDir, constants.F_OK)
315
+ console.log(`Removing existing OI registry for ${existingDid} ...`)
316
+ await rm(existingOiRegistryDir, { recursive: true, force: true })
317
+ } catch { }
318
+ }
319
+ } catch { }
320
+
321
+ // Delete existing stage dir to start completely fresh
322
+ console.log(`Removing existing stage directory ...`)
323
+ await rm(stageDir, { recursive: true, force: true })
324
+
325
+ // Remove stale DCO signatures from source dirs (fresh repo = fresh DCO)
326
+ for (const dir of [config.sourceDir, projectSourceDir].filter(Boolean)) {
327
+ await rm(join(dir!, '.dco-signatures'), { force: true })
328
+ }
329
+
330
+ // Create OI identity using the workspace signing key
331
+ console.log(`Creating GordianOpenIntegrity identity ...`)
332
+ console.log(chalk.gray(` Author: ${authorName} <${authorEmail}>`))
333
+ const author = await this.GordianOpenIntegrity.createIdentity({
334
+ key: {
335
+ privateKeyPath: signingKeyPath,
336
+ publicKeyPath: `${signingKeyPath}.pub`,
337
+ publicKey: signingPublicKey,
338
+ fingerprint: signingFingerprint,
339
+ },
340
+ authorName,
341
+ authorEmail,
342
+ })
343
+
344
+ // Create OI inception repo at stageDir
345
+ console.log(`Creating GordianOpenIntegrity inception repository ...`)
346
+ const repoResult = await this.GordianOpenIntegrity.createRepository({
347
+ repoDir: stageDir,
348
+ author,
349
+ })
350
+ console.log(chalk.green(` ✓ Inception commit: ${repoResult.commitHash.slice(0, 8)}`))
351
+ console.log(chalk.green(` ✓ DID: ${repoResult.did}`))
352
+
353
+ // Set local git author on the fresh repo (needed for DCO signing)
354
+ await $`git config user.name ${authorName}`.cwd(stageDir).quiet()
355
+ await $`git config user.email ${authorEmail}`.cwd(stageDir).quiet()
356
+
357
+ // Store generator and metadata in the registry
358
+ const registryRootDir = await this.HomeRegistry.rootDir
359
+ const oiRegistryDir = join(registryRootDir, OI_REGISTRY_CAPSULE, repoResult.did)
360
+ await mkdir(oiRegistryDir, { recursive: true })
361
+
362
+ console.log(chalk.green(` ✓ Using workspace signing key: ${signingKeyName} (${signingFingerprint})`))
363
+ console.log(chalk.green(` ${signingKeyPath}`))
364
+
365
+ // Copy the generator file from the repo's .git dir to the registry
366
+ const repoGeneratorPath = join(stageDir, GENERATOR_FILE)
367
+ const registryGeneratorPath = join(oiRegistryDir, 'GordianOpenIntegrity-generator.yaml')
368
+ await cp(repoGeneratorPath, registryGeneratorPath)
369
+ console.log(chalk.green(` ✓ Generator stored at: ${registryGeneratorPath}`))
370
+
371
+ // Write repo.json metadata to the registry
372
+ const repoMeta: Record<string, any> = {
373
+ did: repoResult.did,
374
+ firstCommit: repoResult.commitHash,
375
+ firstCommitDate: new Date().toISOString(),
376
+ origin: originUri,
377
+ }
378
+ // Try to read packageName from source package.json
379
+ try {
380
+ const pkgPath = join(config.sourceDir, 'package.json')
381
+ const pkgContent = await readFile(pkgPath, 'utf-8')
382
+ const pkg = JSON.parse(pkgContent)
383
+ if (pkg.name) {
384
+ repoMeta.packageName = pkg.name
385
+ }
386
+ } catch { }
387
+ await writeFile(join(oiRegistryDir, 'repo.json'), JSON.stringify(repoMeta, null, 2), 'utf-8')
388
+ console.log(chalk.green(` ✓ Registry metadata stored at: ${join(oiRegistryDir, 'repo.json')}`))
389
+
390
+ // Copy .o/GordianOpenIntegrity.yaml to both the actual project source dir
391
+ // AND the ProjectRepository stage dir (config.sourceDir) so that
392
+ // rsync preserves it in the OI stage repo on this and future syncs
393
+ const stageInceptionPath = join(stageDir, '.o', 'GordianOpenIntegrity.yaml')
394
+ const projectSourcePath = join(config.sourceDir)
395
+
396
+ // Copy to ProjectRepository stage dir (used as rsync source)
397
+ const prStageInceptionDir = join(projectSourcePath, '.o')
398
+ await mkdir(prStageInceptionDir, { recursive: true })
399
+ await copyFile(stageInceptionPath, join(prStageInceptionDir, 'GordianOpenIntegrity.yaml'))
400
+
401
+ // Copy lifehash SVGs to ProjectRepository stage dir
402
+ const stageODir = join(stageDir, '.o')
403
+ for (const lifehashFile of ['GordianOpenIntegrity-InceptionLifehash.svg', 'GordianOpenIntegrity-CurrentLifehash.svg']) {
404
+ await copyFile(join(stageODir, lifehashFile), join(prStageInceptionDir, lifehashFile))
405
+ }
406
+
407
+ // Copy to actual project source dir (persists across runs)
408
+ if (projectSourceDir) {
409
+ const sourceInceptionDir = join(projectSourceDir, '.o')
410
+ await mkdir(sourceInceptionDir, { recursive: true })
411
+ await copyFile(stageInceptionPath, join(sourceInceptionDir, 'GordianOpenIntegrity.yaml'))
412
+ for (const lifehashFile of ['GordianOpenIntegrity-InceptionLifehash.svg', 'GordianOpenIntegrity-CurrentLifehash.svg']) {
413
+ await copyFile(join(stageODir, lifehashFile), join(sourceInceptionDir, lifehashFile))
414
+ }
415
+ }
416
+ console.log(chalk.green(` ✓ Copied .o/GordianOpenIntegrity.yaml and lifehash images to source directories`))
417
+
418
+ // Update Repository DID in README.md files if present
419
+ const DID_PATTERN = /^(Repository DID: `)([^`]*)(`)$/m
420
+ for (const dir of [projectSourcePath, projectSourceDir].filter(Boolean)) {
421
+ const readmePath = join(dir!, 'README.md')
422
+ try {
423
+ const readmeContent = await readFile(readmePath, 'utf-8')
424
+ if (DID_PATTERN.test(readmeContent)) {
425
+ const updated = readmeContent.replace(DID_PATTERN, `$1${repoResult.did}$3`)
426
+ await writeFile(readmePath, updated, 'utf-8')
427
+ }
428
+ } catch { }
429
+ }
430
+
431
+ // Sync source files into the OI repo
432
+ console.log(`Syncing source files ...`)
433
+ const gitignorePath = join(projectSourcePath, '.gitignore')
434
+ await this.ProjectRepository.sync({
435
+ rootDir: stageDir,
436
+ sourceDir: projectSourcePath,
437
+ gitignorePath
438
+ })
439
+
440
+ // Generate files from config properties starting with '/'
441
+ if (config.provider.config) {
442
+ for (const [key, value] of Object.entries(config.provider.config)) {
443
+ if (key.startsWith('/')) {
444
+ const targetPath = join(stageDir, key)
445
+ const targetDir = join(targetPath, '..')
446
+ await mkdir(targetDir, { recursive: true })
447
+ const content = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
448
+ await writeFile(targetPath, content, 'utf-8')
449
+ }
450
+ }
451
+ }
452
+
453
+ // Run DCO signing process if DCO.md is present
454
+ const hasDco = await this.Dco.hasDco({ repoDir: stageDir })
455
+ if (hasDco) {
456
+ console.log(chalk.cyan(`DCO.md detected — running DCO signing process ...`))
457
+ await this.Dco.sign({ repoDir: stageDir, autoAgree: yesSignoff, signingKeyPath: author.sshKey.privateKeyPath })
458
+ }
459
+
460
+ // Stage all files and commit as a signed commit
461
+ console.log(`Committing source content as signed commit ...`)
462
+ await $`git add -A`.cwd(stageDir).quiet()
463
+ await this.GordianOpenIntegrity.commitToRepository({
464
+ repoDir: stageDir,
465
+ author,
466
+ message: 'Published using @Stream44 Studio',
467
+ })
468
+ console.log(chalk.green(` ✓ Source content committed`))
469
+
470
+ // Copy .dco-signatures back to project source so it persists
471
+ if (hasDco && projectSourceDir) {
472
+ const stageSigFile = join(stageDir, '.dco-signatures')
473
+ try {
474
+ await access(stageSigFile, constants.F_OK)
475
+ await copyFile(stageSigFile, join(projectSourceDir, '.dco-signatures'))
476
+ } catch { }
477
+ }
478
+
479
+ // Add remote origin and force push
480
+ const hasRemote = await this.ProjectRepository.hasRemote({ rootDir: stageDir, name: 'origin' })
481
+ if (!hasRemote) {
482
+ await this.ProjectRepository.addRemote({ rootDir: stageDir, name: 'origin', url: originUri })
483
+ }
484
+
485
+ console.log(`Force pushing to remote ...`)
486
+ await $`git push --force -u origin main --tags`.cwd(stageDir)
487
+ console.log(chalk.green(` ✓ Force pushed to remote`))
488
+
489
+ // Write fact files
490
+ const lastCommit = await this.ProjectRepository.getHeadCommit({ rootDir: stageDir })
491
+ const lastCommitMessage = await this.ProjectRepository.getLastCommitMessage({ rootDir: stageDir })
492
+ const branch = await this.ProjectRepository.getBranch({ rootDir: stageDir })
493
+
494
+ const repoFactName = originUri.replace(/[\/]/g, '~')
495
+
496
+ await this.$GitFact.set(repoFactName, {
497
+ origin: originUri,
498
+ branch: branch,
499
+ lastCommit: lastCommit,
500
+ lastCommitMessage: lastCommitMessage,
501
+ pushedAt: new Date().toISOString()
502
+ })
503
+
504
+ await this.$StatusFact.set(repoFactName, {
505
+ projectName: originUri,
506
+ provider: 'git-scm.com',
507
+ status: 'PUBLISHED',
508
+ publicUrl: originUri
509
+ })
510
+
511
+ return
512
+ } else if (shouldReset) {
513
+ await this.ProjectRepository.squashAllCommits({
514
+ rootDir: stageDir,
515
+ message: 'Published using @Stream44 Studio'
516
+ })
517
+ console.log(`Repository reset to initial commit`)
518
+ }
519
+ } else if (hasNewChanges) {
520
+ // Check if DCO.md exists in the stage dir
521
+ const hasDco = await this.Dco.hasDco({ repoDir: stageDir })
522
+
523
+ if (hasDco) {
524
+ console.log(chalk.cyan(`DCO.md detected — running DCO signing process ...`))
525
+
526
+ // Resolve signing key from workspace SigningKey capsule
527
+ let signingKeyPath: string | undefined
528
+ const skPath = await this.SigningKey.getKeyPath()
529
+ if (skPath) {
530
+ signingKeyPath = skPath
531
+ }
532
+
533
+ await this.Dco.signAndCommit({
534
+ repoDir: stageDir,
535
+ message: 'Published using @Stream44 Studio',
536
+ autoAgree: yesSignoff,
537
+ signingKeyPath,
538
+ projectSourceDir,
539
+ })
540
+ } else {
541
+ await this.ProjectRepository.commit({
542
+ rootDir: stageDir,
543
+ message: 'Published using @Stream44 Studio'
544
+ })
545
+ }
546
+ console.log(`New changes committed`)
547
+ } else {
548
+ console.log(`No new changes to commit`)
549
+ }
550
+
551
+ // Check if local is ahead of remote
552
+ let localAheadOfRemote = false
553
+ if (!shouldReset && !hasNewChanges && !isNewEmptyRepo) {
554
+ localAheadOfRemote = await this.ProjectRepository.isAheadOfRemote({ rootDir: stageDir })
555
+ }
556
+
557
+ // Push to remote
558
+ if (shouldReset) {
559
+ console.log(`Force pushing to remote ...`)
560
+ await this.ProjectRepository.forcePush({ rootDir: stageDir })
561
+ console.log(`Force pushed to remote`)
562
+ } else if (isNewEmptyRepo || hasNewChanges || localAheadOfRemote) {
563
+ console.log(`Pushing to remote ...`)
564
+ await this.ProjectRepository.push({ rootDir: stageDir })
565
+ console.log(`Pushed to remote`)
566
+ }
567
+
568
+ // Write fact files
569
+ const lastCommit = await this.ProjectRepository.getHeadCommit({ rootDir: stageDir })
570
+ const lastCommitMessage = await this.ProjectRepository.getLastCommitMessage({ rootDir: stageDir })
571
+ const branch = await this.ProjectRepository.getBranch({ rootDir: stageDir })
572
+
573
+ const repoFactName = originUri.replace(/[\/]/g, '~')
574
+
575
+ await this.$GitFact.set(repoFactName, {
576
+ origin: originUri,
577
+ branch: branch,
578
+ lastCommit: lastCommit,
579
+ lastCommitMessage: lastCommitMessage,
580
+ pushedAt: new Date().toISOString()
581
+ })
582
+
583
+ await this.$StatusFact.set(repoFactName, {
584
+ projectName: originUri,
585
+ provider: 'git-scm.com',
586
+ status: hasNewChanges || shouldReset || localAheadOfRemote ? 'PUBLISHED' : 'READY',
587
+ publicUrl: originUri
588
+ })
589
+
590
+ }
591
+ },
592
+ afterPush: {
593
+ type: CapsulePropertyTypes.Function,
594
+ value: async function (this: any, { repoName, config, metadata }: {
595
+ repoName: string
596
+ config: any
597
+ metadata?: any
598
+ }): Promise<void> {
599
+ if (!metadata) return
600
+
601
+ const branch = await this.ProjectRepository.getBranch({ rootDir: metadata.stageDir })
602
+ const commit = await this.ProjectRepository.getHeadCommit({ rootDir: metadata.stageDir })
603
+
604
+ const gitData: Record<string, any> = {
605
+ branches: {},
606
+ }
607
+ if (branch && commit) {
608
+ const branchEntry: Record<string, any> = { commit }
609
+ try {
610
+ const tagResult = await $`git tag --points-at ${commit}`.cwd(metadata.stageDir).quiet().nothrow()
611
+ const tag = tagResult.text().trim().split('\n').filter(Boolean).pop()
612
+ if (tag) branchEntry.tag = tag
613
+ } catch { }
614
+ gitData.branches[branch] = branchEntry
615
+ }
616
+
617
+ await this.ProjectCatalogs.updateCatalogRepository({
618
+ repoName,
619
+ providerKey: '#' + capsule['#'],
620
+ providerData: gitData,
621
+ })
622
+
623
+ const oiConfig = config.provider?.config?.['#@stream44.studio/t44-blockchaincommons.com']
624
+ if (oiConfig?.GordianOpenIntegrity === true) {
625
+ const oiYamlPath = join(metadata.stageDir, '.o', 'GordianOpenIntegrity.yaml')
626
+ try {
627
+ const oiContent = await readFile(oiYamlPath, 'utf-8')
628
+ const didMatch = oiContent.match(/^#\s*Repository DID:\s*(.+)$/m)
629
+ const currentMarkMatch = oiContent.match(/^#\s*Current Mark:\s*(\S+)/m)
630
+ const inceptionMarkMatch = oiContent.match(/^#\s*Inception Mark:\s*(\S+)/m)
631
+ if (didMatch) {
632
+ await this.ProjectCatalogs.updateCatalogRepository({
633
+ repoName,
634
+ providerKey: '#t44/caps/providers/blockchaincommons.com/GordianOpenIntegrity',
635
+ providerData: {
636
+ did: didMatch[1].trim(),
637
+ inceptionMark: inceptionMarkMatch?.[1] || undefined,
638
+ currentMark: currentMarkMatch?.[1] || undefined,
639
+ },
640
+ })
641
+ }
642
+ } catch { }
643
+ }
644
+ }
645
+ },
646
+ }
647
+ }
648
+ }, {
649
+ importMeta: import.meta,
650
+ importStack: makeImportStack(),
651
+ capsuleName: capsule['#'],
652
+ })
653
+ }
654
+ capsule['#'] = 't44/caps/providers/git-scm.com/ProjectPublishing'