@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,458 @@
1
+
2
+ import { join, dirname } from 'path'
3
+ import { readFile, writeFile, access } from 'fs/promises'
4
+ import glob from 'fast-glob'
5
+ import chalk from 'chalk'
6
+
7
+ function detectIndent(content: string): number {
8
+ const match = content.match(/^\{\s*\n([ \t]+)/)
9
+ if (match) {
10
+ return match[1].length
11
+ }
12
+ return 2
13
+ }
14
+
15
+ export async function capsule({
16
+ encapsulate,
17
+ CapsulePropertyTypes,
18
+ makeImportStack
19
+ }: {
20
+ encapsulate: any
21
+ CapsulePropertyTypes: any
22
+ makeImportStack: any
23
+ }) {
24
+ return encapsulate({
25
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
26
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
27
+ '#@stream44.studio/t44/structs/ProjectPublishingConfig': {
28
+ as: '$WorkspaceRepositories'
29
+ },
30
+ '#@stream44.studio/t44/structs/WorkspaceMappingsConfig': {
31
+ as: '$WorkspaceMappings'
32
+ },
33
+ '#': {
34
+ reverseRenamePatch: {
35
+ type: CapsulePropertyTypes.Function,
36
+ value: async function (this: any, { patchContent }: { patchContent: string }): Promise<{ content: string, modified: boolean }> {
37
+ const mappingsConfig = await this.$WorkspaceMappings.config
38
+ const publishingMappings = mappingsConfig?.mappings?.['@stream44.studio/t44/caps/patterns/ProjectPublishing']
39
+ if (!publishingMappings?.npm) return { content: patchContent, modified: false }
40
+
41
+ const npmRenames: Record<string, string> = publishingMappings.npm
42
+
43
+ // Build reverse map: publicName → workspaceName
44
+ // Only include mappings where the public name starts with '@' to avoid
45
+ // overly broad replacements (e.g. 't44' would match too many things)
46
+ const reverseRenames: Record<string, string> = {}
47
+ for (const [workspaceName, publicName] of Object.entries(npmRenames)) {
48
+ if (publicName.startsWith('@')) {
49
+ reverseRenames[publicName] = workspaceName
50
+ }
51
+ }
52
+
53
+ // Sort by length descending to replace longer names first (avoid partial matches)
54
+ const reverseEntries = Object.entries(reverseRenames)
55
+ .sort((a, b) => b[0].length - a[0].length)
56
+
57
+ if (reverseEntries.length === 0) return { content: patchContent, modified: false }
58
+
59
+ let content = patchContent
60
+ let modified = false
61
+
62
+ for (const [publicName, workspaceName] of reverseEntries) {
63
+ // Replace the literal public name with workspace name
64
+ const regex = new RegExp(publicName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
65
+ const replaced = content.replace(regex, workspaceName)
66
+ if (replaced !== content) {
67
+ content = replaced
68
+ modified = true
69
+ }
70
+
71
+ // Also replace regex-escaped versions of the public name
72
+ // (e.g. @stream44\.studio\/dco in test patterns)
73
+ const pubEscaped = publicName.replace(/[.*+?^${}()|[\]/\\]/g, '\\$&')
74
+ if (pubEscaped !== publicName) {
75
+ const wsEscaped = workspaceName.replace(/[.*+?^${}()|[\]/\\]/g, '\\$&')
76
+ const escapedRegex = new RegExp(pubEscaped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
77
+ const replacedEscaped = content.replace(escapedRegex, wsEscaped)
78
+ if (replacedEscaped !== content) {
79
+ content = replacedEscaped
80
+ modified = true
81
+ }
82
+ }
83
+ }
84
+
85
+ return { content, modified }
86
+ }
87
+ },
88
+ rename: {
89
+ type: CapsulePropertyTypes.Function,
90
+ value: async function (this: any, { dirs, repos }: { dirs: Iterable<string>, repos?: Record<string, any> }) {
91
+ const mappingsConfig = await this.$WorkspaceMappings.config
92
+ const publishingMappings = mappingsConfig?.mappings?.['@stream44.studio/t44/caps/patterns/ProjectPublishing']
93
+ if (publishingMappings?.npm) {
94
+ const npmRenames: Record<string, string> = publishingMappings.npm
95
+ const renameEntries = Object.entries(npmRenames)
96
+ .sort((a, b) => b[0].length - a[0].length)
97
+
98
+ if (renameEntries.length > 0) {
99
+ console.log('[t44] Applying package name renames ...\n')
100
+
101
+ for (const dir of dirs) {
102
+ const files = await glob('**/*', {
103
+ cwd: dir,
104
+ absolute: true,
105
+ onlyFiles: true,
106
+ dot: true
107
+ })
108
+
109
+ for (const file of files) {
110
+ try {
111
+ const buffer = await readFile(file)
112
+
113
+ // Detect binary files by checking for null bytes (the standard
114
+ // heuristic used by git, diff, file(1), etc.)
115
+ if (buffer.includes(0x00)) continue
116
+
117
+ let content = buffer.toString('utf-8')
118
+ let modified = false
119
+
120
+ for (const [workspaceName, publicName] of renameEntries) {
121
+ // Replace the literal workspace name
122
+ const regex = new RegExp(workspaceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
123
+ const replaced = content.replace(regex, publicName)
124
+ if (replaced !== content) {
125
+ content = replaced
126
+ modified = true
127
+ }
128
+
129
+ // Also replace regex-escaped versions of the workspace name
130
+ // (e.g. @stream44\.studio\/encapsulate in test patterns)
131
+ const wsEscaped = workspaceName.replace(/[.*+?^${}()|[\]/\\]/g, '\\$&')
132
+ if (wsEscaped !== workspaceName) {
133
+ const pubEscaped = publicName.replace(/[.*+?^${}()|[\]/\\]/g, '\\$&')
134
+ const escapedRegex = new RegExp(wsEscaped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
135
+ const replacedEscaped = content.replace(escapedRegex, pubEscaped)
136
+ if (replacedEscaped !== content) {
137
+ content = replacedEscaped
138
+ modified = true
139
+ }
140
+ }
141
+ }
142
+
143
+ if (modified) {
144
+ await writeFile(file, content, 'utf-8')
145
+ }
146
+ } catch (e) {
147
+ // Skip files that can't be read
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Resolve workspace:* dependencies
155
+ if (repos) {
156
+ const repositoriesConfig = await this.$WorkspaceRepositories.config
157
+ const mappingsConfig = await this.$WorkspaceMappings.config
158
+ const npmRenames: Record<string, string> = mappingsConfig?.mappings?.['@stream44.studio/t44/caps/patterns/ProjectPublishing']?.npm || {}
159
+ const { publicNpmPackageNames, workspaceNpmPackageNames, workspacePackageSourceDirs } = await buildWorkspacePackageMaps(repositoriesConfig, npmRenames)
160
+
161
+ console.log('[t44] Resolving workspace dependencies ...\n')
162
+ for (const [repoName, repoSourceDir] of Object.entries(repos)) {
163
+ const packageJsonPath = join(repoSourceDir as string, 'package.json')
164
+
165
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
166
+ const indent = detectIndent(packageJsonContent)
167
+ const packageJson = JSON.parse(packageJsonContent)
168
+
169
+ await updateWorkspaceDependencies(packageJson, workspaceNpmPackageNames, workspacePackageSourceDirs, publicNpmPackageNames)
170
+
171
+ const updatedContent = JSON.stringify(packageJson, null, indent) + '\n'
172
+ if (updatedContent !== packageJsonContent) {
173
+ await writeFile(packageJsonPath, updatedContent, 'utf-8')
174
+ console.log(chalk.green(` ✓ Updated workspace dependencies in ${packageJsonPath}\n`))
175
+ }
176
+
177
+ // Follow workspaces declarations to update sub-workspace package.json files
178
+ const workspaces: string[] = packageJson.workspaces || []
179
+ if (workspaces.length > 0) {
180
+ const workspacePatterns = workspaces.map(ws => join(ws, 'package.json'))
181
+ const subPackageJsonPaths = await glob(workspacePatterns, {
182
+ cwd: repoSourceDir as string,
183
+ absolute: true,
184
+ onlyFiles: true,
185
+ })
186
+
187
+ for (const subPkgPath of subPackageJsonPaths) {
188
+ try {
189
+ const subContent = await readFile(subPkgPath, 'utf-8')
190
+ const subIndent = detectIndent(subContent)
191
+ const subPkg = JSON.parse(subContent)
192
+
193
+ await updateWorkspaceDependencies(subPkg, workspaceNpmPackageNames, workspacePackageSourceDirs, publicNpmPackageNames)
194
+
195
+ const subUpdated = JSON.stringify(subPkg, null, subIndent) + '\n'
196
+ if (subUpdated !== subContent) {
197
+ await writeFile(subPkgPath, subUpdated, 'utf-8')
198
+ console.log(chalk.green(` ✓ Updated workspace dependencies in ${subPkgPath}\n`))
199
+ }
200
+ } catch (e) {
201
+ // Skip sub-workspaces with unreadable package.json
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ // Clean up tsconfig.json extends paths that don't resolve in the published package
208
+ console.log('[t44] Cleaning up tsconfig.json extends paths ...\n')
209
+ for (const [repoName, repoSourceDir] of Object.entries(repos)) {
210
+ await cleanupTsconfigExtends(join(repoSourceDir as string, 'tsconfig.json'))
211
+
212
+ // Follow workspaces to find sub-workspace tsconfig.json files
213
+ const pkgPath = join(repoSourceDir as string, 'package.json')
214
+ try {
215
+ const pkgContent = await readFile(pkgPath, 'utf-8')
216
+ const pkg = JSON.parse(pkgContent)
217
+ const workspaces: string[] = pkg.workspaces || []
218
+ if (workspaces.length > 0) {
219
+ const tsconfigPatterns = workspaces.map(ws => join(ws, 'tsconfig.json'))
220
+ const tsconfigPaths = await glob(tsconfigPatterns, {
221
+ cwd: repoSourceDir as string,
222
+ absolute: true,
223
+ onlyFiles: true,
224
+ })
225
+ for (const tsconfigPath of tsconfigPaths) {
226
+ await cleanupTsconfigExtends(tsconfigPath)
227
+ }
228
+ }
229
+ } catch { }
230
+ }
231
+ }
232
+ }
233
+ },
234
+ bump: {
235
+ type: CapsulePropertyTypes.Function,
236
+ value: async function (this: any, { config, ctx }: { config: any, ctx: any }) {
237
+ const { rc, release } = ctx.options || {}
238
+
239
+ const projectSourceDir = join(ctx.repoSourceDir)
240
+ const packageJsonPath = join(projectSourceDir, 'package.json')
241
+
242
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
243
+ const packageJson = JSON.parse(packageJsonContent)
244
+ const currentVersion = packageJson.version
245
+
246
+ let newVersion: string
247
+
248
+ if (release) {
249
+ const rcMatch = currentVersion.match(/^(.+)-rc\.\d+$/)
250
+ if (rcMatch) {
251
+ newVersion = rcMatch[1]
252
+ console.log(chalk.cyan(` Removing RC suffix: ${currentVersion} → ${newVersion}`))
253
+ } else {
254
+ console.log(chalk.yellow(` Version ${currentVersion} has no RC suffix, skipping bump`))
255
+ return
256
+ }
257
+ } else if (rc) {
258
+ const rcMatch = currentVersion.match(/^(.+)-rc\.(\d+)$/)
259
+ if (rcMatch) {
260
+ const baseVersion = rcMatch[1]
261
+ const rcNumber = parseInt(rcMatch[2], 10)
262
+ newVersion = `${baseVersion}-rc.${rcNumber + 1}`
263
+ console.log(chalk.cyan(` Incrementing RC version: ${currentVersion} → ${newVersion}`))
264
+ } else {
265
+ const versionParts = currentVersion.split('.')
266
+ if (versionParts.length !== 3) {
267
+ throw new Error(`Invalid version format: ${currentVersion}`)
268
+ }
269
+ const [major, minor, patch] = versionParts
270
+ const newMinor = parseInt(minor, 10) + 1
271
+ newVersion = `${major}.${newMinor}.0-rc.1`
272
+ console.log(chalk.cyan(` Bumping minor version and adding RC: ${currentVersion} → ${newVersion}`))
273
+ }
274
+ } else {
275
+ console.log(chalk.yellow(` No version bump requested`))
276
+ return
277
+ }
278
+
279
+ packageJson.version = newVersion
280
+ const indent = detectIndent(packageJsonContent)
281
+ const updatedContent = JSON.stringify(packageJson, null, indent) + '\n'
282
+ await writeFile(packageJsonPath, updatedContent, 'utf-8')
283
+
284
+ // Write bumped version back to the original source package.json
285
+ // so that if this run fails partway through, the next run starts
286
+ // from the already-bumped version instead of producing a duplicate tag.
287
+ const originalSourceDir = ctx.repoConfig?.sourceDir
288
+ if (originalSourceDir) {
289
+ const originalPackageJsonPath = join(originalSourceDir, 'package.json')
290
+ try {
291
+ const originalContent = await readFile(originalPackageJsonPath, 'utf-8')
292
+ const originalJson = JSON.parse(originalContent)
293
+ originalJson.version = newVersion
294
+ const originalIndent = detectIndent(originalContent)
295
+ await writeFile(originalPackageJsonPath, JSON.stringify(originalJson, null, originalIndent) + '\n', 'utf-8')
296
+ console.log(chalk.green(` ✓ Updated ${packageJsonPath} to version ${newVersion}`))
297
+ console.log(chalk.green(` ✓ Written back version ${newVersion} to source ${originalPackageJsonPath}\n`))
298
+ } catch {
299
+ console.log(chalk.green(` ✓ Updated ${packageJsonPath} to version ${newVersion}\n`))
300
+ }
301
+ } else {
302
+ console.log(chalk.green(` ✓ Updated ${packageJsonPath} to version ${newVersion}\n`))
303
+ }
304
+
305
+ ctx.metadata.bumped = true
306
+ ctx.metadata.newVersion = newVersion
307
+ }
308
+ },
309
+ }
310
+ }
311
+ }, {
312
+ importMeta: import.meta,
313
+ importStack: makeImportStack(),
314
+ capsuleName: capsule['#'],
315
+ })
316
+ }
317
+ capsule['#'] = '@stream44.studio/t44/caps/patterns/semver.org/ProjectPublishing'
318
+
319
+
320
+ async function buildWorkspacePackageMaps(repositoriesConfig: any, npmRenames: Record<string, string>) {
321
+ const publicNpmPackageNames: Record<string, string> = {}
322
+ const workspaceNpmPackageNames: Record<string, string> = {}
323
+ const workspacePackageSourceDirs: Record<string, string> = {}
324
+
325
+ // Build reverse rename map: publicName → workspaceName
326
+ const reverseRenames: Record<string, string> = {}
327
+ for (const [workspaceName, publicName] of Object.entries(npmRenames)) {
328
+ reverseRenames[publicName] = workspaceName
329
+ }
330
+
331
+ if (repositoriesConfig?.repositories) {
332
+ for (const [repoKey, repoConfig] of Object.entries(repositoriesConfig.repositories as any)) {
333
+ const sourceDir = (repoConfig as any).sourceDir
334
+ if (!sourceDir) continue
335
+
336
+ const packageJsonPath = join(sourceDir, 'package.json')
337
+
338
+ try {
339
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
340
+ const packageJson = JSON.parse(packageJsonContent)
341
+ const workspacePackageName = packageJson.name
342
+
343
+ // Index source dir by the original workspace package name
344
+ workspacePackageSourceDirs[workspacePackageName] = sourceDir
345
+
346
+ // Also index by the renamed (public) name if a rename mapping exists
347
+ const renamedName = npmRenames[workspacePackageName]
348
+ if (renamedName) {
349
+ workspacePackageSourceDirs[renamedName] = sourceDir
350
+ workspaceNpmPackageNames[renamedName] = workspacePackageName
351
+ }
352
+ } catch (error) {
353
+ console.log(chalk.gray(` [debug] Could not read ${packageJsonPath}: ${error}`))
354
+ }
355
+
356
+ const providers = (repoConfig as any).providers || ((repoConfig as any).provider ? [(repoConfig as any).provider] : [])
357
+
358
+ for (const provider of providers) {
359
+ if (provider.capsule === '@stream44.studio/t44-npmjs.com/caps/ProjectPublishing') {
360
+ try {
361
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
362
+ const packageJson = JSON.parse(packageJsonContent)
363
+ const workspacePackageName = packageJson.name
364
+ const publicPackageName = provider.config.PackageSettings.name
365
+
366
+ publicNpmPackageNames[workspacePackageName] = publicPackageName
367
+ workspaceNpmPackageNames[publicPackageName] = workspacePackageName
368
+ // Also index source dir by the public package name
369
+ workspacePackageSourceDirs[publicPackageName] = sourceDir
370
+ } catch (error) {
371
+ throw new Error(`Could not read package.json from ${packageJsonPath}: ${error}`)
372
+ }
373
+ }
374
+ }
375
+ }
376
+ }
377
+
378
+ return { publicNpmPackageNames, workspaceNpmPackageNames, workspacePackageSourceDirs }
379
+ }
380
+
381
+ async function updateWorkspaceDependencies(
382
+ packageJson: any,
383
+ workspaceNpmPackageNames: Record<string, string>,
384
+ workspacePackageSourceDirs: Record<string, string>,
385
+ publicNpmPackageNames: Record<string, string>
386
+ ) {
387
+ const dependencyFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
388
+ const currentPackageName = packageJson.name
389
+
390
+ for (const depField of dependencyFields) {
391
+ if (packageJson[depField]) {
392
+ const updatedDeps: Record<string, string> = {}
393
+
394
+ for (const [depName, depVersion] of Object.entries(packageJson[depField])) {
395
+ // Skip self-referencing dependencies
396
+ if (depName === currentPackageName) {
397
+ continue
398
+ }
399
+
400
+ if (typeof depVersion === 'string' && depVersion.startsWith('workspace:')) {
401
+ try {
402
+ const workspaceDepName = workspaceNpmPackageNames[depName] || depName
403
+ const depSourceDir = workspacePackageSourceDirs[workspaceDepName] || workspacePackageSourceDirs[depName]
404
+
405
+ if (!depSourceDir) {
406
+ throw new Error(
407
+ `Could not find source directory for workspace dependency ${depName} (resolved to: ${workspaceDepName})\n` +
408
+ ` workspaceNpmPackageNames keys: ${Object.keys(workspaceNpmPackageNames).join(', ')}\n` +
409
+ ` workspacePackageSourceDirs keys: ${Object.keys(workspacePackageSourceDirs).join(', ')}`
410
+ )
411
+ }
412
+
413
+ const depPackageJsonPath = join(depSourceDir, 'package.json')
414
+ const depPackageJsonContent = await readFile(depPackageJsonPath, 'utf-8')
415
+ const depPackageJson = JSON.parse(depPackageJsonContent)
416
+
417
+ // Replace workspace package name with public package name
418
+ const publicDepName = publicNpmPackageNames[workspaceDepName] || depName
419
+ updatedDeps[publicDepName] = `^${depPackageJson.version}`
420
+ } catch (error) {
421
+ throw new Error(`Could not resolve workspace dependency ${depName}: ${error}`)
422
+ }
423
+ } else {
424
+ // Keep non-workspace dependencies as-is
425
+ updatedDeps[depName] = depVersion as string
426
+ }
427
+ }
428
+
429
+ packageJson[depField] = updatedDeps
430
+ }
431
+ }
432
+ }
433
+
434
+ async function cleanupTsconfigExtends(tsconfigPath: string): Promise<void> {
435
+ try {
436
+ const content = await readFile(tsconfigPath, 'utf-8')
437
+ const tsconfig = JSON.parse(content)
438
+
439
+ if (!tsconfig.extends) return
440
+
441
+ // Resolve the extends path relative to the tsconfig.json directory
442
+ const tsconfigDir = dirname(tsconfigPath)
443
+ const extendsPath = join(tsconfigDir, tsconfig.extends)
444
+
445
+ try {
446
+ await access(extendsPath)
447
+ // Path exists — keep it
448
+ } catch {
449
+ // Path does not exist — remove the extends field
450
+ delete tsconfig.extends
451
+ const indent = detectIndent(content)
452
+ await writeFile(tsconfigPath, JSON.stringify(tsconfig, null, indent) + '\n', 'utf-8')
453
+ console.log(chalk.green(` ✓ Removed invalid extends from ${tsconfigPath}\n`))
454
+ }
455
+ } catch {
456
+ // tsconfig.json doesn't exist or isn't valid JSON — skip
457
+ }
458
+ }