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.
- package/.dco-signatures +9 -0
- package/.github/workflows/dco.yml +12 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +203 -0
- package/README.md +183 -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 +51 -0
- package/caps/HomeRegistry.ts +313 -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 +363 -0
- package/caps/ProjectDevelopment.ts +257 -0
- package/caps/ProjectPublishing.ts +522 -0
- package/caps/ProjectRack.ts +155 -0
- package/caps/ProjectRepository.ts +322 -0
- package/caps/RootKey.ts +219 -0
- package/caps/SigningKey.ts +243 -0
- package/caps/WorkspaceCli.ts +442 -0
- package/caps/WorkspaceConfig.ts +268 -0
- package/caps/WorkspaceConfig.yaml +71 -0
- package/caps/WorkspaceConfigFile.ts +799 -0
- package/caps/WorkspaceConnection.ts +249 -0
- package/caps/WorkspaceEntityConfig.ts +78 -0
- package/caps/WorkspaceEntityConfig.v0.ts +77 -0
- package/caps/WorkspaceEntityFact.ts +218 -0
- package/caps/WorkspaceInfo.ts +595 -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 +406 -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/WorkspaceTest.ts +167 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
- package/caps/providers/bunny.net/api-pull.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.ts +164 -0
- package/caps/providers/bunny.net/api-storage.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.ts +248 -0
- package/caps/providers/bunny.net/api.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
- package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
- package/caps/providers/dynadot.com/api-domains.ts +169 -0
- package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
- package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
- package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
- package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
- package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
- package/caps/providers/github.com/ProjectPublishing.ts +118 -0
- package/caps/providers/github.com/api.ts +115 -0
- package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
- package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
- package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
- package/caps/providers/vercel.com/api.test.ts +67 -0
- package/caps/providers/vercel.com/api.ts +132 -0
- package/caps/providers/vercel.com/bun.lock +194 -0
- package/caps/providers/vercel.com/package.json +10 -0
- package/caps/providers/vercel.com/project.test.ts +108 -0
- package/caps/providers/vercel.com/project.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +248 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/key.ts +365 -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 +101 -0
- package/structs/HomeRegistry.ts +55 -0
- package/structs/HomeRegistryConfig.ts +56 -0
- package/structs/ProjectCatalogsConfig.ts +53 -0
- package/structs/ProjectDeploymentConfig.ts +56 -0
- package/structs/ProjectDeploymentFact.ts +106 -0
- package/structs/ProjectPublishingFact.ts +68 -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/WorkspacePublishingConfig.ts +65 -0
- package/structs/WorkspaceShellConfig.ts +83 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
- package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
- package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/dynadot.com/DomainFact.ts +46 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
- package/tests/01-Lifecycle/main.test.ts +173 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- 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'
|