t44 0.2.0-rc.1

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.

Potentially problematic release.


This version of t44 might be problematic. Click here for more details.

Files changed (86) hide show
  1. package/LICENSE.md +203 -0
  2. package/README.md +154 -0
  3. package/bin/activate +36 -0
  4. package/bin/activate.ts +30 -0
  5. package/bin/postinstall.sh +19 -0
  6. package/bin/shell +27 -0
  7. package/bin/t44 +27 -0
  8. package/caps/HomeRegistry.v0.ts +298 -0
  9. package/caps/OpenApiSchema.v0.ts +192 -0
  10. package/caps/ProjectDeployment.v0.ts +363 -0
  11. package/caps/ProjectDevelopment.v0.ts +246 -0
  12. package/caps/ProjectPublishing.v0.ts +307 -0
  13. package/caps/ProjectRack.v0.ts +128 -0
  14. package/caps/WorkspaceCli.v0.ts +391 -0
  15. package/caps/WorkspaceConfig.v0.ts +626 -0
  16. package/caps/WorkspaceConfig.yaml +53 -0
  17. package/caps/WorkspaceConnection.v0.ts +240 -0
  18. package/caps/WorkspaceEntityConfig.v0.ts +64 -0
  19. package/caps/WorkspaceEntityFact.v0.ts +193 -0
  20. package/caps/WorkspaceInfo.v0.ts +554 -0
  21. package/caps/WorkspaceInit.v0.ts +30 -0
  22. package/caps/WorkspaceKey.v0.ts +186 -0
  23. package/caps/WorkspaceProjects.v0.ts +455 -0
  24. package/caps/WorkspacePrompt.v0.ts +396 -0
  25. package/caps/WorkspaceShell.sh +39 -0
  26. package/caps/WorkspaceShell.v0.ts +104 -0
  27. package/caps/WorkspaceShell.yaml +65 -0
  28. package/caps/WorkspaceShellCli.v0.ts +109 -0
  29. package/caps/WorkspaceTest.v0.ts +167 -0
  30. package/caps/providers/LICENSE.md +8 -0
  31. package/caps/providers/README.md +2 -0
  32. package/caps/providers/bunny.net/ProjectDeployment.v0.ts +328 -0
  33. package/caps/providers/bunny.net/api-pull.v0.test.ts +319 -0
  34. package/caps/providers/bunny.net/api-pull.v0.ts +161 -0
  35. package/caps/providers/bunny.net/api-storage.v0.test.ts +168 -0
  36. package/caps/providers/bunny.net/api-storage.v0.ts +245 -0
  37. package/caps/providers/bunny.net/api.v0.ts +95 -0
  38. package/caps/providers/dynadot.com/ProjectDeployment.v0.ts +207 -0
  39. package/caps/providers/dynadot.com/api-domains.v0.test.ts +147 -0
  40. package/caps/providers/dynadot.com/api-domains.v0.ts +137 -0
  41. package/caps/providers/dynadot.com/api.v0.ts +88 -0
  42. package/caps/providers/git-scm.com/ProjectPublishing.v0.ts +231 -0
  43. package/caps/providers/github.com/ProjectPublishing.v0.ts +75 -0
  44. package/caps/providers/github.com/api.v0.ts +90 -0
  45. package/caps/providers/npmjs.com/ProjectPublishing.v0.ts +741 -0
  46. package/caps/providers/vercel.com/ProjectDeployment.v0.ts +339 -0
  47. package/caps/providers/vercel.com/api.v0.test.ts +67 -0
  48. package/caps/providers/vercel.com/api.v0.ts +132 -0
  49. package/caps/providers/vercel.com/bun.lock +194 -0
  50. package/caps/providers/vercel.com/package.json +10 -0
  51. package/caps/providers/vercel.com/project.v0.test.ts +108 -0
  52. package/caps/providers/vercel.com/project.v0.ts +150 -0
  53. package/caps/providers/vercel.com/tsconfig.json +28 -0
  54. package/docs/Overview.drawio +189 -0
  55. package/docs/Overview.svg +4 -0
  56. package/lib/crypto.ts +53 -0
  57. package/lib/openapi.ts +132 -0
  58. package/lib/ucan.ts +137 -0
  59. package/package.json +41 -0
  60. package/structs/HomeRegistryConfig.v0.ts +27 -0
  61. package/structs/ProjectDeploymentConfig.v0.ts +27 -0
  62. package/structs/ProjectDeploymentFact.v0.ts +110 -0
  63. package/structs/ProjectPublishingFact.v0.ts +69 -0
  64. package/structs/ProjectRackConfig.v0.ts +27 -0
  65. package/structs/WorkspaceCliConfig.v0.ts +27 -0
  66. package/structs/WorkspaceConfig.v0.ts +27 -0
  67. package/structs/WorkspaceKeyConfig.v0.ts +27 -0
  68. package/structs/WorkspaceMappings.v0.ts +27 -0
  69. package/structs/WorkspaceProjectsConfig.v0.ts +27 -0
  70. package/structs/WorkspaceRepositories.v0.ts +27 -0
  71. package/structs/WorkspaceShellConfig.v0.ts +45 -0
  72. package/structs/providers/LICENSE.md +8 -0
  73. package/structs/providers/README.md +2 -0
  74. package/structs/providers/bunny.net/ProjectDeploymentFact.v0.ts +41 -0
  75. package/structs/providers/bunny.net/WorkspaceConnectionConfig.v0.ts +42 -0
  76. package/structs/providers/dynadot.com/DomainFact.v0.ts +146 -0
  77. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0.ts +41 -0
  78. package/structs/providers/git-scm.com/ProjectPublishingFact.v0.ts +46 -0
  79. package/structs/providers/github.com/ProjectPublishingFact.v0.ts +52 -0
  80. package/structs/providers/github.com/WorkspaceConnectionConfig.v0.ts +42 -0
  81. package/structs/providers/npmjs.com/ProjectPublishingFact.v0.ts +48 -0
  82. package/structs/providers/vercel.com/ProjectDeploymentFact.v0.ts +38 -0
  83. package/structs/providers/vercel.com/WorkspaceConnectionConfig.v0.ts +48 -0
  84. package/tsconfig.json +28 -0
  85. package/workspace-rt.ts +134 -0
  86. package/workspace.yaml +5 -0
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bun test
2
+
3
+ export const testConfig = {
4
+ group: 'vendor',
5
+ runOnAll: false,
6
+ }
7
+
8
+ import * as bunTest from 'bun:test'
9
+ import { run } from '../../../workspace-rt'
10
+
11
+ const {
12
+ test: { describe, it, expect },
13
+ domains
14
+ } = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
15
+ const spine = await encapsulate({
16
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
17
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
18
+ '#': {
19
+ test: {
20
+ type: CapsulePropertyTypes.Mapping,
21
+ value: 't44/caps/WorkspaceTest.v0',
22
+ options: {
23
+ '#': {
24
+ bunTest,
25
+ env: {
26
+ DYNADOT_API_KEY: { factReference: 't44/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0:apiKey' }
27
+ }
28
+ }
29
+ }
30
+ },
31
+ domains: {
32
+ type: CapsulePropertyTypes.Mapping,
33
+ value: './api-domains.v0'
34
+ },
35
+ }
36
+ }
37
+ }, {
38
+ importMeta: import.meta,
39
+ importStack: makeImportStack(),
40
+ capsuleName: 't44/caps/providers/dynadot.com/api-domains.v0.test'
41
+ })
42
+ return { spine }
43
+ }, async ({ spine, apis }: any) => {
44
+ return apis[spine.capsuleSourceLineRef]
45
+ }, {
46
+ importMeta: import.meta
47
+ })
48
+
49
+ describe('Dynadot Domain API', function () {
50
+
51
+ let domainName: string
52
+
53
+ it('should return list of domains', async function () {
54
+
55
+ const result = await domains.list()
56
+
57
+ expect(result).toBeObject()
58
+ expect(result.ListDomainInfoResponse).toBeObject()
59
+ expect(result.ListDomainInfoResponse.MainDomains).toBeArray()
60
+ expect(result.ListDomainInfoResponse.MainDomains.length).toBeGreaterThan(0)
61
+
62
+ domainName = result.ListDomainInfoResponse.MainDomains[0].Name
63
+ })
64
+
65
+ it('should return domain info for a specific domain', async function () {
66
+
67
+ const result = await domains.getInfo({ name: domainName })
68
+
69
+ expect(result).toBeObject()
70
+ expect(result.DomainInfoResponse).toBeObject()
71
+ expect(result.DomainInfoResponse.DomainInfo).toBeObject()
72
+ expect(result.DomainInfoResponse.DomainInfo.Name).toBe(domainName)
73
+ })
74
+
75
+ it('should return DNS records for a specific domain', async function () {
76
+
77
+ const result = await domains.getDns({ name: domainName })
78
+
79
+ expect(result).toBeObject()
80
+ expect(result.GetDnsResponse).toBeObject()
81
+ })
82
+
83
+ it('should return nameservers for a specific domain', async function () {
84
+
85
+ const result = await domains.getNameservers({ name: domainName })
86
+
87
+ expect(result).toBeObject()
88
+ expect(result.GetNsResponse).toBeObject()
89
+ })
90
+
91
+ it('should set DNS records, verify, and restore original', async function () {
92
+
93
+ const originalDns = await domains.getDns({ name: domainName })
94
+ expect(originalDns).toBeObject()
95
+ expect(originalDns.GetDnsResponse).toBeObject()
96
+
97
+ const originalRecords = originalDns.GetDnsResponse.GetDns?.NameServerSettings?.SubDomains || []
98
+
99
+ const testCnameRecord = {
100
+ record_type: 'cname',
101
+ subdomain: 't44-test-api-domains',
102
+ value: 'example.com'
103
+ }
104
+
105
+ const updatedRecords = [...originalRecords.map((r: any) => ({
106
+ record_type: r.RecordType.toLowerCase(),
107
+ subdomain: r.Subhost,
108
+ value: r.Value
109
+ })), testCnameRecord]
110
+
111
+ const setResult = await domains.setDns({
112
+ name: domainName,
113
+ records: updatedRecords
114
+ })
115
+ expect(setResult).toBeObject()
116
+ expect(setResult.SetDnsResponse.Status).toBe('success')
117
+
118
+ const verifyDns = await domains.getDns({ name: domainName })
119
+ expect(verifyDns).toBeObject()
120
+ expect(verifyDns.GetDnsResponse).toBeObject()
121
+ const verifyRecords = verifyDns.GetDnsResponse.GetDns?.NameServerSettings?.SubDomains || []
122
+ expect(verifyRecords.length).toBe(originalRecords.length + 1)
123
+
124
+ const foundNewRecord = verifyRecords.find((r: any) => r.Subhost === 't44-test-api-domains')
125
+ expect(foundNewRecord).toBeDefined()
126
+ expect(foundNewRecord.RecordType).toBe('CNAME')
127
+ expect(foundNewRecord.Value).toBe('example.com')
128
+
129
+ const restoreRecords = originalRecords.map((r: any) => ({
130
+ record_type: r.RecordType.toLowerCase(),
131
+ subdomain: r.Subhost,
132
+ value: r.Value
133
+ }))
134
+
135
+ const restoreResult = await domains.setDns({
136
+ name: domainName,
137
+ records: restoreRecords
138
+ })
139
+ expect(restoreResult).toBeObject()
140
+
141
+ const finalDns = await domains.getDns({ name: domainName })
142
+ expect(finalDns).toBeObject()
143
+ const finalRecords = finalDns.GetDnsResponse.GetDns?.NameServerSettings?.SubDomains || []
144
+ expect(finalRecords.length).toBe(originalRecords.length)
145
+ })
146
+
147
+ })
@@ -0,0 +1,137 @@
1
+
2
+
3
+ export async function capsule({
4
+ encapsulate,
5
+ CapsulePropertyTypes,
6
+ makeImportStack
7
+ }: {
8
+ encapsulate: any
9
+ CapsulePropertyTypes: any
10
+ makeImportStack: any
11
+ }) {
12
+
13
+ // https://www.dynadot.com/domain/api-commands
14
+ return encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
17
+ '#t44/structs/providers/dynadot.com/DomainFact.v0': {
18
+ as: '$DomainFact'
19
+ },
20
+ '#': {
21
+
22
+ api: {
23
+ type: CapsulePropertyTypes.Mapping,
24
+ value: './api.v0'
25
+ },
26
+
27
+ list: {
28
+ type: CapsulePropertyTypes.Function,
29
+ value: async function (this: any) {
30
+ const result = await this.api.call({
31
+ command: 'list_domain',
32
+ operation: 'list'
33
+ });
34
+
35
+ await this.$DomainFact.set('domains-list', 'list', 'ListDomainInfoResponse', result);
36
+
37
+ return result;
38
+ }
39
+ },
40
+
41
+ getInfo: {
42
+ type: CapsulePropertyTypes.Function,
43
+ value: async function (this: any, options: { name: string }) {
44
+ const result = await this.api.call({
45
+ command: 'domain_info',
46
+ params: {
47
+ domain: options.name
48
+ },
49
+ operation: 'info'
50
+ });
51
+
52
+ await this.$DomainFact.set('domains', options.name, 'DomainInfoResponse', result);
53
+
54
+ return result;
55
+ }
56
+ },
57
+
58
+ getDns: {
59
+ type: CapsulePropertyTypes.Function,
60
+ value: async function (this: any, options: { name: string }) {
61
+ const result = await this.api.call({
62
+ command: 'get_dns',
63
+ params: {
64
+ domain: options.name
65
+ },
66
+ operation: 'getDns'
67
+ });
68
+
69
+ await this.$DomainFact.set('dns', options.name, 'GetDnsResponse', result);
70
+
71
+ return result;
72
+ }
73
+ },
74
+
75
+ getNameservers: {
76
+ type: CapsulePropertyTypes.Function,
77
+ value: async function (this: any, options: { name: string }) {
78
+ const result = await this.api.call({
79
+ command: 'get_ns',
80
+ params: {
81
+ domain: options.name
82
+ },
83
+ operation: 'getNameservers'
84
+ });
85
+
86
+ await this.$DomainFact.set('nameservers', options.name, 'GetNsResponse', result);
87
+
88
+ return result;
89
+ }
90
+ },
91
+
92
+ setDns: {
93
+ type: CapsulePropertyTypes.Function,
94
+ value: async function (this: any, options: { name: string; records: any[]; mainDomain?: { recordType: string; value: string } }) {
95
+ const params: Record<string, any> = {
96
+ domain: options.name
97
+ };
98
+
99
+ // Set main domain record if provided (for root domain A/AAAA/CNAME)
100
+ // Note: Dynadot API requires index suffix (e.g., main_record_type0, main_record0)
101
+ if (options.mainDomain) {
102
+ params['main_record_type0'] = options.mainDomain.recordType;
103
+ params['main_record0'] = options.mainDomain.value;
104
+ }
105
+
106
+ options.records.forEach((record, index) => {
107
+ const recordIndex = index + 1;
108
+
109
+ if (record.subdomain !== undefined) {
110
+ params[`subdomain${recordIndex}`] = record.subdomain;
111
+ }
112
+ if (record.record_type) {
113
+ params[`sub_record_type${recordIndex}`] = record.record_type;
114
+ }
115
+ if (record.value !== undefined) {
116
+ params[`sub_record${recordIndex}`] = record.value;
117
+ }
118
+ });
119
+
120
+ return await this.api.call({
121
+ command: 'set_dns2',
122
+ params,
123
+ operation: 'setDns'
124
+ });
125
+ }
126
+ }
127
+
128
+ }
129
+ }
130
+ }, {
131
+ importMeta: import.meta,
132
+ importStack: makeImportStack(),
133
+ capsuleName: capsule['#'],
134
+ })
135
+ }
136
+ capsule['#'] = 't44/caps/providers/dynadot.com/api-domains.v0'
137
+
@@ -0,0 +1,88 @@
1
+
2
+ import axios from 'axios';
3
+
4
+ export async function capsule({
5
+ encapsulate,
6
+ CapsulePropertyTypes,
7
+ makeImportStack
8
+ }: {
9
+ encapsulate: any
10
+ CapsulePropertyTypes: any
11
+ makeImportStack: any
12
+ }) {
13
+ // https://www.dynadot.com/domain/api-commands
14
+ return encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
17
+ '#t44/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0': {
18
+ as: '$ConnectionConfig'
19
+ },
20
+ '#': {
21
+
22
+ call: {
23
+ type: CapsulePropertyTypes.Function,
24
+ value: async function (this: any, options: {
25
+ command: string;
26
+ params?: Record<string, string | number | boolean>;
27
+ operation?: string;
28
+ }) {
29
+ const apiKey = await this.$ConnectionConfig.getConfigValue('apiKey')
30
+
31
+ const baseUrl = 'https://api.dynadot.com/api3.json';
32
+
33
+ // Build query params
34
+ const searchParams = new URLSearchParams();
35
+ searchParams.append('key', apiKey);
36
+ searchParams.append('command', options.command);
37
+
38
+ if (options.params) {
39
+ for (const [key, value] of Object.entries(options.params)) {
40
+ searchParams.append(key, String(value));
41
+ }
42
+ }
43
+
44
+ const url = `${baseUrl}?${searchParams.toString()}`;
45
+
46
+ try {
47
+ const response = await axios.get(url);
48
+ return response.data;
49
+ } catch (error: any) {
50
+ if (error.response) {
51
+ const errorData = error.response.data;
52
+ const status = error.response.status;
53
+ const statusText = error.response.statusText;
54
+
55
+ const operationName = options.operation || options.command;
56
+ console.error(`Dynadot API Error [${operationName}]:`);
57
+ console.error(` Status: ${status} ${statusText}`);
58
+ console.error(` Response:`, JSON.stringify(errorData, null, 2));
59
+
60
+ let errorMessage = `Dynadot API ${operationName} failed: ${status} ${statusText}`;
61
+
62
+ if (errorData && typeof errorData === 'object') {
63
+ if (errorData.Message) {
64
+ errorMessage += ` - ${errorData.Message}`;
65
+ } else if (errorData.message) {
66
+ errorMessage += ` - ${errorData.message}`;
67
+ } else {
68
+ errorMessage += ` - ${JSON.stringify(errorData)}`;
69
+ }
70
+ }
71
+
72
+ throw new Error(errorMessage);
73
+ }
74
+ throw error;
75
+ }
76
+ }
77
+ }
78
+
79
+ }
80
+ }
81
+ }, {
82
+ importMeta: import.meta,
83
+ importStack: makeImportStack(),
84
+ capsuleName: capsule['#'],
85
+ })
86
+ }
87
+ capsule['#'] = 't44/caps/providers/dynadot.com/api.v0'
88
+
@@ -0,0 +1,231 @@
1
+
2
+ import { join } from 'path'
3
+ import { $ } from 'bun'
4
+ import { mkdir, access, writeFile } from 'fs/promises'
5
+ import { constants } from 'fs'
6
+ import chalk from 'chalk'
7
+
8
+ export async function capsule({
9
+ encapsulate,
10
+ CapsulePropertyTypes,
11
+ makeImportStack
12
+ }: {
13
+ encapsulate: any
14
+ CapsulePropertyTypes: any
15
+ makeImportStack: any
16
+ }) {
17
+ // High level API that deals with everything concerning a git repository.
18
+ return encapsulate({
19
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
20
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
21
+ '#t44/structs/providers/git-scm.com/ProjectPublishingFact.v0': {
22
+ as: '$GitFact'
23
+ },
24
+ '#t44/structs/ProjectPublishingFact.v0': {
25
+ as: '$StatusFact'
26
+ },
27
+ '#': {
28
+ WorkspacePrompt: {
29
+ type: CapsulePropertyTypes.Mapping,
30
+ value: 't44/caps/WorkspacePrompt.v0'
31
+ },
32
+ push: {
33
+ type: CapsulePropertyTypes.Function,
34
+ value: async function (this: any, { projectionDir, config, dangerouslyResetMain }: { projectionDir: string, config: any, dangerouslyResetMain?: boolean }) {
35
+
36
+ const originUri = config.provider.config.RepositorySettings.origin
37
+
38
+ console.log(`Push source '${config.sourceDir}' to git repo '${originUri}' ...`)
39
+ if (dangerouslyResetMain) {
40
+ console.log(`Reset mode enabled - will reset repository to initial commit`)
41
+ }
42
+
43
+ const projectSourceDir = join(config.sourceDir)
44
+ const projectProjectionDir = join(projectionDir, 'repos', originUri.replace(/[@:\/]/g, '~'))
45
+
46
+ // Check if repository directory exists
47
+ let repoExists = false
48
+ try {
49
+ await access(projectProjectionDir, constants.F_OK)
50
+ repoExists = true
51
+ } catch {
52
+ repoExists = false
53
+ }
54
+
55
+ let isNewEmptyRepo = false
56
+ if (!repoExists) {
57
+ // Clone the repository if it doesn't exist
58
+ console.log(`Cloning repository from '${originUri}' ...`)
59
+ const reposDir = join(projectionDir, 'repos')
60
+ await mkdir(reposDir, { recursive: true })
61
+ await $`git clone "${originUri}" "${projectProjectionDir}"`.cwd(reposDir)
62
+
63
+ // Check if the cloned repo is empty (no commits)
64
+ const headCheck = await $`git rev-parse HEAD`.cwd(projectProjectionDir).quiet().nothrow()
65
+ if (headCheck.exitCode !== 0) {
66
+ isNewEmptyRepo = true
67
+ }
68
+ }
69
+
70
+ // Sync files using rsync with gitignore support and delete removed files
71
+ // --exclude-from reads patterns from .gitignore
72
+ // --exclude '.git' ensures we don't copy git metadata
73
+ // --delete removes files in dest that are no longer in source
74
+ const gitignorePath = join(projectSourceDir, '.gitignore')
75
+ await $`rsync -a --delete --exclude '.git' --exclude-from="${gitignorePath}" "${projectSourceDir}/" "${projectProjectionDir}/"`
76
+
77
+ // Generate files from config properties starting with '/'
78
+ // This happens AFTER rsync so generated files are not overwritten
79
+ if (config.provider.config) {
80
+ for (const [key, value] of Object.entries(config.provider.config)) {
81
+ if (key.startsWith('/')) {
82
+ const targetPath = join(projectProjectionDir, key)
83
+ const targetDir = join(targetPath, '..')
84
+
85
+ // Check if file already exists
86
+ let fileExists = false
87
+ try {
88
+ await access(targetPath, constants.F_OK)
89
+ fileExists = true
90
+ } catch {
91
+ fileExists = false
92
+ }
93
+
94
+ if (fileExists) {
95
+ console.log(`Overwriting file '${key}' in repository ...`)
96
+ } else {
97
+ console.log(`Creating file '${key}' in repository ...`)
98
+ }
99
+
100
+ // Ensure directory exists
101
+ await mkdir(targetDir, { recursive: true })
102
+
103
+ // Write file content
104
+ const content = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
105
+ await writeFile(targetPath, content, 'utf-8')
106
+ }
107
+ }
108
+ }
109
+
110
+ // Git add, commit, and push changes
111
+ console.log(`Committing changes ...`)
112
+ await $`git add .`.cwd(projectProjectionDir)
113
+
114
+ // Check if there are changes to commit
115
+ const statusResult = await $`git status --porcelain`.cwd(projectProjectionDir).text()
116
+ const hasNewChanges = statusResult.trim().length > 0
117
+
118
+ // Handle reset (works on existing commits, regardless of new changes)
119
+ let shouldReset = false
120
+ if (dangerouslyResetMain) {
121
+ shouldReset = await this.WorkspacePrompt.confirm({
122
+ title: 'āš ļø WARNING: DESTRUCTIVE OPERATION āš ļø',
123
+ description: [
124
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
125
+ 'Resetting will:',
126
+ ' • Destroy all commit history in the local repository',
127
+ ' • Destroy all commit history on GitHub when force pushed',
128
+ ' • Cannot be undone once pushed to remote',
129
+ '',
130
+ 'This should ONLY be done at the very beginning of a project.',
131
+ '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
132
+ ],
133
+ message: 'Are you absolutely sure you want to reset all commits and destroy the history?',
134
+ defaultValue: false,
135
+ onSuccess: async (confirmed: boolean) => {
136
+ if (confirmed) {
137
+ const chalk = (await import('chalk')).default
138
+ console.log(chalk.cyan(`\nResetting all commits to initial commit ...`))
139
+ } else {
140
+ console.log('\nReset operation cancelled. Pushing without resetting...\n')
141
+ }
142
+ }
143
+ })
144
+
145
+ if (shouldReset) {
146
+ // Get the initial commit (root commit)
147
+ const rootCommit = await $`git rev-list --max-parents=0 HEAD`.cwd(projectProjectionDir).text()
148
+ const rootCommitHash = rootCommit.trim()
149
+
150
+ // Soft reset to the root commit, keeping all changes staged
151
+ await $`git reset --soft ${rootCommitHash}`.cwd(projectProjectionDir)
152
+
153
+ // Commit all changes as a single commit (this replaces the root commit)
154
+ await $`git commit --amend -m "Published using @Stream44 Studio"`.cwd(projectProjectionDir)
155
+
156
+ console.log(`Repository reset to initial commit`)
157
+ }
158
+ } else if (hasNewChanges) {
159
+ // Only commit new changes if not resetting
160
+ await $`git commit -m "Published using @Stream44 Studio"`.cwd(projectProjectionDir)
161
+ console.log(`New changes committed`)
162
+ } else {
163
+ console.log(`No new changes to commit`)
164
+ }
165
+
166
+ // Check if local is ahead of remote using ls-remote (not cached refs)
167
+ let localAheadOfRemote = false
168
+ if (!shouldReset && !hasNewChanges && !isNewEmptyRepo) {
169
+ const lsRemoteResult = await $`git ls-remote origin`.cwd(projectProjectionDir).quiet().nothrow()
170
+ const lsRemoteOutput = lsRemoteResult.text().trim()
171
+
172
+ if (!lsRemoteOutput) {
173
+ // Remote is completely empty — must push
174
+ localAheadOfRemote = true
175
+ } else {
176
+ // Parse remote HEAD ref
177
+ const localHead = (await $`git rev-parse HEAD`.cwd(projectProjectionDir).quiet()).text().trim()
178
+ const remoteHeadLine = lsRemoteOutput.split('\n').find((l: string) => l.includes('refs/heads/main'))
179
+ const remoteHead = remoteHeadLine ? remoteHeadLine.split('\t')[0] : null
180
+
181
+ if (!remoteHead || remoteHead !== localHead) {
182
+ localAheadOfRemote = true
183
+ }
184
+ }
185
+ }
186
+
187
+ // Push to remote
188
+ if (shouldReset) {
189
+ console.log(`Force pushing to remote ...`)
190
+ await $`git push --force --tags`.cwd(projectProjectionDir)
191
+ console.log(`Force pushed to remote`)
192
+ } else if (isNewEmptyRepo || hasNewChanges || localAheadOfRemote) {
193
+ console.log(`Pushing to remote ...`)
194
+ await $`git push -u origin main --tags`.cwd(projectProjectionDir)
195
+ console.log(`Pushed to remote`)
196
+ }
197
+
198
+ // Write fact files
199
+ const lastCommit = await $`git rev-parse HEAD`.cwd(projectProjectionDir).text()
200
+ const lastCommitMessage = await $`git log -1 --pretty=%B`.cwd(projectProjectionDir).text()
201
+ const branch = await $`git rev-parse --abbrev-ref HEAD`.cwd(projectProjectionDir).text()
202
+
203
+ const repoFactName = originUri.replace(/[@:\/]/g, '~')
204
+
205
+ await this.$GitFact.set('repositories', repoFactName, 'GitRepository', {
206
+ origin: originUri,
207
+ branch: branch.trim(),
208
+ lastCommit: lastCommit.trim(),
209
+ lastCommitMessage: lastCommitMessage.trim(),
210
+ pushedAt: new Date().toISOString()
211
+ })
212
+
213
+ await this.$StatusFact.set('ProjectPublishingStatus', repoFactName, 'ProjectPublishingStatus', {
214
+ projectName: originUri,
215
+ provider: 'git-scm.com',
216
+ status: hasNewChanges || shouldReset || localAheadOfRemote ? 'PUBLISHED' : 'READY',
217
+ publicUrl: originUri,
218
+ updatedAt: new Date().toISOString()
219
+ })
220
+
221
+ }
222
+ },
223
+ }
224
+ }
225
+ }, {
226
+ importMeta: import.meta,
227
+ importStack: makeImportStack(),
228
+ capsuleName: capsule['#'],
229
+ })
230
+ }
231
+ capsule['#'] = 't44/caps/providers/git-scm.com/ProjectPublishing.v0'
@@ -0,0 +1,75 @@
1
+
2
+ import chalk from 'chalk'
3
+
4
+ export async function capsule({
5
+ encapsulate,
6
+ CapsulePropertyTypes,
7
+ makeImportStack
8
+ }: {
9
+ encapsulate: any
10
+ CapsulePropertyTypes: any
11
+ makeImportStack: any
12
+ }) {
13
+ // Ensures the GitHub repository is provisioned before git-scm pushes to it.
14
+ return encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
17
+ '#t44/structs/providers/github.com/ProjectPublishingFact.v0': {
18
+ as: '$GitHubFact'
19
+ },
20
+ '#t44/structs/ProjectPublishingFact.v0': {
21
+ as: '$StatusFact'
22
+ },
23
+ '#': {
24
+ GitHubApi: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: 't44/caps/providers/github.com/api.v0'
27
+ },
28
+ push: {
29
+ type: CapsulePropertyTypes.Function,
30
+ value: async function (this: any, { config }: { config: any }) {
31
+
32
+ const repoSettings = config.provider.config.RepositorySettings
33
+ const owner = repoSettings.owner
34
+ const repo = repoSettings.repo
35
+ const isPrivate = repoSettings.public === true ? false : true
36
+ const description = repoSettings.description || ''
37
+
38
+ console.log(chalk.cyan(`\nšŸ” Ensuring GitHub repository '${owner}/${repo}' ...`))
39
+
40
+ const result = await this.GitHubApi.ensureRepo({
41
+ owner,
42
+ repo,
43
+ isPrivate,
44
+ description
45
+ })
46
+
47
+ if (result.created) {
48
+ console.log(chalk.green(` āœ“ Repository '${owner}/${repo}' created on GitHub\n`))
49
+ } else {
50
+ console.log(chalk.green(` āœ“ Repository '${owner}/${repo}' already exists\n`))
51
+ }
52
+
53
+ // Write provider-specific fact file
54
+ await this.$GitHubFact.set('repositories', `${owner}~${repo}`, 'Repository', result.repo)
55
+
56
+ // Write publishing status fact
57
+ const statusResult = {
58
+ projectName: `${owner}/${repo}`,
59
+ provider: 'github.com',
60
+ status: 'READY',
61
+ publicUrl: result.repo.html_url,
62
+ updatedAt: new Date().toISOString()
63
+ }
64
+ await this.$StatusFact.set('ProjectPublishingStatus', `${owner}~${repo}`, 'ProjectPublishingStatus', statusResult)
65
+ }
66
+ },
67
+ }
68
+ }
69
+ }, {
70
+ importMeta: import.meta,
71
+ importStack: makeImportStack(),
72
+ capsuleName: capsule['#'],
73
+ })
74
+ }
75
+ capsule['#'] = 't44/caps/providers/github.com/ProjectPublishing.v0'