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
package/lib/crypto.ts ADDED
@@ -0,0 +1,53 @@
1
+
2
+ import * as crypto from 'crypto'
3
+
4
+ /**
5
+ * Encrypt a string using AES-256-GCM with a key derived from the private key
6
+ */
7
+ export function encryptString(plaintext: string, privateKeyBase64: string): string {
8
+ // Derive a 32-byte symmetric key from the private key using SHA-256
9
+ const keyBytes = Buffer.from(privateKeyBase64, 'base64')
10
+ const symmetricKey = crypto.createHash('sha256').update(keyBytes).digest()
11
+
12
+ // Generate a random IV
13
+ const iv = crypto.randomBytes(16)
14
+
15
+ // Encrypt using AES-256-GCM
16
+ const cipher = crypto.createCipheriv('aes-256-gcm', symmetricKey, iv)
17
+ const encrypted = Buffer.concat([
18
+ cipher.update(plaintext, 'utf8'),
19
+ cipher.final()
20
+ ])
21
+ const authTag = cipher.getAuthTag()
22
+
23
+ // Combine IV + authTag + encrypted data and encode as base64
24
+ const combined = Buffer.concat([iv, authTag, encrypted])
25
+ return combined.toString('base64')
26
+ }
27
+
28
+ /**
29
+ * Decrypt a string using AES-256-GCM with a key derived from the private key
30
+ */
31
+ export function decryptString(ciphertext: string, privateKeyBase64: string): string {
32
+ // Derive the same symmetric key from the private key
33
+ const keyBytes = Buffer.from(privateKeyBase64, 'base64')
34
+ const symmetricKey = crypto.createHash('sha256').update(keyBytes).digest()
35
+
36
+ // Decode the combined data
37
+ const combined = Buffer.from(ciphertext, 'base64')
38
+
39
+ // Extract IV (16 bytes), authTag (16 bytes), and encrypted data
40
+ const iv = combined.subarray(0, 16)
41
+ const authTag = combined.subarray(16, 32)
42
+ const encrypted = combined.subarray(32)
43
+
44
+ // Decrypt using AES-256-GCM
45
+ const decipher = crypto.createDecipheriv('aes-256-gcm', symmetricKey, iv)
46
+ decipher.setAuthTag(authTag)
47
+ const decrypted = Buffer.concat([
48
+ decipher.update(encrypted),
49
+ decipher.final()
50
+ ])
51
+
52
+ return decrypted.toString('utf8')
53
+ }
package/lib/key.ts ADDED
@@ -0,0 +1,365 @@
1
+
2
+ import { execSync } from 'child_process'
3
+ import { readdir, readFile, stat } from 'fs/promises'
4
+ import { join } from 'path'
5
+
6
+ export interface Ed25519KeyInfo {
7
+ name: string
8
+ privateKeyPath: string
9
+ publicKey: string
10
+ }
11
+
12
+ export interface KeyConfig {
13
+ name: string
14
+ privateKeyPath: string
15
+ publicKey: string
16
+ keyFingerprint: string
17
+ }
18
+
19
+ /**
20
+ * Extract the SHA256 fingerprint from ssh-keygen -lf output.
21
+ * Output format: "256 SHA256:xxx comment (ED25519)"
22
+ */
23
+ export function extractFingerprint(sshKeygenOutput: string): string {
24
+ const match = sshKeygenOutput.match(/(SHA256:\S+)/)
25
+ return match ? match[1] : sshKeygenOutput
26
+ }
27
+
28
+ /**
29
+ * Compute the SHA256 fingerprint of a key file.
30
+ */
31
+ export function computeFingerprint(privateKeyPath: string): string {
32
+ return extractFingerprint(execSync(
33
+ `ssh-keygen -lf ${JSON.stringify(privateKeyPath)}`,
34
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
35
+ ).trim())
36
+ }
37
+
38
+ /**
39
+ * Check whether an Ed25519 private key has a passphrase set.
40
+ */
41
+ export function hasPassphrase(privateKeyPath: string): boolean {
42
+ try {
43
+ execSync(
44
+ `ssh-keygen -y -P "" -f ${JSON.stringify(privateKeyPath)}`,
45
+ { stdio: 'pipe' }
46
+ )
47
+ return false // empty passphrase worked → no passphrase
48
+ } catch {
49
+ return true // empty passphrase failed → has passphrase
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Discover existing Ed25519 keys in the given SSH directory.
55
+ */
56
+ export async function discoverEd25519Keys(sshDir: string): Promise<Ed25519KeyInfo[]> {
57
+ const dir = sshDir
58
+ const keys: Ed25519KeyInfo[] = []
59
+
60
+ let entries: string[]
61
+ try {
62
+ entries = await readdir(dir)
63
+ } catch {
64
+ return keys
65
+ }
66
+
67
+ const pubFiles = entries.filter(f => f.endsWith('.pub'))
68
+
69
+ for (const pubFile of pubFiles) {
70
+ try {
71
+ const pubPath = join(dir, pubFile)
72
+ const content = await readFile(pubPath, 'utf-8')
73
+ const trimmed = content.trim()
74
+
75
+ if (!trimmed.startsWith('ssh-ed25519 ')) {
76
+ continue
77
+ }
78
+
79
+ const privateName = pubFile.replace(/\.pub$/, '')
80
+ const privatePath = join(dir, privateName)
81
+
82
+ try {
83
+ await stat(privatePath)
84
+ } catch {
85
+ continue
86
+ }
87
+
88
+ keys.push({
89
+ name: privateName,
90
+ privateKeyPath: privatePath,
91
+ publicKey: trimmed
92
+ })
93
+ } catch {
94
+ // Skip unreadable files
95
+ }
96
+ }
97
+
98
+ return keys
99
+ }
100
+
101
+ /**
102
+ * Check if a key file exists at the given path.
103
+ */
104
+ export async function keyFileExists(privateKeyPath: string): Promise<boolean> {
105
+ try {
106
+ const s = await stat(privateKeyPath)
107
+ return s.isFile()
108
+ } catch {
109
+ return false
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Check if the key is loaded in the ssh-agent by fingerprint.
115
+ */
116
+ export function isKeyInAgent(privateKeyPath: string): boolean {
117
+ let keyFingerprint: string
118
+ try {
119
+ const fpOutput = execSync(
120
+ `ssh-keygen -lf ${JSON.stringify(privateKeyPath)}`,
121
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
122
+ ).trim()
123
+ const match = fpOutput.match(/(SHA256:\S+)/)
124
+ keyFingerprint = match ? match[1] : ''
125
+ } catch {
126
+ return false
127
+ }
128
+
129
+ if (!keyFingerprint) return false
130
+
131
+ try {
132
+ const agentKeys = execSync('ssh-add -l', {
133
+ encoding: 'utf-8',
134
+ stdio: ['pipe', 'pipe', 'pipe']
135
+ })
136
+ return agentKeys.includes(keyFingerprint)
137
+ } catch {
138
+ return false
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Add a key to the macOS ssh-agent with Keychain storage.
144
+ * Returns true if added successfully, false otherwise.
145
+ */
146
+ export function addKeyToAgent(privateKeyPath: string): boolean {
147
+ try {
148
+ execSync(
149
+ `ssh-add --apple-use-keychain ${JSON.stringify(privateKeyPath)}`,
150
+ { stdio: 'inherit' }
151
+ )
152
+ return true
153
+ } catch {
154
+ return false
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Ensure a key is loaded in the ssh-agent. Adds it if not already present.
160
+ * Logs status messages using chalk.
161
+ */
162
+ export async function ensureKeyInAgent(privateKeyPath: string, keyName: string, keyLabel: string): Promise<void> {
163
+ const chalk = (await import('chalk')).default
164
+
165
+ if (isKeyInAgent(privateKeyPath)) {
166
+ return
167
+ }
168
+
169
+ console.log(chalk.gray(`\n Adding ${keyLabel} '${keyName}' to ssh-agent (macOS Keychain) ...`))
170
+ if (addKeyToAgent(privateKeyPath)) {
171
+ console.log(chalk.green(` ✓ ${keyLabel} added to ssh-agent`))
172
+ console.log(chalk.gray(` Passphrase stored in macOS Keychain.\n`))
173
+ } else {
174
+ console.log(chalk.yellow(`\n ⚠ Could not add key to ssh-agent`))
175
+ console.log(chalk.yellow(` You may need to enter the passphrase manually when the key is used.\n`))
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Prompt for a passphrase using inquirer (password input with confirmation).
181
+ */
182
+ export async function promptPassphrase(): Promise<string | null> {
183
+ const inquirer = await import('inquirer')
184
+ const chalk = (await import('chalk')).default
185
+
186
+ try {
187
+ const { passphrase } = await inquirer.default.prompt([
188
+ {
189
+ type: 'password',
190
+ name: 'passphrase',
191
+ message: 'Enter a passphrase for the key:',
192
+ mask: '*',
193
+ validate: (input: string) => {
194
+ if (!input || input.length === 0) {
195
+ return 'Passphrase cannot be empty'
196
+ }
197
+ if (input.length < 5) {
198
+ return 'Passphrase must be at least 5 characters'
199
+ }
200
+ return true
201
+ }
202
+ }
203
+ ])
204
+
205
+ const { confirm } = await inquirer.default.prompt([
206
+ {
207
+ type: 'password',
208
+ name: 'confirm',
209
+ message: 'Confirm passphrase:',
210
+ mask: '*',
211
+ validate: (input: string) => {
212
+ if (input !== passphrase) {
213
+ return 'Passphrases do not match'
214
+ }
215
+ return true
216
+ }
217
+ }
218
+ ])
219
+
220
+ return passphrase
221
+ } catch (error: any) {
222
+ if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
223
+ console.log(chalk.red('\n\nABORTED\n'))
224
+ process.exit(0)
225
+ }
226
+ throw error
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Ensure a key has a passphrase. If not, prompt the user to set one.
232
+ */
233
+ export async function ensurePassphrase(privateKeyPath: string, keyName: string, keyLabel: string): Promise<boolean> {
234
+ const chalk = (await import('chalk')).default
235
+
236
+ if (hasPassphrase(privateKeyPath)) {
237
+ return true
238
+ }
239
+
240
+ console.log(chalk.yellow(`\n ⚠ ${keyLabel} '${keyName}' has no passphrase`))
241
+ console.log(chalk.gray(` A passphrase is required to protect the key. The passphrase will be stored`))
242
+ console.log(chalk.gray(` in the macOS Keychain so you won't need to enter it again.\n`))
243
+
244
+ const passphrase = await promptPassphrase()
245
+ if (!passphrase) {
246
+ console.log(chalk.red(`\n ✗ A passphrase is required for the ${keyLabel.toLowerCase()}.\n`))
247
+ return false
248
+ }
249
+
250
+ console.log(chalk.gray(`\n Setting passphrase on ${privateKeyPath} ...`))
251
+ try {
252
+ execSync(
253
+ `ssh-keygen -p -f ${JSON.stringify(privateKeyPath)} -P "" -N ${JSON.stringify(passphrase)}`,
254
+ { stdio: 'pipe' }
255
+ )
256
+ } catch (error: any) {
257
+ console.log(chalk.red(`\n ✗ Failed to set passphrase: ${error.message}\n`))
258
+ return false
259
+ }
260
+
261
+ console.log(chalk.green(` ✓ Passphrase set on ${keyLabel.toLowerCase()} '${keyName}'\n`))
262
+ return true
263
+ }
264
+
265
+ /**
266
+ * Generate a new Ed25519 SSH key with a passphrase.
267
+ * Returns the key info or null on failure.
268
+ */
269
+ export async function generateEd25519Key(
270
+ privateKeyPath: string,
271
+ passphrase: string,
272
+ comment: string
273
+ ): Promise<{ publicKey: string; keyFingerprint: string } | null> {
274
+ const chalk = (await import('chalk')).default
275
+
276
+ try {
277
+ execSync(
278
+ `ssh-keygen -t ed25519 -f ${JSON.stringify(privateKeyPath)} -N ${JSON.stringify(passphrase)} -C ${JSON.stringify(comment)}`,
279
+ { stdio: 'pipe' }
280
+ )
281
+ } catch (error: any) {
282
+ console.log(chalk.red(`\n ✗ Failed to generate key: ${error.message}\n`))
283
+ return null
284
+ }
285
+
286
+ const pubKeyContent = await readFile(privateKeyPath + '.pub', 'utf-8')
287
+ const publicKey = pubKeyContent.trim()
288
+ const keyFingerprint = computeFingerprint(privateKeyPath)
289
+
290
+ return { publicKey, keyFingerprint }
291
+ }
292
+
293
+ /**
294
+ * Validate that a configured key exists and its fingerprint matches.
295
+ * Returns true if valid, false otherwise (with error messages logged).
296
+ */
297
+ export async function validateConfiguredKey(
298
+ keyConfig: KeyConfig,
299
+ keyLabel: string
300
+ ): Promise<boolean> {
301
+ const chalk = (await import('chalk')).default
302
+
303
+ const exists = await keyFileExists(keyConfig.privateKeyPath)
304
+ if (!exists) {
305
+ console.log(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
306
+ console.log(chalk.red(`│ ✗ ${keyLabel} Error${' '.repeat(Math.max(0, 56 - keyLabel.length))}│`))
307
+ console.log(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
308
+ console.log(chalk.red(`│ The configured private key file is missing: │`))
309
+ console.log(chalk.red(`│ ${keyConfig.privateKeyPath}`))
310
+ console.log(chalk.red(`│ │`))
311
+ console.log(chalk.red(`│ The private key '${keyConfig.name}' has been removed or moved.`))
312
+ console.log(chalk.red(`│ Please restore the key file to the path above to proceed. │`))
313
+ console.log(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
314
+ return false
315
+ }
316
+
317
+ let currentFingerprint: string
318
+ try {
319
+ currentFingerprint = computeFingerprint(keyConfig.privateKeyPath)
320
+ } catch (error: any) {
321
+ console.log(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
322
+ console.log(chalk.red(`│ ✗ ${keyLabel} Error${' '.repeat(Math.max(0, 56 - keyLabel.length))}│`))
323
+ console.log(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
324
+ console.log(chalk.red(`│ Failed to compute fingerprint for the private key at: │`))
325
+ console.log(chalk.red(`│ ${keyConfig.privateKeyPath}`))
326
+ console.log(chalk.red(`│ │`))
327
+ console.log(chalk.red(`│ The key file may be corrupted. Please restore the original │`))
328
+ console.log(chalk.red(`│ key file to proceed. │`))
329
+ console.log(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
330
+ return false
331
+ }
332
+
333
+ if (currentFingerprint !== keyConfig.keyFingerprint) {
334
+ console.log(chalk.red(`\n┌─────────────────────────────────────────────────────────────────┐`))
335
+ console.log(chalk.red(`│ ✗ ${keyLabel} Mismatch${' '.repeat(Math.max(0, 53 - keyLabel.length))}│`))
336
+ console.log(chalk.red(`├─────────────────────────────────────────────────────────────────┤`))
337
+ console.log(chalk.red(`│ The private key at the configured path does not match the │`))
338
+ console.log(chalk.red(`│ fingerprint stored in the workspace config. │`))
339
+ console.log(chalk.red(`│ │`))
340
+ console.log(chalk.red(`│ Key name: ${keyConfig.name}`))
341
+ console.log(chalk.red(`│ Path: ${keyConfig.privateKeyPath}`))
342
+ console.log(chalk.red(`│ │`))
343
+ console.log(chalk.red(`│ The private key has changed. Please restore the original │`))
344
+ console.log(chalk.red(`│ private key that matches the configured fingerprint to proceed.│`))
345
+ console.log(chalk.red(`│ │`))
346
+ console.log(chalk.red(`│ Expected fingerprint: │`))
347
+ console.log(chalk.red(`│ ${keyConfig.keyFingerprint}`))
348
+ console.log(chalk.red(`│ │`))
349
+ console.log(chalk.red(`│ Current fingerprint: │`))
350
+ console.log(chalk.red(`│ ${currentFingerprint}`))
351
+ console.log(chalk.red(`└─────────────────────────────────────────────────────────────────┘\n`))
352
+ return false
353
+ }
354
+
355
+ if (!process.env.T44_KEYS_PASSPHRASE) {
356
+ const passphraseOk = await ensurePassphrase(keyConfig.privateKeyPath, keyConfig.name, keyLabel)
357
+ if (!passphraseOk) {
358
+ return false
359
+ }
360
+
361
+ await ensureKeyInAgent(keyConfig.privateKeyPath, keyConfig.name, keyLabel)
362
+ }
363
+
364
+ return true
365
+ }
@@ -0,0 +1,181 @@
1
+ import chalk from 'chalk'
2
+
3
+ export interface RenderOptions {
4
+ indent?: number
5
+ maxDepth?: number
6
+ currentDepth?: number
7
+ showTypes?: boolean
8
+ }
9
+
10
+ /**
11
+ * Generic schema-based console renderer that formats data based on JSON Schema properties
12
+ */
13
+ export class SchemaConsoleRenderer {
14
+ /**
15
+ * Render entity data based on its schema
16
+ */
17
+ static renderEntity(
18
+ data: any,
19
+ schema: any,
20
+ options: RenderOptions = {}
21
+ ): string {
22
+ const {
23
+ indent = 0,
24
+ maxDepth = 5,
25
+ currentDepth = 0,
26
+ showTypes = false
27
+ } = options
28
+
29
+ // maxDepth of -1 means unlimited depth
30
+ if (maxDepth !== -1 && currentDepth >= maxDepth) {
31
+ return chalk.gray(' '.repeat(indent) + '[max depth reached]')
32
+ }
33
+
34
+ const lines: string[] = []
35
+ const properties = schema?.properties || {}
36
+ const required = new Set(schema?.required || [])
37
+
38
+ // If data is primitive, render directly
39
+ if (typeof data !== 'object' || data === null) {
40
+ return chalk.yellow(String(data))
41
+ }
42
+
43
+ // Render each property based on schema
44
+ for (const [key, value] of Object.entries(data)) {
45
+ const propSchema = properties[key]
46
+ const isRequired = required.has(key)
47
+ const prefix = ' '.repeat(indent)
48
+
49
+ // Format key with required indicator
50
+ const keyDisplay = isRequired
51
+ ? chalk.bold.cyan(key)
52
+ : chalk.cyan(key)
53
+
54
+ // Add type annotation if requested
55
+ const typeAnnotation = showTypes && propSchema?.type
56
+ ? chalk.gray(` (${propSchema.type})`)
57
+ : ''
58
+
59
+ // Render value based on type
60
+ if (value === null || value === undefined) {
61
+ lines.push(`${prefix}${keyDisplay}${typeAnnotation}: ${chalk.gray('null')}`)
62
+ } else if (Array.isArray(value)) {
63
+ lines.push(`${prefix}${keyDisplay}${typeAnnotation}: ${chalk.gray(`[${value.length} items]`)}`)
64
+
65
+ // Render array items if not too deep
66
+ if ((maxDepth === -1 || currentDepth < maxDepth - 1) && value.length > 0) {
67
+ const itemSchema = propSchema?.items
68
+ value.forEach((item, idx) => {
69
+ if (typeof item === 'object' && item !== null) {
70
+ lines.push(`${prefix} ${chalk.gray(`${idx}`)}:`)
71
+ lines.push(this.renderEntity(item, itemSchema, {
72
+ indent: indent + 3,
73
+ maxDepth,
74
+ currentDepth: currentDepth + 1,
75
+ showTypes
76
+ }))
77
+ } else {
78
+ lines.push(`${prefix} ${chalk.gray(`${idx}`)}: ${this.formatValue(item, propSchema)}`)
79
+ }
80
+ })
81
+ }
82
+ } else if (typeof value === 'object') {
83
+ const description = propSchema?.description
84
+ const descSuffix = description ? chalk.gray(` // ${description}`) : ''
85
+ lines.push(`${prefix}${keyDisplay}${typeAnnotation}:${descSuffix}`)
86
+
87
+ // Recursively render nested object
88
+ lines.push(this.renderEntity(value, propSchema, {
89
+ indent: indent + 1,
90
+ maxDepth,
91
+ currentDepth: currentDepth + 1,
92
+ showTypes
93
+ }))
94
+ } else {
95
+ const description = propSchema?.description
96
+ const descSuffix = description ? chalk.gray(` // ${description}`) : ''
97
+ lines.push(`${prefix}${keyDisplay}${typeAnnotation}: ${this.formatValue(value, propSchema)}${descSuffix}`)
98
+ }
99
+ }
100
+
101
+ return lines.join('\n')
102
+ }
103
+
104
+ /**
105
+ * Format a primitive value based on its schema
106
+ */
107
+ private static formatValue(value: any, schema?: any): string {
108
+ if (value === null || value === undefined) {
109
+ return chalk.gray('null')
110
+ }
111
+
112
+ const type = schema?.type || typeof value
113
+ const format = schema?.format
114
+
115
+ switch (type) {
116
+ case 'string':
117
+ if (format === 'date-time') {
118
+ return chalk.green(value)
119
+ } else if (format === 'uri' || format === 'url') {
120
+ return chalk.blue.underline(value)
121
+ } else if (schema?.enum) {
122
+ return chalk.magenta(value)
123
+ }
124
+ return chalk.yellow(JSON.stringify(value))
125
+
126
+ case 'number':
127
+ case 'integer':
128
+ return chalk.cyan(String(value))
129
+
130
+ case 'boolean':
131
+ return value ? chalk.green('true') : chalk.red('false')
132
+
133
+ default:
134
+ return chalk.yellow(JSON.stringify(value))
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Render a summary line for an entity
140
+ */
141
+ static renderSummary(
142
+ entityName: string,
143
+ data: any,
144
+ schema: any
145
+ ): string {
146
+ const properties = schema?.properties || {}
147
+ const parts: string[] = []
148
+
149
+ // Try to find key identifying properties
150
+ const identifiers = ['name', 'identifier', 'id', 'title']
151
+ for (const key of identifiers) {
152
+ if (data[key] && properties[key]) {
153
+ parts.push(chalk.yellow(data[key]))
154
+ break
155
+ }
156
+ }
157
+
158
+ // Add status if present
159
+ if (data.status && properties.status) {
160
+ const statusColor = data.status === 'READY' ? chalk.green :
161
+ data.status === 'ERROR' ? chalk.red :
162
+ chalk.yellow
163
+ parts.push(statusColor(`[${data.status}]`))
164
+ }
165
+
166
+ return parts.length > 0 ? parts.join(' ') : ''
167
+ }
168
+
169
+ /**
170
+ * Render validation errors
171
+ */
172
+ static renderErrors(errors: any[]): string {
173
+ if (errors.length === 0) return ''
174
+
175
+ const lines: string[] = [chalk.red.bold('Validation Errors:')]
176
+ for (const err of errors) {
177
+ lines.push(chalk.red(` ${err.path}: ${err.message}`))
178
+ }
179
+ return lines.join('\n')
180
+ }
181
+ }