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,155 @@
1
+ export async function capsule({
2
+ encapsulate,
3
+ CapsulePropertyTypes,
4
+ makeImportStack
5
+ }: {
6
+ encapsulate: any
7
+ CapsulePropertyTypes: any
8
+ makeImportStack: any
9
+ }) {
10
+ return encapsulate({
11
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
12
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
13
+ '#t44/structs/WorkspaceConfig': {
14
+ as: '$WorkspaceConfig'
15
+ },
16
+ '#t44/structs/ProjectRackConfig': {
17
+ as: '$ProjectRackConfig'
18
+ },
19
+ '#': {
20
+ WorkspacePrompt: {
21
+ type: CapsulePropertyTypes.Mapping,
22
+ value: 't44/caps/WorkspacePrompt'
23
+ },
24
+ HomeRegistry: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: 't44/caps/HomeRegistry'
27
+ },
28
+ ensureRack: {
29
+ type: CapsulePropertyTypes.Function,
30
+ value: async function (this: any): Promise<{ rackName: string }> {
31
+ const workspaceConfig = await this.$WorkspaceConfig.config
32
+ const rackConfig = await this.$ProjectRackConfig.config
33
+
34
+ // Check if projectRack is already set in config (object format: { name, identifier })
35
+ if (rackConfig?.name && rackConfig?.identifier) {
36
+ return { rackName: rackConfig.name }
37
+ }
38
+
39
+ let rackName: string
40
+
41
+ const rackConfigStructKey = '#t44/structs/ProjectRackConfig'
42
+ if (!rackConfig?.name) {
43
+ const chalk = (await import('chalk')).default
44
+
45
+ console.log(chalk.cyan(`\n📦 Project Rack Setup\n`))
46
+ console.log(chalk.gray(` Workspace: ${workspaceConfig?.name || 'unknown'}`))
47
+ console.log(chalk.gray(` Root: ${workspaceConfig?.rootDir || 'unknown'}`))
48
+ console.log(chalk.gray(''))
49
+ console.log(chalk.gray(' The project rack holds an integrated set of projects which can be'))
50
+ console.log(chalk.gray(' pulled into one or more workspaces.'))
51
+ console.log(chalk.gray(' A workspace attached to a rack has access to all projects in the rack'))
52
+ console.log(chalk.gray(' and is able to add more projects to the rack.'))
53
+ console.log(chalk.gray(' All workspaces attached to a rack automatically sync their projects'))
54
+ console.log(chalk.gray(' to the rack.'))
55
+ console.log(chalk.gray(''))
56
+
57
+ // List existing project racks from registry
58
+ const existingRacks = await this.HomeRegistry.listRacks()
59
+
60
+ // Build choices
61
+ const choices: Array<{ name: string; value: any }> = []
62
+
63
+ for (const rack of existingRacks) {
64
+ choices.push({
65
+ name: `${rack.name} ${chalk.gray(rack.did ? rack.did.substring(0, 50) + '...' : '')}`,
66
+ value: { type: 'existing', name: rack.name }
67
+ })
68
+ }
69
+
70
+ choices.push({
71
+ name: chalk.yellow('+ Create a new project rack'),
72
+ value: { type: 'create' }
73
+ })
74
+
75
+ const selected = await this.WorkspacePrompt.select({
76
+ message: 'Select a project rack:',
77
+ choices,
78
+ defaultValue: { type: 'create' },
79
+ pageSize: 15
80
+ })
81
+
82
+ if (selected.type === 'existing') {
83
+ rackName = selected.name
84
+ } else {
85
+ // Prompt for rack name
86
+ rackName = await this.WorkspacePrompt.input({
87
+ message: 'Enter a name for the new project rack:',
88
+ defaultValue: workspaceConfig?.name || 'genesis',
89
+ validate: (input: string) => {
90
+ if (!input || input.trim().length === 0) {
91
+ return 'Project rack name cannot be empty'
92
+ }
93
+ if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
94
+ return 'Project rack name can only contain letters, numbers, underscores, and hyphens'
95
+ }
96
+ return true
97
+ }
98
+ })
99
+ }
100
+ } else {
101
+ rackName = rackConfig.name
102
+ }
103
+
104
+ // Check if rack already exists in registry
105
+ let rackData = await this.HomeRegistry.getRack(rackName)
106
+
107
+ if (!rackData) {
108
+ const chalk = (await import('chalk')).default
109
+ console.log(chalk.cyan(`\n Registering project rack '${rackName}'...\n`))
110
+
111
+ const { generateKeypair } = await import('../lib/ucan.js')
112
+ const { did, privateKey } = await generateKeypair()
113
+
114
+ rackData = {
115
+ did,
116
+ privateKey,
117
+ createdAt: new Date().toISOString()
118
+ }
119
+
120
+ const rackPath = await this.HomeRegistry.setRack(rackName, rackData)
121
+
122
+ console.log(chalk.green(` ✓ Project rack registered at:`))
123
+ console.log(chalk.green(` ${rackPath}`))
124
+ console.log(chalk.green(` ✓ DID: ${rackData.did}\n`))
125
+ } else {
126
+ const chalk = (await import('chalk')).default
127
+ const rackPath = await this.HomeRegistry.getRackPath(rackName)
128
+ console.log(chalk.green(`\n ✓ Using existing project rack at:`))
129
+ console.log(chalk.green(` ${rackPath}`))
130
+ console.log(chalk.green(` ✓ DID: ${rackData.did}\n`))
131
+ }
132
+
133
+ // Store rack as object { name, identifier } in rack config struct
134
+ await this.$ProjectRackConfig.setConfigValue(['name'], rackName)
135
+ await this.$ProjectRackConfig.setConfigValue(['identifier'], rackData.did)
136
+
137
+ return { rackName }
138
+ }
139
+ },
140
+ getRackName: {
141
+ type: CapsulePropertyTypes.Function,
142
+ value: async function (this: any): Promise<string | null> {
143
+ const rackConfig = await this.$ProjectRackConfig.config
144
+ return rackConfig?.name || null
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }, {
150
+ importMeta: import.meta,
151
+ importStack: makeImportStack(),
152
+ capsuleName: capsule['#'],
153
+ })
154
+ }
155
+ capsule['#'] = 't44/caps/ProjectRack'
@@ -0,0 +1,322 @@
1
+
2
+ import { join } from 'path'
3
+ import { $ } from 'bun'
4
+ import { mkdir, access, readFile, writeFile } from 'fs/promises'
5
+ import { constants } from 'fs'
6
+
7
+ export async function capsule({
8
+ encapsulate,
9
+ CapsulePropertyTypes,
10
+ makeImportStack
11
+ }: {
12
+ encapsulate: any
13
+ CapsulePropertyTypes: any
14
+ makeImportStack: any
15
+ }) {
16
+ return encapsulate({
17
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
18
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
19
+ '#t44/structs/WorkspaceConfig': {
20
+ as: '$WorkspaceConfig'
21
+ },
22
+ '#': {
23
+ WorkspaceConfig: {
24
+ type: CapsulePropertyTypes.Mapping,
25
+ value: 't44/caps/WorkspaceConfig'
26
+ },
27
+ getStagePath: {
28
+ type: CapsulePropertyTypes.Function,
29
+ value: async function (this: any, { repoUri }: { repoUri: string }): Promise<string> {
30
+ const normalizedUri = repoUri.replace(/[\/]/g, '~')
31
+ return join(
32
+ this.WorkspaceConfig.workspaceRootDir,
33
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectRepository/stage',
34
+ normalizedUri
35
+ )
36
+ }
37
+ },
38
+ init: {
39
+ type: CapsulePropertyTypes.Function,
40
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
41
+ await mkdir(rootDir, { recursive: true })
42
+
43
+ const gitDir = join(rootDir, '.git')
44
+ let isGitRepo = false
45
+ try {
46
+ await access(gitDir, constants.F_OK)
47
+ isGitRepo = true
48
+ } catch { }
49
+
50
+ if (!isGitRepo) {
51
+ await $`git init`.cwd(rootDir).quiet()
52
+ await $`git commit --allow-empty -m init`.cwd(rootDir).quiet()
53
+ }
54
+ }
55
+ },
56
+ reset: {
57
+ type: CapsulePropertyTypes.Function,
58
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
59
+ await $`git checkout -- .`.cwd(rootDir).quiet().nothrow()
60
+ await $`git clean -fd`.cwd(rootDir).quiet().nothrow()
61
+ }
62
+ },
63
+ sync: {
64
+ type: CapsulePropertyTypes.Function,
65
+ value: async function (this: any, { rootDir, sourceDir, gitignorePath }: {
66
+ rootDir: string
67
+ sourceDir: string
68
+ gitignorePath?: string
69
+ }): Promise<void> {
70
+ let gitignoreExists = false
71
+ if (gitignorePath) {
72
+ try {
73
+ await access(gitignorePath, constants.F_OK)
74
+ gitignoreExists = true
75
+ } catch { }
76
+ }
77
+
78
+ const rsyncArgs = ['rsync', '-a', '--delete', '--exclude', '.git']
79
+ if (gitignoreExists && gitignorePath) {
80
+ rsyncArgs.push('--exclude-from=' + gitignorePath)
81
+ }
82
+ rsyncArgs.push(sourceDir + '/', rootDir + '/')
83
+ await $`${rsyncArgs}`
84
+ }
85
+ },
86
+ hasChanges: {
87
+ type: CapsulePropertyTypes.Function,
88
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
89
+ await $`git add -A`.cwd(rootDir).quiet()
90
+ const diff = await $`git diff --cached --stat`.cwd(rootDir).quiet().nothrow()
91
+ const hasChanges = diff.text().trim().length > 0
92
+ await $`git reset`.cwd(rootDir).quiet().nothrow()
93
+ return hasChanges
94
+ }
95
+ },
96
+ commit: {
97
+ type: CapsulePropertyTypes.Function,
98
+ value: async function (this: any, { rootDir, message }: {
99
+ rootDir: string
100
+ message: string
101
+ }): Promise<void> {
102
+ await $`git add -A`.cwd(rootDir).quiet()
103
+ await $`git commit -m ${message}`.cwd(rootDir).quiet().nothrow()
104
+ }
105
+ },
106
+ getHeadCommit: {
107
+ type: CapsulePropertyTypes.Function,
108
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<string> {
109
+ const result = await $`git rev-parse HEAD`.cwd(rootDir).quiet().nothrow()
110
+ if (result.exitCode !== 0) return ''
111
+ return result.text().trim()
112
+ }
113
+ },
114
+ getBranch: {
115
+ type: CapsulePropertyTypes.Function,
116
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<string> {
117
+ const result = await $`git rev-parse --abbrev-ref HEAD`.cwd(rootDir).quiet()
118
+ return result.text().trim()
119
+ }
120
+ },
121
+ getLastCommitMessage: {
122
+ type: CapsulePropertyTypes.Function,
123
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<string> {
124
+ const result = await $`git log -1 --pretty=%B`.cwd(rootDir).quiet()
125
+ return result.text().trim()
126
+ }
127
+ },
128
+ clone: {
129
+ type: CapsulePropertyTypes.Function,
130
+ value: async function (this: any, { originUri, targetDir }: {
131
+ originUri: string
132
+ targetDir: string
133
+ }): Promise<{ isNewEmptyRepo: boolean }> {
134
+ const parentDir = join(targetDir, '..')
135
+ await mkdir(parentDir, { recursive: true })
136
+ await $`git clone ${originUri} ${targetDir}`.cwd(parentDir)
137
+
138
+ const headCheck = await $`git rev-parse HEAD`.cwd(targetDir).quiet().nothrow()
139
+ return { isNewEmptyRepo: headCheck.exitCode !== 0 }
140
+ }
141
+ },
142
+ addAll: {
143
+ type: CapsulePropertyTypes.Function,
144
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
145
+ await $`git add .`.cwd(rootDir).quiet()
146
+ const statusResult = await $`git status --porcelain`.cwd(rootDir).quiet()
147
+ return statusResult.text().trim().length > 0
148
+ }
149
+ },
150
+ isAheadOfRemote: {
151
+ type: CapsulePropertyTypes.Function,
152
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
153
+ const lsRemoteResult = await $`git ls-remote origin`.cwd(rootDir).quiet().nothrow()
154
+ const lsRemoteOutput = lsRemoteResult.text().trim()
155
+
156
+ if (!lsRemoteOutput) {
157
+ return true
158
+ }
159
+
160
+ const localHead = (await $`git rev-parse HEAD`.cwd(rootDir).quiet()).text().trim()
161
+ const remoteHeadLine = lsRemoteOutput.split('\n').find((l: string) => l.includes('refs/heads/main'))
162
+ const remoteHead = remoteHeadLine ? remoteHeadLine.split('\t')[0] : null
163
+
164
+ return !remoteHead || remoteHead !== localHead
165
+ }
166
+ },
167
+ push: {
168
+ type: CapsulePropertyTypes.Function,
169
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
170
+ await $`git push -u origin main --tags`.cwd(rootDir)
171
+ }
172
+ },
173
+ forcePush: {
174
+ type: CapsulePropertyTypes.Function,
175
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
176
+ await $`git push --force --tags`.cwd(rootDir)
177
+ }
178
+ },
179
+ squashAllCommits: {
180
+ type: CapsulePropertyTypes.Function,
181
+ value: async function (this: any, { rootDir, message }: {
182
+ rootDir: string
183
+ message: string
184
+ }): Promise<void> {
185
+ const rootCommit = await $`git rev-list --max-parents=0 HEAD`.cwd(rootDir).text()
186
+ await $`git reset --soft ${rootCommit.trim()}`.cwd(rootDir)
187
+ await $`git commit --amend -m ${message}`.cwd(rootDir)
188
+ }
189
+ },
190
+ tag: {
191
+ type: CapsulePropertyTypes.Function,
192
+ value: async function (this: any, { rootDir, tag }: {
193
+ rootDir: string
194
+ tag: string
195
+ }): Promise<void> {
196
+ await $`git tag ${tag}`.cwd(rootDir)
197
+ }
198
+ },
199
+ hasTag: {
200
+ type: CapsulePropertyTypes.Function,
201
+ value: async function (this: any, { rootDir, tag }: {
202
+ rootDir: string
203
+ tag: string
204
+ }): Promise<{ exists: boolean, commit?: string }> {
205
+ const localTagCheck = await $`git tag -l ${tag}`.cwd(rootDir).quiet().nothrow()
206
+ if (localTagCheck.text().trim() === tag) {
207
+ const tagCommit = (await $`git rev-parse ${tag}^{}`.cwd(rootDir).quiet().nothrow()).text().trim()
208
+ return { exists: true, commit: tagCommit }
209
+ }
210
+ return { exists: false }
211
+ }
212
+ },
213
+ hasRemoteTag: {
214
+ type: CapsulePropertyTypes.Function,
215
+ value: async function (this: any, { rootDir, tag }: {
216
+ rootDir: string
217
+ tag: string
218
+ }): Promise<{ exists: boolean, commit?: string }> {
219
+ const remoteTagCheck = await $`git ls-remote --tags origin ${tag}`.cwd(rootDir).quiet().nothrow()
220
+ const output = remoteTagCheck.text().trim()
221
+ if (output.length > 0) {
222
+ const commit = output.split(/\s+/)[0]
223
+ return { exists: true, commit }
224
+ }
225
+ return { exists: false }
226
+ }
227
+ },
228
+ diff: {
229
+ type: CapsulePropertyTypes.Function,
230
+ value: async function (this: any, { rootDir, from, to }: {
231
+ rootDir: string
232
+ from: string
233
+ to?: string
234
+ }): Promise<string> {
235
+ const toRef = to || 'HEAD'
236
+ const result = await $`git diff ${from} ${toRef}`.cwd(rootDir).quiet().nothrow()
237
+ return result.text().trim()
238
+ }
239
+ },
240
+ exists: {
241
+ type: CapsulePropertyTypes.Function,
242
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<boolean> {
243
+ try {
244
+ await access(join(rootDir, '.git'), constants.F_OK)
245
+ return true
246
+ } catch {
247
+ return false
248
+ }
249
+ }
250
+ },
251
+ initBare: {
252
+ type: CapsulePropertyTypes.Function,
253
+ value: async function (this: any, { rootDir }: { rootDir: string }): Promise<void> {
254
+ await mkdir(rootDir, { recursive: true })
255
+
256
+ let isBareRepo = false
257
+ try {
258
+ await access(join(rootDir, 'HEAD'), constants.F_OK)
259
+ isBareRepo = true
260
+ } catch { }
261
+
262
+ if (!isBareRepo) {
263
+ await $`git init --bare`.cwd(rootDir).quiet()
264
+ }
265
+ }
266
+ },
267
+ hasRemote: {
268
+ type: CapsulePropertyTypes.Function,
269
+ value: async function (this: any, { rootDir, name }: {
270
+ rootDir: string
271
+ name: string
272
+ }): Promise<boolean> {
273
+ const result = await $`git remote`.cwd(rootDir).quiet().nothrow()
274
+ const remotes = result.text().trim().split('\n').filter(Boolean)
275
+ return remotes.includes(name)
276
+ }
277
+ },
278
+ addRemote: {
279
+ type: CapsulePropertyTypes.Function,
280
+ value: async function (this: any, { rootDir, name, url }: {
281
+ rootDir: string
282
+ name: string
283
+ url: string
284
+ }): Promise<void> {
285
+ await $`git remote add ${name} ${url}`.cwd(rootDir).quiet()
286
+ }
287
+ },
288
+ setRemoteUrl: {
289
+ type: CapsulePropertyTypes.Function,
290
+ value: async function (this: any, { rootDir, name, url }: {
291
+ rootDir: string
292
+ name: string
293
+ url: string
294
+ }): Promise<void> {
295
+ await $`git remote set-url ${name} ${url}`.cwd(rootDir).quiet()
296
+ }
297
+ },
298
+ pushToRemote: {
299
+ type: CapsulePropertyTypes.Function,
300
+ value: async function (this: any, { rootDir, remote, branch, force }: {
301
+ rootDir: string
302
+ remote: string
303
+ branch?: string
304
+ force?: boolean
305
+ }): Promise<void> {
306
+ const branchName = branch || 'main'
307
+ if (force) {
308
+ await $`git push --force ${remote} ${branchName}`.cwd(rootDir).quiet()
309
+ } else {
310
+ await $`git push ${remote} ${branchName}`.cwd(rootDir).quiet()
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }, {
317
+ importMeta: import.meta,
318
+ importStack: makeImportStack(),
319
+ capsuleName: capsule['#'],
320
+ })
321
+ }
322
+ capsule['#'] = 't44/caps/ProjectRepository'