@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,257 @@
1
+ import * as path from 'path'
2
+ import * as fsPromises from 'fs/promises'
3
+ import { constants as fsConstants } from 'fs'
4
+ import { spawn } from 'bun'
5
+
6
+ export async function capsule({
7
+ encapsulate,
8
+ CapsulePropertyTypes,
9
+ makeImportStack
10
+ }: {
11
+ encapsulate: any
12
+ CapsulePropertyTypes: any
13
+ makeImportStack: any
14
+ }) {
15
+ return encapsulate({
16
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
17
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
18
+ '#': {
19
+ path: {
20
+ type: CapsulePropertyTypes.Constant,
21
+ value: path,
22
+ },
23
+
24
+ fs: {
25
+ type: CapsulePropertyTypes.Constant,
26
+ value: {
27
+ ...fsPromises,
28
+ ...fsConstants
29
+ },
30
+ },
31
+
32
+ spawnProcess: {
33
+ type: CapsulePropertyTypes.Function,
34
+ value: async function (this: any, options: any): Promise<any> {
35
+ const {
36
+ cmd,
37
+ cwd = process.cwd(),
38
+ waitForReady = false,
39
+ readySignal = 'READY',
40
+ waitForExit = false,
41
+ showOutput = false,
42
+ env = {},
43
+ verbose = false,
44
+ detached = false
45
+ } = options;
46
+
47
+ const outputData = { stdout: '', stderr: '' };
48
+ const mergedEnv = { ...process.env, ...env };
49
+
50
+ if (verbose && env.NODE_ENV) {
51
+ console.log('[spawnProcess] Setting NODE_ENV to:', env.NODE_ENV);
52
+ console.log('[spawnProcess] Merged env NODE_ENV:', mergedEnv.NODE_ENV);
53
+ }
54
+
55
+ const proc = spawn({
56
+ cmd,
57
+ cwd,
58
+ stdout: 'pipe',
59
+ stderr: 'pipe',
60
+ env: mergedEnv
61
+ });
62
+
63
+ if (detached && proc.unref) {
64
+ proc.unref();
65
+ }
66
+
67
+ let readySignalFound: (() => void) | null = null;
68
+ const readyPromise = waitForReady ? new Promise<void>((resolve) => {
69
+ readySignalFound = resolve;
70
+ }) : null;
71
+
72
+ if (proc.stdout && !detached) {
73
+ const reader = (proc.stdout as any).getReader();
74
+ const decoder = new TextDecoder();
75
+ (async () => {
76
+ try {
77
+ while (true) {
78
+ const { done, value } = await reader.read();
79
+ if (done) break;
80
+ const chunk = decoder.decode(value);
81
+ outputData.stdout += chunk;
82
+ if (waitForReady && readySignalFound && chunk.indexOf(readySignal) !== -1) {
83
+ readySignalFound();
84
+ readySignalFound = null;
85
+ }
86
+ if (showOutput || verbose) {
87
+ process.stdout.write(chunk);
88
+ }
89
+ }
90
+ } catch (e) {
91
+ // Stream closed
92
+ }
93
+ })();
94
+ }
95
+
96
+ if (proc.stderr && !detached) {
97
+ const reader = (proc.stderr as any).getReader();
98
+ const decoder = new TextDecoder();
99
+ (async () => {
100
+ try {
101
+ while (true) {
102
+ const { done, value } = await reader.read();
103
+ if (done) break;
104
+ const chunk = decoder.decode(value);
105
+ outputData.stderr += chunk;
106
+ if (waitForReady && readySignalFound && chunk.indexOf(readySignal) !== -1) {
107
+ readySignalFound();
108
+ readySignalFound = null;
109
+ }
110
+ if (showOutput || verbose) {
111
+ process.stderr.write(chunk);
112
+ }
113
+ }
114
+ } catch (e) {
115
+ // Stream closed
116
+ }
117
+ })();
118
+ }
119
+
120
+ if (waitForReady && readyPromise) {
121
+ await Promise.race([
122
+ readyPromise,
123
+ proc.exited.then(() => {
124
+ throw new Error(`Process exited with code ${proc.exitCode} before emitting ${readySignal} signal. stderr: ${outputData.stderr}`);
125
+ })
126
+ ]);
127
+ } else if (waitForExit) {
128
+ await proc.exited;
129
+ }
130
+
131
+ return {
132
+ process: proc,
133
+ stdout: outputData.stdout,
134
+ stderr: outputData.stderr,
135
+ exitCode: proc.exitCode ?? 0,
136
+ getStdout: () => outputData.stdout,
137
+ getStderr: () => outputData.stderr
138
+ };
139
+ }
140
+ },
141
+
142
+ runPackageScript: {
143
+ type: CapsulePropertyTypes.Function,
144
+ value: async function (this: any, options: any): Promise<any> {
145
+ const {
146
+ runtime = 'bun',
147
+ script,
148
+ args = [],
149
+ cwd,
150
+ env,
151
+ verbose = false
152
+ } = options;
153
+
154
+ const cmdArgs = [...args];
155
+ if (cmdArgs.length) {
156
+ cmdArgs.unshift('--')
157
+ }
158
+
159
+ const spawned = await this.spawnProcess({
160
+ cmd: [runtime, 'run', script, ...cmdArgs],
161
+ cwd,
162
+ waitForReady: false,
163
+ waitForExit: true,
164
+ env,
165
+ verbose
166
+ });
167
+
168
+ return {
169
+ exitCode: spawned.exitCode,
170
+ stdout: spawned.stdout,
171
+ stderr: spawned.stderr
172
+ };
173
+ }
174
+ },
175
+
176
+ waitForFetch: {
177
+ type: CapsulePropertyTypes.Function,
178
+ value: async function (this: any, options: any): Promise<boolean | Response> {
179
+ const {
180
+ url,
181
+ method = 'GET',
182
+ headers,
183
+ body,
184
+ status,
185
+ retryDelayMs = 1000,
186
+ requestTimeoutMs = 2000,
187
+ timeoutMs = 30000,
188
+ verbose = false,
189
+ returnResponse = false
190
+ } = options;
191
+
192
+ const startTime = Date.now();
193
+ let attemptCount = 0;
194
+
195
+ while (Date.now() - startTime < timeoutMs) {
196
+ attemptCount++;
197
+ const elapsed = Date.now() - startTime;
198
+
199
+ try {
200
+ const response = await fetch(url, {
201
+ method,
202
+ headers,
203
+ body,
204
+ signal: AbortSignal.timeout(requestTimeoutMs)
205
+ });
206
+
207
+ if (status === true) {
208
+ if (verbose) {
209
+ console.log(`[waitForFetch] URL ${url} responded (status: ${response.status}) after ${attemptCount} attempts (${elapsed}ms)`);
210
+ }
211
+ return returnResponse ? response : true;
212
+ } else if (typeof status === 'number') {
213
+ if (response.status === status) {
214
+ if (verbose) {
215
+ console.log(`[waitForFetch] URL ${url} responded with status ${status} after ${attemptCount} attempts (${elapsed}ms)`);
216
+ }
217
+ return returnResponse ? response : true;
218
+ } else {
219
+ if (verbose) {
220
+ console.log(`[waitForFetch] Attempt ${attemptCount}: Got status ${response.status}, expected ${status} (${elapsed}ms)`);
221
+ }
222
+ }
223
+ }
224
+ } catch (error) {
225
+ if (status === false) {
226
+ if (verbose) {
227
+ console.log(`[waitForFetch] URL ${url} is not responding (as expected) after ${attemptCount} attempts (${elapsed}ms)`);
228
+ }
229
+ return true;
230
+ } else {
231
+ if (verbose) {
232
+ console.log(`[waitForFetch] Attempt ${attemptCount}: Request failed (${elapsed}ms)`);
233
+ }
234
+ }
235
+ }
236
+
237
+ const remainingTime = timeoutMs - (Date.now() - startTime);
238
+ if (remainingTime > 0) {
239
+ await new Promise(resolve => setTimeout(resolve, Math.min(retryDelayMs, remainingTime)));
240
+ }
241
+ }
242
+
243
+ if (verbose) {
244
+ console.log(`[waitForFetch] Timeout reached after ${attemptCount} attempts (${Date.now() - startTime}ms)`);
245
+ }
246
+ return false;
247
+ }
248
+ },
249
+ }
250
+ }
251
+ }, {
252
+ importMeta: import.meta,
253
+ importStack: makeImportStack(),
254
+ capsuleName: capsule['#']
255
+ })
256
+ }
257
+ capsule['#'] = '@stream44.studio/t44/caps/ProjectTestLib'
@@ -0,0 +1,219 @@
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/RootKeyConfig': {
17
+ as: '$RootKeyConfig'
18
+ },
19
+ '#': {
20
+ Home: {
21
+ type: CapsulePropertyTypes.Mapping,
22
+ value: '@stream44.studio/t44/caps/Home'
23
+ },
24
+ WorkspacePrompt: {
25
+ type: CapsulePropertyTypes.Mapping,
26
+ value: '@stream44.studio/t44/caps/WorkspacePrompt'
27
+ },
28
+ ensureKey: {
29
+ type: CapsulePropertyTypes.Function,
30
+ value: async function (this: any): Promise<{ keyName: string; privateKeyPath: string; publicKey: string; keyFingerprint: string } | null> {
31
+ const { join } = await import('path')
32
+ const { stat: statFile } = await import('fs/promises')
33
+ const chalk = (await import('chalk')).default
34
+ const {
35
+ discoverEd25519Keys,
36
+ validateConfiguredKey,
37
+ ensurePassphrase,
38
+ ensureKeyInAgent,
39
+ computeFingerprint,
40
+ promptPassphrase,
41
+ generateEd25519Key
42
+ } = await import('../lib/key.js')
43
+
44
+ const workspaceConfig = await this.$WorkspaceConfig.config
45
+ const keyConfig = await this.$RootKeyConfig.config
46
+ const sshDir = await this.Home.sshDir
47
+
48
+ // --- Already configured: validate ---
49
+ if (keyConfig?.name && keyConfig?.privateKeyPath && keyConfig?.publicKey && keyConfig?.keyFingerprint) {
50
+ const valid = await validateConfiguredKey(keyConfig, 'Root Key')
51
+ if (!valid) {
52
+ return null
53
+ }
54
+ return {
55
+ keyName: keyConfig.name,
56
+ privateKeyPath: keyConfig.privateKeyPath,
57
+ publicKey: keyConfig.publicKey,
58
+ keyFingerprint: keyConfig.keyFingerprint
59
+ }
60
+ }
61
+
62
+ // --- Not configured: discover or create ---
63
+ console.log(chalk.cyan(`\n🔑 Root Key Setup\n`))
64
+ console.log(chalk.gray(` Workspace: ${workspaceConfig?.name || 'unknown'}`))
65
+ console.log(chalk.gray(` Root: ${workspaceConfig?.rootDir || 'unknown'}`))
66
+ console.log(chalk.gray(''))
67
+ console.log(chalk.gray(' The root key is an Ed25519 SSH key used to identify this workspace.'))
68
+ console.log(chalk.gray(` You can select an existing key from ${sshDir} or create a new one.`))
69
+ console.log(chalk.gray(''))
70
+
71
+ // Discover existing Ed25519 keys in ~/.ssh
72
+ const existingKeys = await discoverEd25519Keys(sshDir)
73
+
74
+ // Build choices
75
+ const choices: Array<{ name: string; value: any }> = []
76
+
77
+ for (const key of existingKeys) {
78
+ choices.push({
79
+ name: `${key.name} ${chalk.gray(key.publicKey.substring(0, 60) + '...')}`,
80
+ value: { type: 'existing', ...key }
81
+ })
82
+ }
83
+
84
+ choices.push({
85
+ name: chalk.yellow('+ Create a new Ed25519 key'),
86
+ value: { type: 'create' }
87
+ })
88
+
89
+ const selected = await this.WorkspacePrompt.select({
90
+ message: 'Select an Ed25519 key:',
91
+ choices,
92
+ defaultValue: { type: 'create' },
93
+ pageSize: 15
94
+ })
95
+
96
+ let keyName: string
97
+ let privateKeyPath: string
98
+ let publicKey: string
99
+ let keyFingerprint: string
100
+
101
+ if (selected.type === 'existing') {
102
+ keyName = selected.name
103
+ privateKeyPath = selected.privateKeyPath
104
+ publicKey = selected.publicKey
105
+ keyFingerprint = computeFingerprint(selected.privateKeyPath)
106
+
107
+ // Ensure selected existing key has a passphrase
108
+ const passphraseOk = await ensurePassphrase(privateKeyPath, keyName, 'Root key')
109
+ if (!passphraseOk) {
110
+ return null
111
+ }
112
+
113
+ // Add to ssh-agent
114
+ await ensureKeyInAgent(privateKeyPath, keyName, 'Root key')
115
+ } else {
116
+ // Prompt for key name
117
+ keyName = await this.WorkspacePrompt.input({
118
+ message: 'Enter a name for the new key:',
119
+ defaultValue: 'id_t44_ed25519',
120
+ validate: (input: string) => {
121
+ if (!input || input.trim().length === 0) {
122
+ return 'Key name cannot be empty'
123
+ }
124
+ if (!/^[a-zA-Z0-9_.-]+$/.test(input)) {
125
+ return 'Key name can only contain letters, numbers, underscores, dots, and hyphens'
126
+ }
127
+ return true
128
+ }
129
+ })
130
+
131
+ privateKeyPath = join(sshDir, keyName)
132
+
133
+ // Check if file already exists
134
+ let exists = false
135
+ try {
136
+ await statFile(privateKeyPath)
137
+ exists = true
138
+ } catch { }
139
+
140
+ if (exists) {
141
+ console.log(chalk.red(`\n ✗ File already exists: ${privateKeyPath}`))
142
+ console.log(chalk.red(` Choose a different name or select the existing key.\n`))
143
+ return null
144
+ }
145
+
146
+ // Prompt for passphrase
147
+ console.log(chalk.cyan(`\n Generating Ed25519 key '${keyName}'...`))
148
+ console.log(chalk.gray(` The key will be protected with a passphrase and added to the macOS Keychain.\n`))
149
+
150
+ const envPassphrase = process.env.T44_KEYS_PASSPHRASE
151
+ const passphrase = envPassphrase || await promptPassphrase()
152
+ if (!passphrase) {
153
+ console.log(chalk.red(`\n ✗ A passphrase is required for the root key.\n`))
154
+ return null
155
+ }
156
+
157
+ const result = await generateEd25519Key(privateKeyPath, passphrase, 't44-root-key')
158
+ if (!result) {
159
+ return null
160
+ }
161
+
162
+ publicKey = result.publicKey
163
+ keyFingerprint = result.keyFingerprint
164
+
165
+ console.log(chalk.green(` ✓ Key generated:`))
166
+ console.log(chalk.green(` ${privateKeyPath}`))
167
+ console.log(chalk.green(` ${privateKeyPath}.pub\n`))
168
+
169
+ // Add the new key to the ssh-agent with Keychain storage
170
+ if (!envPassphrase) {
171
+ await ensureKeyInAgent(privateKeyPath, keyName, 'Root key')
172
+ }
173
+ }
174
+
175
+ // Store in config
176
+ await this.$RootKeyConfig.setConfigValue(['name'], keyName)
177
+ await this.$RootKeyConfig.setConfigValue(['privateKeyPath'], privateKeyPath)
178
+ await this.$RootKeyConfig.setConfigValue(['publicKey'], publicKey)
179
+ await this.$RootKeyConfig.setConfigValue(['keyFingerprint'], keyFingerprint)
180
+
181
+ console.log(chalk.green(` ✓ Root key configured: ${keyName}`))
182
+ console.log(chalk.green(` ${keyFingerprint}\n`))
183
+
184
+ return { keyName, privateKeyPath, publicKey, keyFingerprint }
185
+ }
186
+ },
187
+ getKeyPath: {
188
+ type: CapsulePropertyTypes.Function,
189
+ value: async function (this: any): Promise<string | null> {
190
+ const keyConfig = await this.$RootKeyConfig.config
191
+
192
+ if (!keyConfig?.privateKeyPath) {
193
+ return null
194
+ }
195
+
196
+ return keyConfig.privateKeyPath
197
+ }
198
+ },
199
+ getPublicKey: {
200
+ type: CapsulePropertyTypes.Function,
201
+ value: async function (this: any): Promise<string | null> {
202
+ const keyConfig = await this.$RootKeyConfig.config
203
+
204
+ if (!keyConfig?.publicKey) {
205
+ return null
206
+ }
207
+
208
+ return keyConfig.publicKey
209
+ }
210
+ },
211
+ }
212
+ }
213
+ }, {
214
+ importMeta: import.meta,
215
+ importStack: makeImportStack(),
216
+ capsuleName: capsule['#'],
217
+ })
218
+ }
219
+ capsule['#'] = '@stream44.studio/t44/caps/RootKey'