@stream44.studio/t44 0.4.0-rc.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yaml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yaml +13 -0
  4. package/.github/workflows/test.yaml +31 -0
  5. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  7. package/.o/GordianOpenIntegrity.yaml +21 -0
  8. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  9. package/.o/stream44.studio/assets/Icon-v1.svg +1170 -0
  10. package/.repo-identifier +1 -0
  11. package/DCO.md +34 -0
  12. package/LICENSE.txt +186 -0
  13. package/README.md +189 -0
  14. package/bin/activate +36 -0
  15. package/bin/activate.ts +30 -0
  16. package/bin/postinstall.sh +19 -0
  17. package/bin/shell +27 -0
  18. package/bin/t44 +27 -0
  19. package/caps/ConfigSchemaStruct.ts +55 -0
  20. package/caps/Home.ts +57 -0
  21. package/caps/HomeRegistry.ts +319 -0
  22. package/caps/HomeRegistryFile.ts +144 -0
  23. package/caps/JsonSchemas.ts +220 -0
  24. package/caps/OpenApiSchema.ts +67 -0
  25. package/caps/PackageDescriptor.ts +88 -0
  26. package/caps/ProjectCatalogs.ts +153 -0
  27. package/caps/ProjectDeployment.ts +426 -0
  28. package/caps/ProjectDevelopment.ts +257 -0
  29. package/caps/ProjectPublishing.ts +654 -0
  30. package/caps/ProjectPulling.ts +234 -0
  31. package/caps/ProjectRack.ts +155 -0
  32. package/caps/ProjectRepository.ts +332 -0
  33. package/caps/ProjectTest.ts +251 -0
  34. package/caps/ProjectTestLib.ts +257 -0
  35. package/caps/RootKey.ts +219 -0
  36. package/caps/SigningKey.ts +243 -0
  37. package/caps/TaskWorkflow.ts +192 -0
  38. package/caps/WorkspaceCli.ts +448 -0
  39. package/caps/WorkspaceConfig.ts +268 -0
  40. package/caps/WorkspaceConfig.yaml +87 -0
  41. package/caps/WorkspaceConfigFile.ts +902 -0
  42. package/caps/WorkspaceConnection.ts +329 -0
  43. package/caps/WorkspaceEntityConfig.ts +78 -0
  44. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  45. package/caps/WorkspaceEntityFact.ts +218 -0
  46. package/caps/WorkspaceInfo.ts +619 -0
  47. package/caps/WorkspaceInit.ts +30 -0
  48. package/caps/WorkspaceKey.ts +338 -0
  49. package/caps/WorkspaceModel.ts +373 -0
  50. package/caps/WorkspaceProjects.ts +636 -0
  51. package/caps/WorkspacePrompt.ts +430 -0
  52. package/caps/WorkspaceShell.sh +39 -0
  53. package/caps/WorkspaceShell.ts +104 -0
  54. package/caps/WorkspaceShell.yaml +64 -0
  55. package/caps/WorkspaceShellCli.ts +109 -0
  56. package/caps/patterns/README.md +2 -0
  57. package/caps/patterns/git-scm.com/ProjectPublishing.ts +507 -0
  58. package/caps/patterns/semver.org/ProjectPublishing.ts +458 -0
  59. package/docs/Overview.drawio +248 -0
  60. package/docs/Overview.svg +4 -0
  61. package/examples/01-Lifecycle/main.test.ts +223 -0
  62. package/lib/crypto.ts +53 -0
  63. package/lib/key.ts +381 -0
  64. package/lib/schema-console-renderer.ts +181 -0
  65. package/lib/schema-resolver.ts +349 -0
  66. package/lib/ucan.ts +137 -0
  67. package/package.json +91 -0
  68. package/standalone-rt.test.ts +150 -0
  69. package/standalone-rt.ts +140 -0
  70. package/structs/HomeRegistry.ts +55 -0
  71. package/structs/HomeRegistryConfig.ts +60 -0
  72. package/structs/ProjectCatalogsConfig.ts +53 -0
  73. package/structs/ProjectDeploymentConfig.ts +56 -0
  74. package/structs/ProjectDeploymentFact.ts +106 -0
  75. package/structs/ProjectPublishingConfig.ts +78 -0
  76. package/structs/ProjectPublishingFact.ts +68 -0
  77. package/structs/ProjectPullingConfig.ts +52 -0
  78. package/structs/ProjectRack.ts +51 -0
  79. package/structs/ProjectRackConfig.ts +56 -0
  80. package/structs/RepositoryOriginDescriptor.ts +51 -0
  81. package/structs/RootKeyConfig.ts +64 -0
  82. package/structs/SigningKeyConfig.ts +64 -0
  83. package/structs/Workspace.ts +56 -0
  84. package/structs/WorkspaceCatalogs.ts +56 -0
  85. package/structs/WorkspaceCliConfig.ts +53 -0
  86. package/structs/WorkspaceConfig.ts +64 -0
  87. package/structs/WorkspaceConfigFile.ts +50 -0
  88. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  89. package/structs/WorkspaceKey.ts +55 -0
  90. package/structs/WorkspaceKeyConfig.ts +56 -0
  91. package/structs/WorkspaceMappingsConfig.ts +56 -0
  92. package/structs/WorkspaceProject.ts +104 -0
  93. package/structs/WorkspaceProjectsConfig.ts +67 -0
  94. package/structs/WorkspaceShellConfig.ts +83 -0
  95. package/structs/patterns/README.md +2 -0
  96. package/structs/patterns/git-scm.com/ProjectPublishingFact.ts +46 -0
  97. package/tsconfig.json +33 -0
  98. package/workspace-rt.ts +152 -0
  99. package/workspace.yaml +3 -0
@@ -0,0 +1,234 @@
1
+
2
+ import { join, resolve } from 'path'
3
+ import { $ } from 'bun'
4
+ import chalk from 'chalk'
5
+
6
+ // ── Provider Lifecycle Steps ─────────────────────────────────────────
7
+ // Each pull provider capsule can implement these methods:
8
+ //
9
+ // 1. parseUrl — parse a URL and return { owner, repo, branch } or null
10
+ // 2. prepare — clone/fetch the mirror repo, checkout the branch
11
+ // 3. pull — generate diff and apply to sourceDir
12
+ //
13
+ // Every method receives { config, ctx } where:
14
+ // config = the provider's own config entry (with .capsule and .config)
15
+ // ctx = shared pullingContext for the current operation
16
+
17
+ export async function capsule({
18
+ encapsulate,
19
+ CapsulePropertyTypes,
20
+ makeImportStack
21
+ }: {
22
+ encapsulate: any
23
+ CapsulePropertyTypes: any
24
+ makeImportStack: any
25
+ }) {
26
+ return encapsulate({
27
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
28
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
29
+ '#@stream44.studio/t44/structs/WorkspaceConfig': {
30
+ as: '$WorkspaceConfig'
31
+ },
32
+ '#@stream44.studio/t44/structs/ProjectPullingConfig': {
33
+ as: '$WorkspacePulling'
34
+ },
35
+ '#@stream44.studio/t44/structs/ProjectPublishingConfig': {
36
+ as: '$WorkspaceRepositories'
37
+ },
38
+ '#': {
39
+ WorkspaceConfig: {
40
+ type: CapsulePropertyTypes.Mapping,
41
+ value: '@stream44.studio/t44/caps/WorkspaceConfig'
42
+ },
43
+ ProjectRepository: {
44
+ type: CapsulePropertyTypes.Mapping,
45
+ value: '@stream44.studio/t44/caps/ProjectRepository'
46
+ },
47
+ run: {
48
+ type: CapsulePropertyTypes.Function,
49
+ value: async function (this: any, { args }: any): Promise<void> {
50
+
51
+ const { url } = args
52
+
53
+ if (!url) {
54
+ console.error(chalk.red('\n[t44] ERROR: A URL argument is required.'))
55
+ console.error(chalk.red(' Usage: t44 pull <url>'))
56
+ console.error(chalk.red(' Example: t44 pull https://github.com/Stream44/FramespaceGenesis/tree/fixes\n'))
57
+ process.exit(1)
58
+ }
59
+
60
+ // ── Dynamic provider loader ──────────────────────────
61
+ const providerCache = new Map<string, any>()
62
+ const getProvider = async (uri: string) => {
63
+ const cleanUri = uri.startsWith('#') ? uri.substring(1) : uri
64
+ if (!providerCache.has(cleanUri)) {
65
+ const { api } = await this.self.importCapsule({ uri: cleanUri })
66
+ providerCache.set(cleanUri, api)
67
+ }
68
+ return providerCache.get(cleanUri)!
69
+ }
70
+
71
+ // ── Load pulling config ──────────────────────────────
72
+ const pullingConfig = await this.$WorkspacePulling.config
73
+
74
+ const globalProviders: any[] = Array.isArray(pullingConfig?.providers)
75
+ ? pullingConfig.providers
76
+ : []
77
+
78
+ if (globalProviders.length === 0) {
79
+ console.error(chalk.red('\n[t44] ERROR: No pulling providers configured.'))
80
+ console.error(chalk.red(' Add providers under "#@stream44.studio/t44/structs/ProjectPullingConfig" in workspace.yaml\n'))
81
+ process.exit(1)
82
+ }
83
+
84
+ // ── STEP 1: parseUrl — ask each provider to parse the URL ──
85
+ console.log(`\n[t44] Parsing URL: ${chalk.cyan(url)}\n`)
86
+
87
+ let parsedUrl: any = null
88
+ let matchedProviderConfig: any = null
89
+
90
+ for (const providerConfig of globalProviders) {
91
+ if (providerConfig.enabled === false) continue
92
+
93
+ const provider = await getProvider(providerConfig.capsule)
94
+ if (typeof provider.parseUrl !== 'function') continue
95
+
96
+ const result = await provider.parseUrl({ config: providerConfig, url })
97
+ if (result) {
98
+ parsedUrl = result
99
+ matchedProviderConfig = providerConfig
100
+ console.log(chalk.green(` āœ“ URL parsed by '${providerConfig.capsule}'`))
101
+ console.log(chalk.gray(` owner: ${result.owner}, repo: ${result.repo}, branch: ${result.branch}\n`))
102
+ break
103
+ }
104
+ }
105
+
106
+ if (!parsedUrl || !matchedProviderConfig) {
107
+ console.error(chalk.red('\n[t44] ERROR: No pull provider could parse the URL.'))
108
+ console.error(chalk.red(` URL: ${url}`))
109
+ console.error(chalk.red(` Configured providers:`))
110
+ for (const p of globalProviders) {
111
+ console.error(chalk.red(` - ${p.capsule} (enabled: ${p.enabled !== false})`))
112
+ }
113
+ console.error(chalk.red(`\n Make sure a provider for this URL type is configured.\n`))
114
+ process.exit(1)
115
+ }
116
+
117
+ // ── STEP 2: Find matching publishing repository ──────
118
+ const publishingConfig = await this.$WorkspaceRepositories.config
119
+
120
+ if (!publishingConfig?.repositories) {
121
+ console.error(chalk.red('\n[t44] ERROR: No publishing repositories configured.'))
122
+ console.error(chalk.red(' Cannot match URL to a local project without publishing config.\n'))
123
+ process.exit(1)
124
+ }
125
+
126
+ let matchedRepoName: string | null = null
127
+ let matchedRepoConfig: any = null
128
+
129
+ for (const [repoName, repoConfig] of Object.entries(publishingConfig.repositories)) {
130
+ const typedConfig = repoConfig as any
131
+ const providers: any[] = Array.isArray(typedConfig.providers) ? typedConfig.providers : []
132
+
133
+ for (const provider of providers) {
134
+ // Match against GitHub publishing provider config
135
+ if (provider.capsule === '@stream44.studio/t44-github.com/caps/ProjectPublishing') {
136
+ const settings = provider.config?.RepositorySettings
137
+ if (settings?.owner === parsedUrl.owner && settings?.repo === parsedUrl.repo) {
138
+ matchedRepoName = repoName
139
+ matchedRepoConfig = typedConfig
140
+ break
141
+ }
142
+ }
143
+ }
144
+ if (matchedRepoName) break
145
+ }
146
+
147
+ if (!matchedRepoName || !matchedRepoConfig) {
148
+ console.error(chalk.red(`\n[t44] ERROR: No matching publishing repository found for ${parsedUrl.owner}/${parsedUrl.repo}.`))
149
+ console.error(chalk.red(` The URL points to '${parsedUrl.owner}/${parsedUrl.repo}' but no repository in`))
150
+ console.error(chalk.red(` "#@stream44.studio/t44/structs/ProjectPublishingConfig" has a GitHub provider with matching owner/repo.`))
151
+ console.error(chalk.red(`\n Configured repositories:`))
152
+ for (const rn of Object.keys(publishingConfig.repositories)) {
153
+ console.error(chalk.red(` - ${rn}`))
154
+ }
155
+ console.error('')
156
+ process.exit(1)
157
+ }
158
+
159
+ const sourceDir = resolve(matchedRepoConfig.sourceDir)
160
+ console.log(chalk.green(` āœ“ Matched repository: '${matchedRepoName}'`))
161
+ console.log(chalk.gray(` sourceDir: ${sourceDir}\n`))
162
+
163
+ // ── STEP 3: prepare — ensure sourceDir has no uncommitted changes ──
164
+ console.log('[t44] Preparing: checking for uncommitted changes ...\n')
165
+
166
+ const statusResult = await $`git status --porcelain`.cwd(sourceDir).quiet().nothrow()
167
+ const statusOutput = statusResult.text().trim()
168
+
169
+ if (statusOutput.length > 0) {
170
+ console.error(chalk.red(`\n[t44] ERROR: sourceDir has uncommitted changes. Commit or stash them first.`))
171
+ console.error(chalk.red(` sourceDir: ${sourceDir}`))
172
+ console.error(chalk.red(` Changes:\n`))
173
+ for (const line of statusOutput.split('\n').slice(0, 20)) {
174
+ console.error(chalk.red(` ${line}`))
175
+ }
176
+ if (statusOutput.split('\n').length > 20) {
177
+ console.error(chalk.red(` ... and ${statusOutput.split('\n').length - 20} more`))
178
+ }
179
+ console.error('')
180
+ process.exit(1)
181
+ }
182
+
183
+ console.log(chalk.green(` āœ“ sourceDir is clean\n`))
184
+
185
+ // ── STEP 4: Set up mirror repo ───────────────────────
186
+ // Reuse the publishing stage repo as a fast clone source if it exists.
187
+ // The mirror lives in its own ProjectPulling projection dir.
188
+
189
+ const pullingApi = {
190
+ getProjectionDir: (capsuleName: string) => join(
191
+ this.WorkspaceConfig.workspaceRootDir,
192
+ '.~o/workspace.foundation/@t44.sh~t44~caps~ProjectPulling',
193
+ capsuleName.replace(/\//g, '~')
194
+ ),
195
+ }
196
+
197
+ const ctx = {
198
+ parsedUrl,
199
+ matchedRepoName,
200
+ matchedRepoConfig,
201
+ sourceDir,
202
+ publishingConfig,
203
+ pullingApi,
204
+ metadata: {} as Record<string, any>,
205
+ }
206
+
207
+ // ── STEP 5: Call provider prepare + pull ─────────────
208
+ const provider = await getProvider(matchedProviderConfig.capsule)
209
+
210
+ if (typeof provider.prepare === 'function') {
211
+ console.log('[t44] Preparing mirror repository ...\n')
212
+ await provider.prepare({ config: matchedProviderConfig, ctx })
213
+ }
214
+
215
+ if (typeof provider.pull !== 'function') {
216
+ console.error(chalk.red(`\n[t44] ERROR: Provider '${matchedProviderConfig.capsule}' does not implement 'pull'.`))
217
+ process.exit(1)
218
+ }
219
+
220
+ console.log('[t44] Pulling changes ...\n')
221
+ await provider.pull({ config: matchedProviderConfig, ctx })
222
+
223
+ console.log(chalk.green('\n[t44] Pull complete!'))
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }, {
229
+ importMeta: import.meta,
230
+ importStack: makeImportStack(),
231
+ capsuleName: capsule['#'],
232
+ })
233
+ }
234
+ capsule['#'] = '@stream44.studio/t44/caps/ProjectPulling'
@@ -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
+ '#@stream44.studio/t44/structs/WorkspaceConfig': {
14
+ as: '$WorkspaceConfig'
15
+ },
16
+ '#@stream44.studio/t44/structs/ProjectRackConfig': {
17
+ as: '$ProjectRackConfig'
18
+ },
19
+ '#': {
20
+ WorkspacePrompt: {
21
+ type: CapsulePropertyTypes.Mapping,
22
+ value: '@stream44.studio/t44/caps/WorkspacePrompt'
23
+ },
24
+ HomeRegistry: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: '@stream44.studio/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 = '#@stream44.studio/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['#'] = '@stream44.studio/t44/caps/ProjectRack'