t44 0.4.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yml +12 -0
  3. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  4. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity.yaml +25 -0
  6. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  7. package/DCO.md +34 -0
  8. package/LICENSE.md +203 -0
  9. package/README.md +183 -0
  10. package/bin/activate +36 -0
  11. package/bin/activate.ts +30 -0
  12. package/bin/postinstall.sh +19 -0
  13. package/bin/shell +27 -0
  14. package/bin/t44 +27 -0
  15. package/caps/ConfigSchemaStruct.ts +55 -0
  16. package/caps/Home.ts +51 -0
  17. package/caps/HomeRegistry.ts +313 -0
  18. package/caps/HomeRegistryFile.ts +144 -0
  19. package/caps/JsonSchemas.ts +220 -0
  20. package/caps/OpenApiSchema.ts +67 -0
  21. package/caps/PackageDescriptor.ts +88 -0
  22. package/caps/ProjectCatalogs.ts +153 -0
  23. package/caps/ProjectDeployment.ts +363 -0
  24. package/caps/ProjectDevelopment.ts +257 -0
  25. package/caps/ProjectPublishing.ts +522 -0
  26. package/caps/ProjectRack.ts +155 -0
  27. package/caps/ProjectRepository.ts +322 -0
  28. package/caps/RootKey.ts +219 -0
  29. package/caps/SigningKey.ts +243 -0
  30. package/caps/WorkspaceCli.ts +442 -0
  31. package/caps/WorkspaceConfig.ts +268 -0
  32. package/caps/WorkspaceConfig.yaml +71 -0
  33. package/caps/WorkspaceConfigFile.ts +799 -0
  34. package/caps/WorkspaceConnection.ts +249 -0
  35. package/caps/WorkspaceEntityConfig.ts +78 -0
  36. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  37. package/caps/WorkspaceEntityFact.ts +218 -0
  38. package/caps/WorkspaceInfo.ts +595 -0
  39. package/caps/WorkspaceInit.ts +30 -0
  40. package/caps/WorkspaceKey.ts +338 -0
  41. package/caps/WorkspaceModel.ts +373 -0
  42. package/caps/WorkspaceProjects.ts +636 -0
  43. package/caps/WorkspacePrompt.ts +406 -0
  44. package/caps/WorkspaceShell.sh +39 -0
  45. package/caps/WorkspaceShell.ts +104 -0
  46. package/caps/WorkspaceShell.yaml +64 -0
  47. package/caps/WorkspaceShellCli.ts +109 -0
  48. package/caps/WorkspaceTest.ts +167 -0
  49. package/caps/providers/README.md +2 -0
  50. package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
  51. package/caps/providers/bunny.net/api-pull.test.ts +319 -0
  52. package/caps/providers/bunny.net/api-pull.ts +164 -0
  53. package/caps/providers/bunny.net/api-storage.test.ts +168 -0
  54. package/caps/providers/bunny.net/api-storage.ts +248 -0
  55. package/caps/providers/bunny.net/api.ts +95 -0
  56. package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
  57. package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
  58. package/caps/providers/dynadot.com/api-domains.ts +169 -0
  59. package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
  60. package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
  61. package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
  62. package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
  63. package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
  64. package/caps/providers/github.com/ProjectPublishing.ts +118 -0
  65. package/caps/providers/github.com/api.ts +115 -0
  66. package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
  67. package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
  68. package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
  69. package/caps/providers/vercel.com/api.test.ts +67 -0
  70. package/caps/providers/vercel.com/api.ts +132 -0
  71. package/caps/providers/vercel.com/bun.lock +194 -0
  72. package/caps/providers/vercel.com/package.json +10 -0
  73. package/caps/providers/vercel.com/project.test.ts +108 -0
  74. package/caps/providers/vercel.com/project.ts +150 -0
  75. package/caps/providers/vercel.com/tsconfig.json +28 -0
  76. package/docs/Overview.drawio +248 -0
  77. package/docs/Overview.svg +4 -0
  78. package/lib/crypto.ts +53 -0
  79. package/lib/key.ts +365 -0
  80. package/lib/schema-console-renderer.ts +181 -0
  81. package/lib/schema-resolver.ts +349 -0
  82. package/lib/ucan.ts +137 -0
  83. package/package.json +101 -0
  84. package/structs/HomeRegistry.ts +55 -0
  85. package/structs/HomeRegistryConfig.ts +56 -0
  86. package/structs/ProjectCatalogsConfig.ts +53 -0
  87. package/structs/ProjectDeploymentConfig.ts +56 -0
  88. package/structs/ProjectDeploymentFact.ts +106 -0
  89. package/structs/ProjectPublishingFact.ts +68 -0
  90. package/structs/ProjectRack.ts +51 -0
  91. package/structs/ProjectRackConfig.ts +56 -0
  92. package/structs/RepositoryOriginDescriptor.ts +51 -0
  93. package/structs/RootKeyConfig.ts +64 -0
  94. package/structs/SigningKeyConfig.ts +64 -0
  95. package/structs/Workspace.ts +56 -0
  96. package/structs/WorkspaceCatalogs.ts +56 -0
  97. package/structs/WorkspaceCliConfig.ts +53 -0
  98. package/structs/WorkspaceConfig.ts +64 -0
  99. package/structs/WorkspaceConfigFile.ts +50 -0
  100. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  101. package/structs/WorkspaceKey.ts +55 -0
  102. package/structs/WorkspaceKeyConfig.ts +56 -0
  103. package/structs/WorkspaceMappingsConfig.ts +56 -0
  104. package/structs/WorkspaceProject.ts +104 -0
  105. package/structs/WorkspaceProjectsConfig.ts +67 -0
  106. package/structs/WorkspacePublishingConfig.ts +65 -0
  107. package/structs/WorkspaceShellConfig.ts +83 -0
  108. package/structs/providers/README.md +2 -0
  109. package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
  110. package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
  111. package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
  112. package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
  113. package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
  114. package/structs/providers/dynadot.com/DomainFact.ts +46 -0
  115. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
  116. package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
  117. package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
  118. package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
  119. package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
  120. package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
  121. package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
  122. package/tests/01-Lifecycle/main.test.ts +173 -0
  123. package/tsconfig.json +28 -0
  124. package/workspace-rt.ts +134 -0
  125. package/workspace.yaml +3 -0
@@ -0,0 +1,442 @@
1
+
2
+ import { Command } from 'commander'
3
+ import { $ } from 'bun'
4
+
5
+ export async function capsule({
6
+ encapsulate,
7
+ CapsulePropertyTypes,
8
+ makeImportStack
9
+ }: {
10
+ encapsulate: any
11
+ CapsulePropertyTypes: any
12
+ makeImportStack: any
13
+ }) {
14
+ return encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
17
+ '#t44/structs/WorkspaceCliConfig': {
18
+ as: '$config'
19
+ },
20
+ '#': {
21
+ WorkspaceConfig: {
22
+ type: CapsulePropertyTypes.Mapping,
23
+ value: 't44/caps/WorkspaceConfig'
24
+ },
25
+ WorkspaceKey: {
26
+ type: CapsulePropertyTypes.Mapping,
27
+ value: 't44/caps/WorkspaceKey'
28
+ },
29
+ ProjectRack: {
30
+ type: CapsulePropertyTypes.Mapping,
31
+ value: 't44/caps/ProjectRack'
32
+ },
33
+ WorkspaceShell: {
34
+ type: CapsulePropertyTypes.Mapping,
35
+ value: 't44/caps/WorkspaceShell'
36
+ },
37
+ ProjectDeployment: {
38
+ type: CapsulePropertyTypes.Mapping,
39
+ value: 't44/caps/ProjectDeployment'
40
+ },
41
+ ProjectPublishing: {
42
+ type: CapsulePropertyTypes.Mapping,
43
+ value: 't44/caps/ProjectPublishing'
44
+ },
45
+ ProjectDevelopment: {
46
+ type: CapsulePropertyTypes.Mapping,
47
+ value: 't44/caps/ProjectDevelopment'
48
+ },
49
+ WorkspaceInit: {
50
+ type: CapsulePropertyTypes.Mapping,
51
+ value: 't44/caps/WorkspaceInit'
52
+ },
53
+ WorkspaceInfo: {
54
+ type: CapsulePropertyTypes.Mapping,
55
+ value: 't44/caps/WorkspaceInfo'
56
+ },
57
+ WorkspaceModel: {
58
+ type: CapsulePropertyTypes.Mapping,
59
+ value: 't44/caps/WorkspaceModel'
60
+ },
61
+ WorkspacePrompt: {
62
+ type: CapsulePropertyTypes.Mapping,
63
+ value: 't44/caps/WorkspacePrompt'
64
+ },
65
+ HomeRegistry: {
66
+ type: CapsulePropertyTypes.Mapping,
67
+ value: 't44/caps/HomeRegistry'
68
+ },
69
+ RootKey: {
70
+ type: CapsulePropertyTypes.Mapping,
71
+ value: 't44/caps/RootKey'
72
+ },
73
+ SigningKey: {
74
+ type: CapsulePropertyTypes.Mapping,
75
+ value: 't44/caps/SigningKey'
76
+ },
77
+ WorkspaceProjects: {
78
+ type: CapsulePropertyTypes.Mapping,
79
+ value: 't44/caps/WorkspaceProjects'
80
+ },
81
+ ProjectCatalogs: {
82
+ type: CapsulePropertyTypes.Mapping,
83
+ value: 't44/caps/ProjectCatalogs'
84
+ },
85
+ cliOptions: {
86
+ type: CapsulePropertyTypes.Literal,
87
+ value: { yes: false }
88
+ },
89
+ jsApi: {
90
+ type: CapsulePropertyTypes.GetterFunction,
91
+ value: async function (this: any): Promise<object> {
92
+
93
+ const config = await this.WorkspaceConfig.config as any
94
+
95
+ const api: Record<string, any> = {}
96
+ for (const propertyName in config.javascript.api) {
97
+ api[propertyName] = config.javascript.api[propertyName]
98
+ }
99
+ return api
100
+ }
101
+ },
102
+ cliCommands: {
103
+ type: CapsulePropertyTypes.GetterFunction,
104
+ value: async function (this: any): Promise<object> {
105
+
106
+ const cliConfig = await this.$config.config
107
+ const self = this
108
+
109
+ const commands: Record<string, (commandArgs?: any) => Promise<void>> = {}
110
+ for (const commandName in cliConfig.cli.commands) {
111
+ const commandConfig = cliConfig.cli.commands[commandName]
112
+
113
+ commands[commandName] = async function (commandArgs?: any) {
114
+
115
+ const { cmd, capsule } = commandConfig
116
+
117
+ if (capsule) {
118
+ // TODO: Dynamically load capsule
119
+ if (capsule === 't44/caps/ProjectDeployment') {
120
+ await self.ProjectDeployment.run({ args: commandArgs })
121
+ } else if (capsule === 't44/caps/ProjectPublishing') {
122
+ await self.ProjectPublishing.run({ args: commandArgs })
123
+ } else if (capsule === 't44/caps/WorkspaceShell') {
124
+ await self.WorkspaceShell.run({ args: commandArgs })
125
+ } else if (capsule === 't44/caps/ProjectDevelopment') {
126
+ await self.ProjectDevelopment.run({ args: commandArgs })
127
+ } else if (capsule === 't44/caps/WorkspaceInit') {
128
+ await self.WorkspaceInit.run({ args: commandArgs })
129
+ } else if (capsule === 't44/caps/WorkspaceInfo') {
130
+ await self.WorkspaceInfo.run({ args: commandArgs })
131
+ } else if (capsule === 't44/caps/WorkspaceModel') {
132
+ const full = commandArgs?.full || false
133
+ const entitySelector = commandArgs?.entitySelector
134
+ await self.WorkspaceModel.run({ full, entitySelector })
135
+ } else {
136
+ throw new Error(`Unsupported capsule '${capsule}'!`)
137
+ }
138
+ } else if (cmd) {
139
+ await $`sh -c ${cmd}`.cwd(self.WorkspaceConfig.workspaceRootDir)
140
+ }
141
+ }
142
+ }
143
+ return commands
144
+ }
145
+ },
146
+ validateIdentities: {
147
+ type: CapsulePropertyTypes.Function,
148
+ value: async function (this: any): Promise<boolean> {
149
+ const chalk = (await import('chalk')).default
150
+
151
+ const fullConfig = await this.WorkspaceConfig.config
152
+ const wsConfigStructKey = '#t44/structs/WorkspaceConfig'
153
+ const keyConfigStructKey = '#t44/structs/WorkspaceKeyConfig'
154
+ const rackConfigStructKey = '#t44/structs/ProjectRackConfig'
155
+ const homeRegistryConfigStructKey = '#t44/structs/HomeRegistryConfig'
156
+
157
+ const ws = fullConfig?.[wsConfigStructKey]
158
+ const keyConfig = fullConfig?.[keyConfigStructKey]
159
+ const rackConfig = fullConfig?.[rackConfigStructKey]
160
+ const homeRegistryConfig = fullConfig?.[homeRegistryConfigStructKey]
161
+
162
+ if (!ws) return true
163
+
164
+ // --- Home Registry identity ---
165
+ if (homeRegistryConfig?.rootDir) {
166
+ const registryData = await this.HomeRegistry.getRegistry()
167
+ const registryPath = await this.HomeRegistry.getRegistryPath()
168
+
169
+ if (registryData) {
170
+ if (homeRegistryConfig.identifier) {
171
+ if (registryData.did !== homeRegistryConfig.identifier) {
172
+ console.log(chalk.red(`\n✗ Home Registry Identity Mismatch\n`))
173
+ console.log(chalk.red(` The home registry identifier in your config does not match the registry.\n`))
174
+ console.log(chalk.white(` Config identifier:`))
175
+ console.log(chalk.white(` ${homeRegistryConfig.identifier}`))
176
+ console.log(chalk.white(` Registry identifier (${registryPath}):`))
177
+ console.log(chalk.white(` ${registryData.did}\n`))
178
+ console.log(chalk.red(` To fix this, either:`))
179
+ console.log(chalk.red(` • Update the identifier in your workspace config to match the registry`))
180
+ console.log(chalk.red(` • Or delete the relevant config fields and re-run to set up fresh\n`))
181
+ return false
182
+ }
183
+ } else {
184
+ // rootDir set but no identifier — adopt from registry
185
+ await this.WorkspaceConfig.setConfigValue([homeRegistryConfigStructKey, 'identifier'], registryData.did)
186
+ console.log(chalk.green(` ✓ Adopted home registry identity from registry`))
187
+ console.log(chalk.green(` DID: ${registryData.did}\n`))
188
+ }
189
+ }
190
+ }
191
+
192
+ // --- Workspace identity ---
193
+ if (ws.name) {
194
+ const registryData = await this.HomeRegistry.getWorkspace(ws.name)
195
+ const registryPath = await this.HomeRegistry.getWorkspacePath(ws.name)
196
+
197
+ if (registryData) {
198
+ if (ws.identifier) {
199
+ // Validate: config identifier must match registry
200
+ if (registryData.did !== ws.identifier) {
201
+ console.log(chalk.red(`\n✗ Workspace Identity Mismatch\n`))
202
+ console.log(chalk.red(` The identifier in your workspace config does not match the registry.\n`))
203
+ console.log(chalk.white(` Config identifier:`))
204
+ console.log(chalk.white(` ${ws.identifier}`))
205
+ console.log(chalk.white(` Registry identifier (${registryPath}):`))
206
+ console.log(chalk.white(` ${registryData.did}\n`))
207
+ console.log(chalk.red(` This can happen if the registry file was regenerated or the config was manually edited.`))
208
+ console.log(chalk.red(` To fix this, either:`))
209
+ console.log(chalk.red(` • Update the identifier in your workspace config to match the registry`))
210
+ console.log(chalk.red(` • Or delete the relevant config fields and re-run to set up fresh\n`))
211
+ return false
212
+ }
213
+ // Validate: rootDir must match
214
+ if (registryData.workspaceRootDir && registryData.workspaceRootDir !== ws.rootDir) {
215
+ console.log(chalk.red(`\n✗ Workspace Root Directory Mismatch\n`))
216
+ console.log(chalk.red(` The workspace "${ws.name}" is registered to a different directory.\n`))
217
+ console.log(chalk.white(` Config rootDir:`))
218
+ console.log(chalk.white(` ${ws.rootDir}`))
219
+ console.log(chalk.white(` Registry rootDir (${registryPath}):`))
220
+ console.log(chalk.white(` ${registryData.workspaceRootDir}\n`))
221
+ console.log(chalk.red(` A workspace can only be connected to one directory.\n`))
222
+ return false
223
+ }
224
+ } else {
225
+ // Name set but no identifier — check rootDir and adopt if matching
226
+ if (registryData.workspaceRootDir && registryData.workspaceRootDir !== ws.rootDir) {
227
+ console.log(chalk.red(`\n✗ Workspace "${ws.name}" Cannot Be Adopted\n`))
228
+ console.log(chalk.red(` The workspace "${ws.name}" is registered to a different directory.\n`))
229
+ console.log(chalk.white(` Your rootDir:`))
230
+ console.log(chalk.white(` ${ws.rootDir}`))
231
+ console.log(chalk.white(` Registry rootDir (${registryPath}):`))
232
+ console.log(chalk.white(` ${registryData.workspaceRootDir}\n`))
233
+ console.log(chalk.red(` A workspace can only be connected to one directory.`))
234
+ console.log(chalk.red(` Choose a different workspace name or update the registry.\n`))
235
+ return false
236
+ }
237
+ // rootDir matches — adopt identity
238
+ await this.WorkspaceConfig.setConfigValue([wsConfigStructKey, 'identifier'], registryData.did)
239
+ console.log(chalk.green(` ✓ Adopted workspace identity for "${ws.name}" from registry`))
240
+ console.log(chalk.green(` DID: ${registryData.did}\n`))
241
+ }
242
+ }
243
+ }
244
+
245
+ // --- Key identity ---
246
+ if (keyConfig?.name) {
247
+ const registryData = await this.HomeRegistry.getKey(keyConfig.name)
248
+ const registryPath = await this.HomeRegistry.getKeyPath(keyConfig.name)
249
+
250
+ if (registryData) {
251
+ if (keyConfig.identifier) {
252
+ if (registryData.did !== keyConfig.identifier) {
253
+ console.log(chalk.red(`\n✗ Workspace Key Identity Mismatch\n`))
254
+ console.log(chalk.red(` The key identifier in your config does not match the registry.\n`))
255
+ console.log(chalk.white(` Config identifier:`))
256
+ console.log(chalk.white(` ${keyConfig.identifier}`))
257
+ console.log(chalk.white(` Registry identifier (${registryPath}):`))
258
+ console.log(chalk.white(` ${registryData.did}\n`))
259
+ console.log(chalk.red(` To fix this, either:`))
260
+ console.log(chalk.red(` • Update the identifier in your workspace config to match the registry`))
261
+ console.log(chalk.red(` • Or delete the relevant config fields and re-run to set up fresh\n`))
262
+ return false
263
+ }
264
+ } else {
265
+ // Name set but no identifier — adopt from registry
266
+ await this.WorkspaceConfig.setConfigValue([keyConfigStructKey, 'identifier'], registryData.did)
267
+ console.log(chalk.green(` ✓ Adopted key identity for "${keyConfig.name}" from registry`))
268
+ console.log(chalk.green(` DID: ${registryData.did}\n`))
269
+ }
270
+ }
271
+ }
272
+
273
+ // --- Project Rack identity ---
274
+ if (rackConfig?.name) {
275
+ const registryData = await this.HomeRegistry.getRack(rackConfig.name)
276
+ const registryPath = await this.HomeRegistry.getRackPath(rackConfig.name)
277
+
278
+ if (registryData) {
279
+ if (rackConfig.identifier) {
280
+ if (registryData.did !== rackConfig.identifier) {
281
+ console.log(chalk.red(`\n✗ Project Rack Identity Mismatch\n`))
282
+ console.log(chalk.red(` The rack identifier in your config does not match the registry.\n`))
283
+ console.log(chalk.white(` Config identifier:`))
284
+ console.log(chalk.white(` ${rackConfig.identifier}`))
285
+ console.log(chalk.white(` Registry identifier (${registryPath}):`))
286
+ console.log(chalk.white(` ${registryData.did}\n`))
287
+ console.log(chalk.red(` To fix this, either:`))
288
+ console.log(chalk.red(` • Update the identifier in your workspace config to match the registry`))
289
+ console.log(chalk.red(` • Or delete the relevant config fields and re-run to set up fresh\n`))
290
+ return false
291
+ }
292
+ } else {
293
+ // Name set but no identifier — adopt from registry
294
+ await this.WorkspaceConfig.setConfigValue([rackConfigStructKey, 'identifier'], registryData.did)
295
+ console.log(chalk.green(` ✓ Adopted project rack identity for "${rackConfig.name}" from registry`))
296
+ console.log(chalk.green(` DID: ${registryData.did}\n`))
297
+ }
298
+ }
299
+ }
300
+
301
+ return true
302
+ }
303
+ },
304
+ runCli: {
305
+ type: CapsulePropertyTypes.Function,
306
+ value: async function (this: any, argv: string[]): Promise<void> {
307
+
308
+ const program = new Command()
309
+ .option('--trace', 'Detailed logging for debugging and performance tuning.')
310
+ .option('--yes', 'Confirm all questions with default values.')
311
+ .option('--now', 'Fetch fresh data instead of using cached values.')
312
+
313
+ // Check for flags without parsing (to avoid consuming argv)
314
+ const hasYesFlag = argv.includes('--yes')
315
+ const hasNowFlag = argv.includes('--now')
316
+
317
+ // Set cliOptions for use by other capsules
318
+ this.cliOptions = { yes: hasYesFlag, now: hasNowFlag }
319
+ this.WorkspacePrompt.cliOptions = { yes: hasYesFlag }
320
+
321
+ // Ensure workspace config base fields (rootDir, rootConfigFilepath)
322
+ await this.WorkspaceConfig.ensureConfigBase()
323
+
324
+ // Ensure home registry directory is configured
325
+ await this.HomeRegistry.ensureRootDir()
326
+
327
+ // Ensure workspace identity fields (name, identifier) — requires registry
328
+ await this.WorkspaceConfig.ensureConfigIdentity()
329
+
330
+ // Validate identities: adopt from registry if only name is set, halt on mismatch
331
+ const identityValid = await this.validateIdentities()
332
+ if (!identityValid) return
333
+
334
+ // Ensure root key is configured and valid
335
+ const rootKey = await this.RootKey.ensureKey()
336
+ if (!rootKey) return
337
+
338
+ // Ensure signing key is configured and valid
339
+ const signingKey = await this.SigningKey.ensureKey()
340
+ if (!signingKey) return
341
+
342
+ // Ensure workspace key is configured
343
+ await this.WorkspaceKey.ensureKey()
344
+
345
+ // Ensure project rack is configured
346
+ await this.ProjectRack.ensureRack()
347
+
348
+ // Ensure project identifiers exist in package.json descriptors
349
+ await this.WorkspaceProjects.ensureIdentifiers()
350
+
351
+ // Validate project catalogs configuration
352
+ const catalogsValid = await this.ProjectCatalogs.validate()
353
+ if (!catalogsValid) return
354
+
355
+ const cliConfig = await this.$config.config
356
+ const cliCommands = await this.cliCommands as Record<string, (args?: any) => Promise<void>>
357
+
358
+ for (const commandName in cliConfig.cli.commands) {
359
+ const commandConfig = cliConfig.cli.commands[commandName]
360
+ const { description, arguments: commandArgs } = commandConfig
361
+
362
+ const cmd = program
363
+ .command(commandName)
364
+ .description(description || '')
365
+
366
+ // Add arguments if defined
367
+ if (commandArgs) {
368
+ for (const argName in commandArgs) {
369
+ const argConfig = commandArgs[argName]
370
+ const argSyntax = argConfig.optional ? `[${argName}]` : `<${argName}>`
371
+ cmd.argument(argSyntax, argConfig.description || '')
372
+ }
373
+ }
374
+
375
+ // Add options if defined
376
+ const commandOptions = commandConfig.options
377
+ if (commandOptions) {
378
+ for (const optionName in commandOptions) {
379
+ const optionConfig = commandOptions[optionName]
380
+ if (optionConfig.value === 'optional') {
381
+ cmd.option(`--${optionName} [value]`, optionConfig.description || '')
382
+ } else if (optionConfig.value === 'required') {
383
+ cmd.option(`--${optionName} <value>`, optionConfig.description || '')
384
+ } else {
385
+ cmd.option(`--${optionName}`, optionConfig.description || '')
386
+ }
387
+ }
388
+ }
389
+
390
+ cmd.action(async function (...actionArgs) {
391
+ // Helper to convert hyphenated names to camelCase (e.g., 'dangerously-squash' -> 'dangerouslySquash')
392
+ const toCamelCase = (str: string) => {
393
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
394
+ }
395
+
396
+ // Extract argument values (last arg is the command object)
397
+ const commandObj = actionArgs[actionArgs.length - 1]
398
+ const argValues: any = {}
399
+
400
+ if (commandArgs) {
401
+ const argNames = Object.keys(commandArgs)
402
+ argNames.forEach((name, index) => {
403
+ argValues[name] = actionArgs[index]
404
+ })
405
+ }
406
+
407
+ // Extract option values
408
+ // Commander.js converts hyphenated options to camelCase in opts()
409
+ // Store them as camelCase in argValues for consistency
410
+ if (commandOptions) {
411
+ const opts = commandObj.opts()
412
+ for (const optionName in commandOptions) {
413
+ const camelCaseKey = toCamelCase(optionName)
414
+ argValues[camelCaseKey] = opts[camelCaseKey] || false
415
+ }
416
+ }
417
+
418
+ // Pass global options (like --yes, --now) from program to command args
419
+ const globalOpts = program.opts()
420
+ if (globalOpts.yes) {
421
+ argValues.yes = true
422
+ }
423
+ if (globalOpts.now) {
424
+ argValues.now = true
425
+ }
426
+
427
+ await cliCommands[commandName](argValues)
428
+ })
429
+ }
430
+
431
+ await program.parseAsync(argv)
432
+ }
433
+ }
434
+ }
435
+ }
436
+ }, {
437
+ importMeta: import.meta,
438
+ importStack: makeImportStack(),
439
+ capsuleName: capsule['#'],
440
+ })
441
+ }
442
+ capsule['#'] = 't44/caps/WorkspaceCli'