t44 0.4.0-rc.10

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 (127) 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/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity.yaml +25 -0
  7. package/.o/assets/Hero-Terminal44-v0.jpeg +0 -0
  8. package/DCO.md +34 -0
  9. package/LICENSE.md +203 -0
  10. package/README.md +185 -0
  11. package/bin/activate +36 -0
  12. package/bin/activate.ts +30 -0
  13. package/bin/postinstall.sh +19 -0
  14. package/bin/shell +27 -0
  15. package/bin/t44 +27 -0
  16. package/caps/ConfigSchemaStruct.ts +55 -0
  17. package/caps/Home.ts +57 -0
  18. package/caps/HomeRegistry.ts +319 -0
  19. package/caps/HomeRegistryFile.ts +144 -0
  20. package/caps/JsonSchemas.ts +220 -0
  21. package/caps/OpenApiSchema.ts +67 -0
  22. package/caps/PackageDescriptor.ts +88 -0
  23. package/caps/ProjectCatalogs.ts +153 -0
  24. package/caps/ProjectDeployment.ts +363 -0
  25. package/caps/ProjectDevelopment.ts +257 -0
  26. package/caps/ProjectPublishing.ts +522 -0
  27. package/caps/ProjectRack.ts +155 -0
  28. package/caps/ProjectRepository.ts +322 -0
  29. package/caps/RootKey.ts +219 -0
  30. package/caps/SigningKey.ts +243 -0
  31. package/caps/WorkspaceCli.ts +442 -0
  32. package/caps/WorkspaceConfig.ts +268 -0
  33. package/caps/WorkspaceConfig.yaml +71 -0
  34. package/caps/WorkspaceConfigFile.ts +807 -0
  35. package/caps/WorkspaceConnection.ts +256 -0
  36. package/caps/WorkspaceEntityConfig.ts +78 -0
  37. package/caps/WorkspaceEntityConfig.v0.ts +77 -0
  38. package/caps/WorkspaceEntityFact.ts +218 -0
  39. package/caps/WorkspaceInfo.ts +595 -0
  40. package/caps/WorkspaceInit.ts +30 -0
  41. package/caps/WorkspaceKey.ts +338 -0
  42. package/caps/WorkspaceModel.ts +373 -0
  43. package/caps/WorkspaceProjects.ts +636 -0
  44. package/caps/WorkspacePrompt.ts +406 -0
  45. package/caps/WorkspaceShell.sh +39 -0
  46. package/caps/WorkspaceShell.ts +104 -0
  47. package/caps/WorkspaceShell.yaml +64 -0
  48. package/caps/WorkspaceShellCli.ts +109 -0
  49. package/caps/WorkspaceTest.ts +167 -0
  50. package/caps/providers/README.md +2 -0
  51. package/caps/providers/bunny.net/ProjectDeployment.ts +327 -0
  52. package/caps/providers/bunny.net/api-pull.test.ts +319 -0
  53. package/caps/providers/bunny.net/api-pull.ts +164 -0
  54. package/caps/providers/bunny.net/api-storage.test.ts +168 -0
  55. package/caps/providers/bunny.net/api-storage.ts +248 -0
  56. package/caps/providers/bunny.net/api.ts +95 -0
  57. package/caps/providers/dynadot.com/ProjectDeployment.ts +202 -0
  58. package/caps/providers/dynadot.com/api-domains.test.ts +224 -0
  59. package/caps/providers/dynadot.com/api-domains.ts +169 -0
  60. package/caps/providers/dynadot.com/api-restful-v1.test.ts +190 -0
  61. package/caps/providers/dynadot.com/api-restful-v1.ts +94 -0
  62. package/caps/providers/dynadot.com/api-restful-v2.test.ts +200 -0
  63. package/caps/providers/dynadot.com/api-restful-v2.ts +94 -0
  64. package/caps/providers/git-scm.com/ProjectPublishing.ts +654 -0
  65. package/caps/providers/github.com/ProjectPublishing.ts +133 -0
  66. package/caps/providers/github.com/api.ts +130 -0
  67. package/caps/providers/npmjs.com/ProjectPublishing.ts +536 -0
  68. package/caps/providers/semver.org/ProjectPublishing.ts +286 -0
  69. package/caps/providers/vercel.com/ProjectDeployment.ts +326 -0
  70. package/caps/providers/vercel.com/api.test.ts +67 -0
  71. package/caps/providers/vercel.com/api.ts +132 -0
  72. package/caps/providers/vercel.com/bun.lock +194 -0
  73. package/caps/providers/vercel.com/package.json +10 -0
  74. package/caps/providers/vercel.com/project.test.ts +108 -0
  75. package/caps/providers/vercel.com/project.ts +150 -0
  76. package/caps/providers/vercel.com/tsconfig.json +28 -0
  77. package/docs/Overview.drawio +248 -0
  78. package/docs/Overview.svg +4 -0
  79. package/examples/01-Lifecycle/main.test.ts +228 -0
  80. package/lib/crypto.ts +53 -0
  81. package/lib/key.ts +369 -0
  82. package/lib/schema-console-renderer.ts +181 -0
  83. package/lib/schema-resolver.ts +349 -0
  84. package/lib/ucan.ts +137 -0
  85. package/package.json +102 -0
  86. package/standalone-rt.ts +121 -0
  87. package/structs/HomeRegistry.ts +55 -0
  88. package/structs/HomeRegistryConfig.ts +60 -0
  89. package/structs/ProjectCatalogsConfig.ts +53 -0
  90. package/structs/ProjectDeploymentConfig.ts +56 -0
  91. package/structs/ProjectDeploymentFact.ts +106 -0
  92. package/structs/ProjectPublishingFact.ts +68 -0
  93. package/structs/ProjectRack.ts +51 -0
  94. package/structs/ProjectRackConfig.ts +56 -0
  95. package/structs/RepositoryOriginDescriptor.ts +51 -0
  96. package/structs/RootKeyConfig.ts +64 -0
  97. package/structs/SigningKeyConfig.ts +64 -0
  98. package/structs/Workspace.ts +56 -0
  99. package/structs/WorkspaceCatalogs.ts +56 -0
  100. package/structs/WorkspaceCliConfig.ts +53 -0
  101. package/structs/WorkspaceConfig.ts +64 -0
  102. package/structs/WorkspaceConfigFile.ts +50 -0
  103. package/structs/WorkspaceConfigFileMeta.ts +70 -0
  104. package/structs/WorkspaceKey.ts +55 -0
  105. package/structs/WorkspaceKeyConfig.ts +56 -0
  106. package/structs/WorkspaceMappingsConfig.ts +56 -0
  107. package/structs/WorkspaceProject.ts +104 -0
  108. package/structs/WorkspaceProjectsConfig.ts +67 -0
  109. package/structs/WorkspacePublishingConfig.ts +65 -0
  110. package/structs/WorkspaceShellConfig.ts +83 -0
  111. package/structs/providers/README.md +2 -0
  112. package/structs/providers/bunny.net/PullZoneFact.ts +55 -0
  113. package/structs/providers/bunny.net/PullZoneListFact.ts +55 -0
  114. package/structs/providers/bunny.net/StorageZoneFact.ts +55 -0
  115. package/structs/providers/bunny.net/StorageZoneListFact.ts +55 -0
  116. package/structs/providers/bunny.net/WorkspaceConnectionConfig.ts +43 -0
  117. package/structs/providers/dynadot.com/DomainFact.ts +46 -0
  118. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.ts +54 -0
  119. package/structs/providers/git-scm.com/ProjectPublishingFact.ts +46 -0
  120. package/structs/providers/github.com/ProjectPublishingFact.ts +46 -0
  121. package/structs/providers/github.com/WorkspaceConnectionConfig.ts +43 -0
  122. package/structs/providers/npmjs.com/ProjectPublishingFact.ts +46 -0
  123. package/structs/providers/vercel.com/ProjectDeploymentFact.ts +55 -0
  124. package/structs/providers/vercel.com/WorkspaceConnectionConfig.ts +49 -0
  125. package/tsconfig.json +28 -0
  126. package/workspace-rt.ts +134 -0
  127. package/workspace.yaml +3 -0
@@ -0,0 +1,363 @@
1
+
2
+ import { join } from 'path'
3
+
4
+ export async function capsule({
5
+ encapsulate,
6
+ CapsulePropertyTypes,
7
+ makeImportStack
8
+ }: {
9
+ encapsulate: any
10
+ CapsulePropertyTypes: any
11
+ makeImportStack: any
12
+ }) {
13
+ return encapsulate({
14
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
15
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
16
+ '#t44/structs/ProjectDeploymentConfig': {
17
+ as: '$ProjectDeploymentConfig',
18
+ },
19
+ '#t44/structs/WorkspaceConfig': {
20
+ as: '$WorkspaceConfig'
21
+ },
22
+ '#': {
23
+ WorkspacePrompt: {
24
+ type: CapsulePropertyTypes.Mapping,
25
+ value: 't44/caps/WorkspacePrompt'
26
+ },
27
+ Vercel: {
28
+ type: CapsulePropertyTypes.Mapping,
29
+ value: 't44/caps/providers/vercel.com/ProjectDeployment'
30
+ },
31
+ Bunny: {
32
+ type: CapsulePropertyTypes.Mapping,
33
+ value: 't44/caps/providers/bunny.net/ProjectDeployment'
34
+ },
35
+ Dynadot: {
36
+ type: CapsulePropertyTypes.Mapping,
37
+ value: 't44/caps/providers/dynadot.com/ProjectDeployment'
38
+ },
39
+ WorkspaceProjects: {
40
+ type: CapsulePropertyTypes.Mapping,
41
+ value: 't44/caps/WorkspaceProjects'
42
+ },
43
+ run: {
44
+ type: CapsulePropertyTypes.Function,
45
+ value: async function (this: any, { args }: any): Promise<void> {
46
+
47
+ let { projectSelector, deprovision, yes } = args
48
+
49
+ const deploymentConfig = await this.$ProjectDeploymentConfig.config
50
+
51
+ if (!deploymentConfig?.deployments) {
52
+ throw new Error('No deployments configuration found')
53
+ }
54
+
55
+ let matchingDeployments: Record<string, any> = {}
56
+
57
+ if (!projectSelector) {
58
+ // Show interactive project selection
59
+ const chalk = (await import('chalk')).default
60
+ const allProjects = Object.keys(deploymentConfig.deployments)
61
+
62
+ if (allProjects.length === 0) {
63
+ throw new Error('No deployments configured')
64
+ }
65
+
66
+ // Display heading
67
+ const actionText = deprovision ? 'deprovision' : 'deploy'
68
+ console.log(chalk.cyan(`\nPick a project to ${actionText}. You will be asked for necessary credentials as needed.\n`))
69
+
70
+ // Build choices with deployment status
71
+ const choices: Array<{ name: string; value: string }> = []
72
+
73
+ for (const projectName of allProjects) {
74
+ const projectAliases = deploymentConfig.deployments[projectName]
75
+ const aliasNames = Object.keys(projectAliases)
76
+ const firstAlias = aliasNames[0]
77
+ const aliasConfig = projectAliases[firstAlias]
78
+
79
+ // Support both 'provider' (single) and 'providers' (array) patterns
80
+ const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
81
+
82
+ // Extract provider name from the first provider's capsule path
83
+ const firstCapsulePath = providers[0]?.capsule || ''
84
+ const providerMatch = firstCapsulePath.match(/providers\/([^/]+)\//)
85
+ const providerName = providerMatch ? providerMatch[1] : 'unknown'
86
+
87
+ // Check deployment status across all providers that support it
88
+ let statusText = ''
89
+ let isDeployed = false
90
+ try {
91
+ let status: any
92
+ for (const providerConfig of providers) {
93
+ const capsulePath = providerConfig.capsule || ''
94
+ const config = { ...aliasConfig, provider: providerConfig }
95
+ if (capsulePath.includes('vercel.com')) {
96
+ status = await this.Vercel.status({ config, passive: true })
97
+ } else if (capsulePath.includes('bunny.net')) {
98
+ status = await this.Bunny.status({ config, passive: true })
99
+ }
100
+ // Use the first provider that returns a valid status
101
+ if (status && !status.error) break
102
+ }
103
+
104
+ if (!status || status?.error) {
105
+ statusText = chalk.gray('not deployed')
106
+ } else if (status?.status === 'READY') {
107
+ isDeployed = true
108
+ // Calculate deployment duration
109
+ let durationText = ''
110
+ if (status.createdAt || status.updatedAt) {
111
+ const deployedDate = new Date(status.updatedAt || status.createdAt)
112
+ const now = new Date()
113
+ const diffMs = now.getTime() - deployedDate.getTime()
114
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
115
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
116
+ const diffMinutes = Math.floor(diffMs / (1000 * 60))
117
+
118
+ if (diffDays > 0) {
119
+ durationText = chalk.gray(` (${diffDays}d ago)`)
120
+ } else if (diffHours > 0) {
121
+ durationText = chalk.gray(` (${diffHours}h ago)`)
122
+ } else if (diffMinutes > 0) {
123
+ durationText = chalk.gray(` (${diffMinutes}m ago)`)
124
+ } else {
125
+ durationText = chalk.gray(' (just now)')
126
+ }
127
+ }
128
+ statusText = chalk.green('deployed') + durationText
129
+ } else if (status?.status) {
130
+ statusText = chalk.gray(status.status.toLowerCase())
131
+ } else {
132
+ statusText = chalk.gray('not deployed')
133
+ }
134
+ } catch {
135
+ statusText = chalk.gray('not deployed')
136
+ }
137
+
138
+ // When deprovisioning, only show deployed projects
139
+ if (deprovision && !isDeployed) continue
140
+
141
+ const providerText = chalk.cyan(`[${providerName}]`)
142
+ const aliasText = chalk.gray(`[${aliasNames.join(', ')}]`)
143
+ const projectText = chalk.white(projectName)
144
+
145
+ choices.push({
146
+ name: `${projectText} ${providerText} ${aliasText} ${statusText}`,
147
+ value: projectName
148
+ })
149
+ }
150
+
151
+ if (choices.length === 0) {
152
+ console.log(chalk.gray('No deployed projects found.\n'))
153
+ return
154
+ }
155
+
156
+ try {
157
+ const selectedProject = await this.WorkspacePrompt.select({
158
+ message: `Select a project:`,
159
+ choices
160
+ })
161
+
162
+ // Set the selected project as workspaceProject for further processing
163
+ matchingDeployments[selectedProject] = deploymentConfig.deployments[selectedProject]
164
+ } catch (error: any) {
165
+ if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
166
+ console.log(chalk.red('\nABORTED\n'))
167
+ return
168
+ }
169
+ throw error
170
+ }
171
+ } else {
172
+ matchingDeployments = await this.WorkspaceProjects.resolveMatchingDeployments({
173
+ workspaceProject: projectSelector,
174
+ deployments: deploymentConfig.deployments
175
+ })
176
+ }
177
+
178
+ // Deploy or deprovision each matching project
179
+ for (const [projectName, deploymentConfig] of Object.entries(matchingDeployments)) {
180
+ if (deprovision) {
181
+ console.log(`\n=> Deprovisioning project '${projectName}' ...\n`)
182
+ } else {
183
+ console.log(`\n=> Deploying project '${projectName}' ...\n`)
184
+ }
185
+
186
+ const orderedAliases = orderAliasesByDependencies(deploymentConfig)
187
+
188
+ // For deprovision, confirm once at the project level
189
+ if (deprovision && !yes) {
190
+ const chalk = (await import('chalk')).default
191
+ const aliasNames = orderedAliases.join(', ')
192
+ console.log(chalk.red(`\n⚠️ WARNING: You are about to DELETE all deployments for project '${projectName}':\n`))
193
+ console.log(chalk.red(` Aliases: ${aliasNames}`))
194
+ console.log(chalk.red(`\n ⚠️ THIS ACTION IS IRREVERSIBLE ⚠️\n`))
195
+
196
+ try {
197
+ const confirmation = await this.WorkspacePrompt.input({
198
+ message: chalk.red(`To confirm deletion, type the project name exactly: "${projectName}"`),
199
+ defaultValue: ''
200
+ })
201
+
202
+ if (confirmation !== projectName) {
203
+ console.log(chalk.red('\n⚠️ ABORTED\n'))
204
+ continue
205
+ }
206
+
207
+ console.log(chalk.red(`\n✓ Confirmation received. Proceeding with deprovisioning...\n`))
208
+ } catch (error: any) {
209
+ if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
210
+ console.log(chalk.red('\n\nABORTED\n'))
211
+ return
212
+ }
213
+ throw error
214
+ }
215
+ } else if (deprovision && yes) {
216
+ const chalk = (await import('chalk')).default
217
+ console.log(chalk.red(`\n✓ Auto-confirmed with --yes flag. Proceeding with deprovisioning...\n`))
218
+ }
219
+
220
+ // For deprovision, reverse the order to handle dependencies correctly
221
+ const aliasesToProcess = deprovision ? orderedAliases.reverse() : orderedAliases
222
+
223
+ for (const alias of aliasesToProcess) {
224
+ if (deprovision) {
225
+ console.log(`\n=> Deprovisioning provider project alias '${alias}' for workspace project '${projectName}' ...\n`)
226
+ } else {
227
+ console.log(`\n=> Running deployment of provider project alias '${alias}' for workspace project '${projectName}' ...\n`)
228
+ }
229
+
230
+ const aliasConfig = deploymentConfig[alias]
231
+
232
+ // Support both 'provider' (single) and 'providers' (array) patterns
233
+ const providers = aliasConfig.providers || (aliasConfig.provider ? [aliasConfig.provider] : [])
234
+
235
+ for (const providerConfig of providers) {
236
+ const capsulePath = providerConfig.capsule
237
+
238
+ // Build config object that matches expected structure
239
+ const config = {
240
+ ...aliasConfig,
241
+ provider: providerConfig
242
+ }
243
+
244
+ if (capsulePath === 't44/caps/providers/vercel.com/ProjectDeployment') {
245
+
246
+ if (deprovision) {
247
+ // Check if project exists before attempting to deprovision
248
+ const status = await this.Vercel.status({ config })
249
+
250
+ if (status.error) {
251
+ console.log(`Project not found on provider. Skipping deprovisioning.`)
252
+ continue
253
+ }
254
+
255
+ await this.Vercel.deprovision({ config })
256
+ } else {
257
+ await this.Vercel.deploy({
258
+ alias,
259
+ config,
260
+ projectionDir: join(
261
+ (await this.$WorkspaceConfig.config).rootDir,
262
+ '.~o/workspace.foundation/o/vercel.com'
263
+ )
264
+ })
265
+ }
266
+
267
+ } else if (capsulePath === 't44/caps/providers/bunny.net/ProjectDeployment') {
268
+
269
+ if (deprovision) {
270
+ await this.Bunny.deprovision({ config })
271
+ } else {
272
+ await this.Bunny.deploy({
273
+ alias,
274
+ config,
275
+ projectionDir: join(
276
+ (await this.$WorkspaceConfig.config).rootDir,
277
+ '.~o/workspace.foundation/o/bunny.net'
278
+ ),
279
+ workspaceProjectName: projectName
280
+ })
281
+ }
282
+
283
+ } else if (capsulePath === 't44/caps/providers/dynadot.com/ProjectDeployment') {
284
+
285
+ if (deprovision) {
286
+ await this.Dynadot.deprovision({ config })
287
+ } else {
288
+ await this.Dynadot.deploy({
289
+ alias,
290
+ config,
291
+ projectionDir: join(
292
+ (await this.$WorkspaceConfig.config).rootDir,
293
+ '.~o/workspace.foundation/o/dynadot.com'
294
+ ),
295
+ workspaceProjectName: projectName
296
+ })
297
+ }
298
+
299
+ } else {
300
+ throw new Error(`Unsupported capsule '${capsulePath}'!`)
301
+ }
302
+ }
303
+
304
+ if (deprovision) {
305
+ console.log(`\n<= Deprovisioning of provider project alias '${alias}' for workspace project '${projectName}' done.\n`)
306
+ } else {
307
+ console.log(`\n<= Deployment of provider project alias '${alias}' for workspace project '${projectName}' done.\n`)
308
+ }
309
+ }
310
+
311
+ if (deprovision) {
312
+ console.log(`\n<= Project '${projectName}' deprovisioning complete.\n`)
313
+ } else {
314
+ console.log(`\n<= Project '${projectName}' deployment complete.\n`)
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }, {
322
+ importMeta: import.meta,
323
+ importStack: makeImportStack(),
324
+ capsuleName: capsule['#'],
325
+ })
326
+ }
327
+ capsule['#'] = 't44/caps/ProjectDeployment'
328
+
329
+
330
+ function orderAliasesByDependencies(deploymentConfig: Record<string, any>): string[] {
331
+ const aliases = Object.keys(deploymentConfig)
332
+ const ordered: string[] = []
333
+ const visited = new Set<string>()
334
+ const visiting = new Set<string>()
335
+
336
+ function visit(alias: string): void {
337
+ if (visited.has(alias)) return
338
+
339
+ if (visiting.has(alias)) {
340
+ throw new Error(`Circular dependency detected involving alias: ${alias}`)
341
+ }
342
+
343
+ visiting.add(alias)
344
+
345
+ const depends = deploymentConfig[alias].depends || []
346
+ for (const dep of depends) {
347
+ if (!deploymentConfig[dep]) {
348
+ throw new Error(`Dependency '${dep}' not found for alias '${alias}'`)
349
+ }
350
+ visit(dep)
351
+ }
352
+
353
+ visiting.delete(alias)
354
+ visited.add(alias)
355
+ ordered.push(alias)
356
+ }
357
+
358
+ for (const alias of aliases) {
359
+ visit(alias)
360
+ }
361
+
362
+ return ordered
363
+ }
@@ -0,0 +1,257 @@
1
+
2
+ import { join, resolve, relative } from 'path'
3
+ import { readdir, readFile, access } from 'fs/promises'
4
+ import { constants } from 'fs'
5
+ import { $ } from 'bun'
6
+ import chalk from 'chalk'
7
+
8
+ export async function capsule({
9
+ encapsulate,
10
+ CapsulePropertyTypes,
11
+ makeImportStack
12
+ }: {
13
+ encapsulate: any
14
+ CapsulePropertyTypes: any
15
+ makeImportStack: any
16
+ }) {
17
+ return encapsulate({
18
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
19
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
20
+ '#': {
21
+ WorkspaceConfig: {
22
+ type: CapsulePropertyTypes.Mapping,
23
+ value: 't44/caps/WorkspaceConfig'
24
+ },
25
+ WorkspaceProjects: {
26
+ type: CapsulePropertyTypes.Mapping,
27
+ value: 't44/caps/WorkspaceProjects'
28
+ },
29
+ WorkspacePrompt: {
30
+ type: CapsulePropertyTypes.Mapping,
31
+ value: 't44/caps/WorkspacePrompt'
32
+ },
33
+ run: {
34
+ type: CapsulePropertyTypes.Function,
35
+ value: async function (this: any, { args }: any): Promise<void> {
36
+
37
+ const { projectSelector } = args
38
+
39
+ const projects = await this.WorkspaceProjects.list
40
+
41
+ // Discover all dev scripts across projects and their packages
42
+ const devTargets: Array<{
43
+ label: string
44
+ type: 'project' | 'package'
45
+ projectName: string
46
+ packageName?: string
47
+ dir: string
48
+ script: string
49
+ }> = []
50
+
51
+ for (const [projectName, projectInfo] of Object.entries(projects)) {
52
+ const sourceDir = (projectInfo as any).sourceDir
53
+ if (!sourceDir) continue
54
+
55
+ // Check project root for dev script
56
+ const projectPkgPath = join(sourceDir, 'package.json')
57
+ try {
58
+ await access(projectPkgPath, constants.F_OK)
59
+ const pkgContent = await readFile(projectPkgPath, 'utf-8')
60
+ const pkg = JSON.parse(pkgContent)
61
+ if (pkg.scripts?.dev) {
62
+ devTargets.push({
63
+ label: projectName,
64
+ type: 'project',
65
+ projectName,
66
+ dir: sourceDir,
67
+ script: pkg.scripts.dev
68
+ })
69
+ }
70
+ } catch { }
71
+
72
+ // Check packages/* for dev scripts
73
+ const packagesDir = join(sourceDir, 'packages')
74
+ try {
75
+ await access(packagesDir, constants.F_OK)
76
+ const packageEntries = await readdir(packagesDir, { withFileTypes: true })
77
+ for (const entry of packageEntries) {
78
+ if (!entry.isDirectory()) continue
79
+ const pkgDir = join(packagesDir, entry.name)
80
+ const pkgJsonPath = join(pkgDir, 'package.json')
81
+ try {
82
+ await access(pkgJsonPath, constants.F_OK)
83
+ const pkgContent = await readFile(pkgJsonPath, 'utf-8')
84
+ const pkg = JSON.parse(pkgContent)
85
+ if (pkg.scripts?.dev) {
86
+ devTargets.push({
87
+ label: `${projectName}/packages/${entry.name}`,
88
+ type: 'package',
89
+ projectName,
90
+ packageName: entry.name,
91
+ dir: pkgDir,
92
+ script: pkg.scripts.dev
93
+ })
94
+ }
95
+ } catch { }
96
+ }
97
+ } catch { }
98
+ }
99
+
100
+ if (devTargets.length === 0) {
101
+ console.log(chalk.yellow('\nNo projects or packages with a "dev" script found.\n'))
102
+ return
103
+ }
104
+
105
+ // Sort alphabetically by label
106
+ devTargets.sort((a, b) => a.label.localeCompare(b.label))
107
+
108
+ let selectedTarget: typeof devTargets[0]
109
+
110
+ if (projectSelector) {
111
+ // Match by project name, package path, package name, or resolved path
112
+ const resolvedSelector = resolve(process.cwd(), projectSelector)
113
+
114
+ const matches = devTargets.filter(t => {
115
+ // Name-based matching
116
+ if (t.label === projectSelector ||
117
+ t.label.startsWith(projectSelector) ||
118
+ t.projectName === projectSelector ||
119
+ t.packageName === projectSelector) {
120
+ return true
121
+ }
122
+ // Path-based matching: resolved selector matches or contains the target dir
123
+ const resolvedDir = resolve(t.dir)
124
+ if (resolvedDir === resolvedSelector || resolvedDir.startsWith(resolvedSelector + '/')) {
125
+ return true
126
+ }
127
+ return false
128
+ })
129
+
130
+ if (matches.length === 0) {
131
+ console.log(chalk.red(`\nNo dev script found matching '${projectSelector}'.\n`))
132
+ console.log(chalk.gray('Available targets:'))
133
+ for (const t of devTargets) {
134
+ const typeTag = t.type === 'project'
135
+ ? chalk.cyan('[project]')
136
+ : chalk.magenta('[package]')
137
+ console.log(chalk.gray(` - ${t.label} ${typeTag}`))
138
+ }
139
+ console.log('')
140
+ return
141
+ }
142
+
143
+ if (matches.length > 1) {
144
+ // Prefer exact match over substring matches
145
+ const exactMatch = matches.find(t =>
146
+ t.label === projectSelector ||
147
+ t.projectName === projectSelector ||
148
+ t.packageName === projectSelector
149
+ )
150
+ if (exactMatch) {
151
+ matches.length = 0
152
+ matches.push(exactMatch)
153
+ } else {
154
+ console.log(chalk.red(`\nMultiple dev targets match '${projectSelector}':\n`))
155
+ for (const m of matches) {
156
+ console.log(chalk.gray(` - ${m.label}`))
157
+ }
158
+ console.log(chalk.red('\nPlease be more specific.\n'))
159
+ return
160
+ }
161
+ }
162
+
163
+ selectedTarget = matches[0]
164
+ } else {
165
+ // Interactive picker
166
+ console.log(chalk.cyan('\nSelect a dev server to run:\n'))
167
+
168
+ const choices: Array<{ name: string; value: number }> = []
169
+
170
+ for (let i = 0; i < devTargets.length; i++) {
171
+ const t = devTargets[i]
172
+ const typeTag = t.type === 'project'
173
+ ? chalk.cyan('[project]')
174
+ : chalk.magenta('[package]')
175
+ const scriptPreview = chalk.gray(t.script)
176
+
177
+ choices.push({
178
+ name: `${chalk.white(t.label)} ${typeTag} ${scriptPreview}`,
179
+ value: i
180
+ })
181
+ }
182
+
183
+ try {
184
+ const selectedIndex = await this.WorkspacePrompt.select({
185
+ message: 'Select dev target:',
186
+ choices,
187
+ pageSize: 20
188
+ })
189
+ selectedTarget = devTargets[selectedIndex]
190
+ } catch (error: any) {
191
+ if (error.message?.includes('SIGINT') || error.message?.includes('force closed')) {
192
+ console.log(chalk.red('\nABORTED\n'))
193
+ return
194
+ }
195
+ throw error
196
+ }
197
+ }
198
+
199
+ // Run the dev script interactively
200
+ const typeTag = selectedTarget.type === 'project'
201
+ ? chalk.cyan('[project]')
202
+ : chalk.magenta('[package]')
203
+
204
+ console.log(chalk.green(`\n=> Starting dev server for ${selectedTarget.label} ${typeTag}\n`))
205
+ console.log(chalk.gray(` Directory: ${selectedTarget.dir}`))
206
+ console.log(chalk.gray(` Script: ${selectedTarget.script}\n`))
207
+
208
+ // Check if node_modules exists; if not, check for dependencies and run bun install
209
+ const nodeModulesDir = join(selectedTarget.dir, 'node_modules')
210
+ let needsInstall = false
211
+ try {
212
+ await access(nodeModulesDir, constants.F_OK)
213
+ } catch {
214
+ // node_modules missing — check if package.json has dependencies
215
+ const pkgPath = join(selectedTarget.dir, 'package.json')
216
+ try {
217
+ const pkgContent = await readFile(pkgPath, 'utf-8')
218
+ const pkg = JSON.parse(pkgContent)
219
+ if ((pkg.dependencies && Object.keys(pkg.dependencies).length > 0) ||
220
+ (pkg.devDependencies && Object.keys(pkg.devDependencies).length > 0)) {
221
+ needsInstall = true
222
+ }
223
+ } catch { }
224
+ }
225
+
226
+ if (needsInstall) {
227
+ console.log(chalk.yellow(` Installing dependencies ...\n`))
228
+ const installProc = Bun.spawn(['bun', 'install'], {
229
+ cwd: selectedTarget.dir,
230
+ stdin: 'inherit',
231
+ stdout: 'inherit',
232
+ stderr: 'inherit'
233
+ })
234
+ await installProc.exited
235
+ console.log('')
236
+ }
237
+
238
+ // Use Bun.spawn for full interactive mode (stdin/stdout/stderr passthrough)
239
+ const proc = Bun.spawn(['bun', 'run', 'dev'], {
240
+ cwd: selectedTarget.dir,
241
+ stdin: 'inherit',
242
+ stdout: 'inherit',
243
+ stderr: 'inherit'
244
+ })
245
+
246
+ await proc.exited
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }, {
252
+ importMeta: import.meta,
253
+ importStack: makeImportStack(),
254
+ capsuleName: capsule['#'],
255
+ })
256
+ }
257
+ capsule['#'] = 't44/caps/ProjectDevelopment'