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