t44 0.2.0-rc.1

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.

Potentially problematic release.


This version of t44 might be problematic. Click here for more details.

Files changed (86) hide show
  1. package/LICENSE.md +203 -0
  2. package/README.md +154 -0
  3. package/bin/activate +36 -0
  4. package/bin/activate.ts +30 -0
  5. package/bin/postinstall.sh +19 -0
  6. package/bin/shell +27 -0
  7. package/bin/t44 +27 -0
  8. package/caps/HomeRegistry.v0.ts +298 -0
  9. package/caps/OpenApiSchema.v0.ts +192 -0
  10. package/caps/ProjectDeployment.v0.ts +363 -0
  11. package/caps/ProjectDevelopment.v0.ts +246 -0
  12. package/caps/ProjectPublishing.v0.ts +307 -0
  13. package/caps/ProjectRack.v0.ts +128 -0
  14. package/caps/WorkspaceCli.v0.ts +391 -0
  15. package/caps/WorkspaceConfig.v0.ts +626 -0
  16. package/caps/WorkspaceConfig.yaml +53 -0
  17. package/caps/WorkspaceConnection.v0.ts +240 -0
  18. package/caps/WorkspaceEntityConfig.v0.ts +64 -0
  19. package/caps/WorkspaceEntityFact.v0.ts +193 -0
  20. package/caps/WorkspaceInfo.v0.ts +554 -0
  21. package/caps/WorkspaceInit.v0.ts +30 -0
  22. package/caps/WorkspaceKey.v0.ts +186 -0
  23. package/caps/WorkspaceProjects.v0.ts +455 -0
  24. package/caps/WorkspacePrompt.v0.ts +396 -0
  25. package/caps/WorkspaceShell.sh +39 -0
  26. package/caps/WorkspaceShell.v0.ts +104 -0
  27. package/caps/WorkspaceShell.yaml +65 -0
  28. package/caps/WorkspaceShellCli.v0.ts +109 -0
  29. package/caps/WorkspaceTest.v0.ts +167 -0
  30. package/caps/providers/LICENSE.md +8 -0
  31. package/caps/providers/README.md +2 -0
  32. package/caps/providers/bunny.net/ProjectDeployment.v0.ts +328 -0
  33. package/caps/providers/bunny.net/api-pull.v0.test.ts +319 -0
  34. package/caps/providers/bunny.net/api-pull.v0.ts +161 -0
  35. package/caps/providers/bunny.net/api-storage.v0.test.ts +168 -0
  36. package/caps/providers/bunny.net/api-storage.v0.ts +245 -0
  37. package/caps/providers/bunny.net/api.v0.ts +95 -0
  38. package/caps/providers/dynadot.com/ProjectDeployment.v0.ts +207 -0
  39. package/caps/providers/dynadot.com/api-domains.v0.test.ts +147 -0
  40. package/caps/providers/dynadot.com/api-domains.v0.ts +137 -0
  41. package/caps/providers/dynadot.com/api.v0.ts +88 -0
  42. package/caps/providers/git-scm.com/ProjectPublishing.v0.ts +231 -0
  43. package/caps/providers/github.com/ProjectPublishing.v0.ts +75 -0
  44. package/caps/providers/github.com/api.v0.ts +90 -0
  45. package/caps/providers/npmjs.com/ProjectPublishing.v0.ts +741 -0
  46. package/caps/providers/vercel.com/ProjectDeployment.v0.ts +339 -0
  47. package/caps/providers/vercel.com/api.v0.test.ts +67 -0
  48. package/caps/providers/vercel.com/api.v0.ts +132 -0
  49. package/caps/providers/vercel.com/bun.lock +194 -0
  50. package/caps/providers/vercel.com/package.json +10 -0
  51. package/caps/providers/vercel.com/project.v0.test.ts +108 -0
  52. package/caps/providers/vercel.com/project.v0.ts +150 -0
  53. package/caps/providers/vercel.com/tsconfig.json +28 -0
  54. package/docs/Overview.drawio +189 -0
  55. package/docs/Overview.svg +4 -0
  56. package/lib/crypto.ts +53 -0
  57. package/lib/openapi.ts +132 -0
  58. package/lib/ucan.ts +137 -0
  59. package/package.json +41 -0
  60. package/structs/HomeRegistryConfig.v0.ts +27 -0
  61. package/structs/ProjectDeploymentConfig.v0.ts +27 -0
  62. package/structs/ProjectDeploymentFact.v0.ts +110 -0
  63. package/structs/ProjectPublishingFact.v0.ts +69 -0
  64. package/structs/ProjectRackConfig.v0.ts +27 -0
  65. package/structs/WorkspaceCliConfig.v0.ts +27 -0
  66. package/structs/WorkspaceConfig.v0.ts +27 -0
  67. package/structs/WorkspaceKeyConfig.v0.ts +27 -0
  68. package/structs/WorkspaceMappings.v0.ts +27 -0
  69. package/structs/WorkspaceProjectsConfig.v0.ts +27 -0
  70. package/structs/WorkspaceRepositories.v0.ts +27 -0
  71. package/structs/WorkspaceShellConfig.v0.ts +45 -0
  72. package/structs/providers/LICENSE.md +8 -0
  73. package/structs/providers/README.md +2 -0
  74. package/structs/providers/bunny.net/ProjectDeploymentFact.v0.ts +41 -0
  75. package/structs/providers/bunny.net/WorkspaceConnectionConfig.v0.ts +42 -0
  76. package/structs/providers/dynadot.com/DomainFact.v0.ts +146 -0
  77. package/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0.ts +41 -0
  78. package/structs/providers/git-scm.com/ProjectPublishingFact.v0.ts +46 -0
  79. package/structs/providers/github.com/ProjectPublishingFact.v0.ts +52 -0
  80. package/structs/providers/github.com/WorkspaceConnectionConfig.v0.ts +42 -0
  81. package/structs/providers/npmjs.com/ProjectPublishingFact.v0.ts +48 -0
  82. package/structs/providers/vercel.com/ProjectDeploymentFact.v0.ts +38 -0
  83. package/structs/providers/vercel.com/WorkspaceConnectionConfig.v0.ts +48 -0
  84. package/tsconfig.json +28 -0
  85. package/workspace-rt.ts +134 -0
  86. package/workspace.yaml +5 -0
@@ -0,0 +1,741 @@
1
+
2
+ import { join } from 'path'
3
+ import { $ } from 'bun'
4
+ import { mkdir, access, writeFile, readFile } from 'fs/promises'
5
+ import { constants } from 'fs'
6
+ import glob from 'fast-glob'
7
+ import chalk from 'chalk'
8
+ import { createHash } from 'crypto'
9
+
10
+
11
+ export async function capsule({
12
+ encapsulate,
13
+ CapsulePropertyTypes,
14
+ makeImportStack
15
+ }: {
16
+ encapsulate: any
17
+ CapsulePropertyTypes: any
18
+ makeImportStack: any
19
+ }) {
20
+ return encapsulate({
21
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
22
+ '#@stream44.studio/encapsulate/structs/Capsule.v0': {},
23
+ '#t44/structs/WorkspaceRepositories.v0': {
24
+ as: '$WorkspaceRepositories'
25
+ },
26
+ '#t44/structs/providers/npmjs.com/ProjectPublishingFact.v0': {
27
+ as: '$NpmFact'
28
+ },
29
+ '#t44/structs/ProjectPublishingFact.v0': {
30
+ as: '$StatusFact'
31
+ },
32
+ '#': {
33
+ WorkspacePrompt: {
34
+ type: CapsulePropertyTypes.Mapping,
35
+ value: 't44/caps/WorkspacePrompt.v0'
36
+ },
37
+ prepare: {
38
+ type: CapsulePropertyTypes.Function,
39
+ value: async function (this: any, { projectionDir, config, repoSourceDir }: { projectionDir: string, config: any, repoSourceDir?: string }) {
40
+ const repositoriesConfig = await this.$WorkspaceRepositories.config
41
+ const { publicNpmPackageNames, workspaceNpmPackageNames, workspacePackageSourceDirs } = await buildWorkspacePackageMaps(repositoriesConfig)
42
+
43
+ const name = config.provider.config.PackageSettings.name
44
+ const projectSourceDir = join(config.sourceDir)
45
+ const projectProjectionDir = join(projectionDir, 'packages', name.replace(/[@:\/]/g, '~'))
46
+
47
+ await mkdir(projectProjectionDir, { recursive: true })
48
+
49
+ const gitignorePath = join(projectSourceDir, '.gitignore')
50
+ const npmignorePath = join(projectSourceDir, '.npmignore')
51
+
52
+ let gitignoreExists = false
53
+ let npmignoreExists = false
54
+
55
+ try {
56
+ await access(gitignorePath, constants.F_OK)
57
+ gitignoreExists = true
58
+ } catch { }
59
+
60
+ try {
61
+ await access(npmignorePath, constants.F_OK)
62
+ npmignoreExists = true
63
+ } catch { }
64
+
65
+ const rsyncArgs = ['rsync', '-a', '--delete', '--delete-excluded', '--exclude', '.git']
66
+ if (gitignoreExists) rsyncArgs.push('--exclude-from=' + gitignorePath)
67
+ if (npmignoreExists) rsyncArgs.push('--exclude-from=' + npmignorePath)
68
+ rsyncArgs.push(projectSourceDir + '/', projectProjectionDir + '/')
69
+ await $`${rsyncArgs}`
70
+
71
+ const packageJsonPath = join(projectProjectionDir, 'package.json')
72
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
73
+ const originalPackageJson = JSON.parse(packageJsonContent)
74
+ const packageJson = JSON.parse(packageJsonContent)
75
+
76
+ // Only remove private flag if not explicitly set to private in config
77
+ const isPrivate = config.provider.config.PackageSettings?.private === true
78
+ if (!isPrivate) {
79
+ delete packageJson.private
80
+ }
81
+
82
+ // Replace package name with public npm name
83
+ packageJson.name = name
84
+
85
+ // Collect workspace packages that need to be renamed
86
+ // Do this BEFORE updating dependencies so we have the original workspace names
87
+ const renameWorkspacePackages = new Set<string>()
88
+
89
+ // Always include the package's own workspace name for self-references
90
+ const ownWorkspaceName = originalPackageJson.name
91
+ if (ownWorkspaceName && publicNpmPackageNames[ownWorkspaceName]) {
92
+ renameWorkspacePackages.add(ownWorkspaceName)
93
+ }
94
+
95
+ // Also include workspace packages used in dependencies
96
+ const dependencyFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
97
+ for (const depField of dependencyFields) {
98
+ if (packageJson[depField]) {
99
+ for (const depName of Object.keys(packageJson[depField])) {
100
+ // Check if this is a workspace package
101
+ const workspaceDepName = workspaceNpmPackageNames[depName] || depName
102
+ if (workspacePackageSourceDirs[workspaceDepName]) {
103
+ renameWorkspacePackages.add(workspaceDepName)
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ // Replace workspace package names in files for both projection and central directories
110
+ if (renameWorkspacePackages.size > 0) {
111
+ const dirsToProcess = [projectProjectionDir]
112
+ if (repoSourceDir) dirsToProcess.push(repoSourceDir)
113
+
114
+ for (const dir of dirsToProcess) {
115
+ const files = await glob('**/*.{ts,tsx,js,jsx,json,md,txt,yml,yaml}', {
116
+ cwd: dir,
117
+ absolute: true,
118
+ onlyFiles: true
119
+ })
120
+
121
+ for (const file of files) {
122
+ try {
123
+ let content = await readFile(file, 'utf-8')
124
+ let modified = false
125
+
126
+ // Only replace workspace packages that are used in dependencies
127
+ for (const workspaceName of Array.from(renameWorkspacePackages)) {
128
+ const publicName = publicNpmPackageNames[workspaceName]
129
+ if (publicName) {
130
+ const regex = new RegExp(workspaceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
131
+ if (regex.test(content)) {
132
+ content = content.replace(regex, publicName)
133
+ modified = true
134
+ }
135
+ }
136
+ }
137
+
138
+ if (modified) {
139
+ await writeFile(file, content, 'utf-8')
140
+ }
141
+ } catch (e) {
142
+ // Skip files that can't be read as text (binary files, etc.)
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ await updateWorkspaceDependencies(packageJson, workspaceNpmPackageNames, workspacePackageSourceDirs, publicNpmPackageNames)
149
+
150
+ const modifiedPackageJsonContent = JSON.stringify(packageJson, null, 4) + '\n'
151
+ await writeFile(packageJsonPath, modifiedPackageJsonContent, 'utf-8')
152
+
153
+ // Also update package.json in central source directory if provided
154
+ if (repoSourceDir) {
155
+ const centralPackageJsonPath = join(repoSourceDir, 'package.json')
156
+ await writeFile(centralPackageJsonPath, modifiedPackageJsonContent, 'utf-8')
157
+ }
158
+
159
+ const localVersion = packageJson.version
160
+
161
+ let publishedInfo: any = null
162
+ let publishedFiles: Map<string, string> = new Map()
163
+ let hasChanges = false
164
+
165
+ try {
166
+ const viewResult = await $`npm view ${name} --json`.cwd(projectProjectionDir).quiet()
167
+ const result = viewResult.text()
168
+
169
+ if (result && result.trim()) {
170
+ publishedInfo = JSON.parse(result)
171
+
172
+ if (publishedInfo && publishedInfo['dist-tags']) {
173
+ try {
174
+ // Determine which tag to compare against based on local version
175
+ const isReleaseCandidate = localVersion.includes('-rc.')
176
+ const compareTag = isReleaseCandidate ? 'next' : 'latest'
177
+ const distTags = publishedInfo['dist-tags']
178
+ const compareVersion = distTags[compareTag]
179
+
180
+ if (compareVersion) {
181
+ // Include version in directory name for proper caching
182
+ const publishedDir = join(projectionDir, 'packages-published', `${name.replace(/[@:\/]/g, '~')}@${compareVersion}`)
183
+ const publishedPackageDir = join(publishedDir, 'package')
184
+
185
+ // Check if already downloaded
186
+ let alreadyDownloaded = false
187
+ try {
188
+ await access(publishedPackageDir, constants.F_OK)
189
+ alreadyDownloaded = true
190
+ } catch { }
191
+
192
+ if (!alreadyDownloaded) {
193
+ await mkdir(publishedDir, { recursive: true })
194
+
195
+ await $`npm pack ${name}@${compareVersion}`.cwd(publishedDir).quiet()
196
+
197
+ const tarballFiles = await glob('*.tgz', { cwd: publishedDir, absolute: false })
198
+ if (tarballFiles.length > 0) {
199
+ await $`tar -xzf ${tarballFiles[0]}`.cwd(publishedDir).quiet()
200
+ await $`rm ${tarballFiles[0]}`.cwd(publishedDir).quiet()
201
+ }
202
+ }
203
+
204
+ // Read file hashes from extracted package
205
+ const publishedFilePaths = await glob('**/*', {
206
+ cwd: publishedPackageDir,
207
+ absolute: false,
208
+ onlyFiles: true
209
+ })
210
+
211
+ for (const filePath of publishedFilePaths) {
212
+ const fullPath = join(publishedPackageDir, filePath)
213
+ const fileBuffer = await readFile(fullPath)
214
+ const fileHash = createHash('sha1').update(fileBuffer).digest('hex')
215
+ publishedFiles.set(filePath, fileHash)
216
+ }
217
+ }
218
+ } catch (e) { }
219
+ }
220
+ }
221
+ } catch (error: any) {
222
+ hasChanges = true
223
+ }
224
+
225
+ const localPackResult = await $`npm pack`.cwd(projectProjectionDir).quiet()
226
+ const localTarballName = localPackResult.text().trim().split('\n').pop()?.trim()
227
+ const localTarballPath = join(projectProjectionDir, localTarballName || '')
228
+
229
+ const localTarballBuffer = await readFile(localTarballPath)
230
+ const localShasum = createHash('sha1').update(localTarballBuffer).digest('hex')
231
+ const localIntegrity = 'sha512-' + createHash('sha512').update(localTarballBuffer).digest('base64')
232
+
233
+ await $`rm ${localTarballPath}`.quiet()
234
+
235
+ let versionExistsOnNpm = false
236
+ let versionMatchesChecksum = false
237
+ let localMatchesAnyTag = false
238
+
239
+ if (publishedInfo) {
240
+ // Check if local version exists in any tag
241
+ const distTags = publishedInfo['dist-tags'] || {}
242
+ for (const [tag, version] of Object.entries(distTags)) {
243
+ if (version === localVersion) {
244
+ versionExistsOnNpm = true
245
+
246
+ // Fetch version-specific info to check shasum
247
+ try {
248
+ const versionResult = await $`npm view ${name}@${version as string} --json`.cwd(projectProjectionDir).quiet()
249
+ const versionInfo = JSON.parse(versionResult.text())
250
+ const publishedShasum = versionInfo.dist?.shasum || ''
251
+ const publishedIntegrity = versionInfo.dist?.integrity || ''
252
+
253
+ if (publishedShasum === localShasum || publishedIntegrity === localIntegrity) {
254
+ versionMatchesChecksum = true
255
+ localMatchesAnyTag = true
256
+ }
257
+ } catch (e) { }
258
+
259
+ break
260
+ }
261
+ }
262
+ }
263
+
264
+ if (!publishedInfo || !localMatchesAnyTag) {
265
+ hasChanges = true
266
+ }
267
+
268
+ return {
269
+ name,
270
+ localVersion,
271
+ projectSourceDir,
272
+ projectProjectionDir,
273
+ publishedInfo,
274
+ localShasum,
275
+ localIntegrity,
276
+ versionExistsOnNpm,
277
+ versionMatchesChecksum,
278
+ localMatchesAnyTag,
279
+ hasChanges,
280
+ publishedFiles,
281
+ workspaceNpmPackageNames,
282
+ workspacePackageSourceDirs
283
+ }
284
+ }
285
+ },
286
+ bump: {
287
+ type: CapsulePropertyTypes.Function,
288
+ value: async function (this: any, { config, options, repoSourceDir, metadata }: { config: any, options?: { rc?: boolean, release?: boolean }, repoSourceDir?: string, metadata?: any }) {
289
+ const { rc, release } = options || {}
290
+
291
+ const projectSourceDir = join(config.sourceDir)
292
+ const packageJsonPath = join(projectSourceDir, 'package.json')
293
+
294
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
295
+ const packageJson = JSON.parse(packageJsonContent)
296
+ const currentVersion = packageJson.version
297
+
298
+ // Check if current version is already newer than published version for the appropriate tag
299
+ if (metadata && metadata.publishedInfo) {
300
+ const isReleaseCandidate = currentVersion.includes('-rc.')
301
+ const targetTag = isReleaseCandidate ? 'next' : 'latest'
302
+ const distTags = metadata.publishedInfo['dist-tags'] || {}
303
+ const publishedVersion = distTags[targetTag]
304
+
305
+ if (publishedVersion) {
306
+ // Compare versions using Bun's semver - if current is already ahead, skip bump
307
+ const currentIsNewer = currentVersion !== publishedVersion &&
308
+ !Bun.semver.satisfies(currentVersion, `<=${publishedVersion}`)
309
+
310
+ if (currentIsNewer) {
311
+ console.log(chalk.yellow(` Current version ${currentVersion} is already newer than published ${targetTag}@${publishedVersion}`))
312
+ console.log(chalk.yellow(` Skipping bump - publish this version first\n`))
313
+ return
314
+ }
315
+ }
316
+ }
317
+
318
+ let newVersion: string
319
+
320
+ if (release) {
321
+ const rcMatch = currentVersion.match(/^(.+)-rc\.\d+$/)
322
+ if (rcMatch) {
323
+ newVersion = rcMatch[1]
324
+ console.log(chalk.cyan(` Removing RC suffix: ${currentVersion} → ${newVersion}`))
325
+ } else {
326
+ console.log(chalk.yellow(` Version ${currentVersion} has no RC suffix, skipping bump`))
327
+ return
328
+ }
329
+ } else if (rc) {
330
+ const rcMatch = currentVersion.match(/^(.+)-rc\.(\d+)$/)
331
+ if (rcMatch) {
332
+ const baseVersion = rcMatch[1]
333
+ const rcNumber = parseInt(rcMatch[2], 10)
334
+ newVersion = `${baseVersion}-rc.${rcNumber + 1}`
335
+ console.log(chalk.cyan(` Incrementing RC version: ${currentVersion} → ${newVersion}`))
336
+ } else {
337
+ const versionParts = currentVersion.split('.')
338
+ if (versionParts.length !== 3) {
339
+ throw new Error(`Invalid version format: ${currentVersion}`)
340
+ }
341
+ const [major, minor, patch] = versionParts
342
+ const newMinor = parseInt(minor, 10) + 1
343
+ newVersion = `${major}.${newMinor}.0-rc.1`
344
+ console.log(chalk.cyan(` Bumping minor version and adding RC: ${currentVersion} → ${newVersion}`))
345
+ }
346
+ } else {
347
+ console.log(chalk.yellow(` No version bump requested`))
348
+ return
349
+ }
350
+
351
+ packageJson.version = newVersion
352
+ const updatedContent = JSON.stringify(packageJson, null, 2) + '\n'
353
+ await writeFile(packageJsonPath, updatedContent, 'utf-8')
354
+
355
+ // Also update package.json in central source directory if provided
356
+ if (repoSourceDir) {
357
+ const centralPackageJsonPath = join(repoSourceDir, 'package.json')
358
+ await writeFile(centralPackageJsonPath, updatedContent, 'utf-8')
359
+ }
360
+
361
+ console.log(chalk.green(` ✓ Updated ${packageJsonPath} to version ${newVersion}\n`))
362
+ }
363
+ },
364
+ finalize: {
365
+ type: CapsulePropertyTypes.Function,
366
+ value: async function (this: any, { config }: { config: any }) {
367
+ const repositoriesConfig = await this.$WorkspaceRepositories.config
368
+ const { publicNpmPackageNames, workspaceNpmPackageNames, workspacePackageSourceDirs } = await buildWorkspacePackageMaps(repositoriesConfig)
369
+
370
+ const projectSourceDir = join(config.sourceDir)
371
+ const packageJsonPath = join(projectSourceDir, 'package.json')
372
+
373
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
374
+ const packageJson = JSON.parse(packageJsonContent)
375
+
376
+ await updateWorkspaceDependencies(packageJson, workspaceNpmPackageNames, workspacePackageSourceDirs, publicNpmPackageNames)
377
+
378
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8')
379
+
380
+ console.log(chalk.green(` ✓ Updated workspace dependencies in ${packageJsonPath}\n`))
381
+ }
382
+ },
383
+ push: {
384
+ type: CapsulePropertyTypes.Function,
385
+ value: async function (this: any, { projectionDir, config, metadata }: { projectionDir: string, config: any, metadata?: any }) {
386
+
387
+ if (!metadata) {
388
+ throw new Error('Push method requires metadata from prepare phase')
389
+ }
390
+
391
+ // Check if package is marked as private in config
392
+ const isPrivate = config.provider.config.PackageSettings?.private === true
393
+ if (isPrivate) {
394
+ console.log(chalk.yellow(`\n⚠️ Package is marked as private - skipping npm publish\n`))
395
+ return
396
+ }
397
+
398
+ const {
399
+ name,
400
+ localVersion,
401
+ projectSourceDir,
402
+ projectProjectionDir,
403
+ publishedInfo,
404
+ localShasum,
405
+ localIntegrity,
406
+ versionExistsOnNpm,
407
+ versionMatchesChecksum,
408
+ localMatchesAnyTag,
409
+ hasChanges,
410
+ publishedFiles
411
+ } = metadata
412
+
413
+ console.log(chalk.cyan(`\n📋 Package Details:`))
414
+ console.log(chalk.gray(` Package: ${chalk.white(name)}`))
415
+ console.log(chalk.gray(` Version: ${chalk.white(localVersion)}`))
416
+ console.log(chalk.gray(` Shasum: ${chalk.white(localShasum)}`))
417
+ console.log(chalk.gray(` Source: ${chalk.white(projectSourceDir)}`))
418
+ console.log(chalk.gray(` Build: ${chalk.white(projectProjectionDir)}`))
419
+
420
+ if (publishedInfo) {
421
+ const npmUrl = `https://www.npmjs.com/package/${name}`
422
+ const distTags = publishedInfo['dist-tags'] || {}
423
+
424
+ console.log(chalk.cyan(`\n📦 Published package: ${chalk.underline(npmUrl)}`))
425
+
426
+ // Fetch and display info for each tag
427
+ const tagInfos: Array<{ tag: string, version: string, shasum: string, matches: boolean }> = []
428
+ let anyTagMatches = false
429
+
430
+ for (const [tag, version] of Object.entries(distTags)) {
431
+ try {
432
+ const tagVersionResult = await $`npm view ${name}@${version as string} --json`.cwd(projectProjectionDir).quiet()
433
+ const tagVersionInfo = JSON.parse(tagVersionResult.text())
434
+
435
+ const publishedShasum = tagVersionInfo.dist?.shasum || ''
436
+ const publishedIntegrity = tagVersionInfo.dist?.integrity || ''
437
+ const matches = publishedShasum === localShasum || publishedIntegrity === localIntegrity
438
+
439
+ if (matches) anyTagMatches = true
440
+
441
+ tagInfos.push({ tag, version: version as string, shasum: publishedShasum, matches })
442
+ } catch (e) {
443
+ tagInfos.push({ tag, version: version as string, shasum: '', matches: false })
444
+ }
445
+ }
446
+
447
+ console.log(chalk.gray(` Versions:`))
448
+ for (const { tag, version, shasum } of tagInfos) {
449
+ console.log(chalk.white(` ${tag.padEnd(10)} v${version}`))
450
+ console.log(chalk.gray(` ${shasum}`))
451
+ }
452
+ console.log('')
453
+
454
+ // Store for later use
455
+ metadata.anyTagMatches = anyTagMatches
456
+ }
457
+
458
+ // Show file list (always show, even for first publish)
459
+ const localFilePaths = await glob('**/*', {
460
+ cwd: projectProjectionDir,
461
+ absolute: false,
462
+ onlyFiles: true
463
+ })
464
+
465
+ const localFileHashes = new Map<string, string>()
466
+ for (const filePath of localFilePaths) {
467
+ const fullPath = join(projectProjectionDir, filePath)
468
+ const fileBuffer = await readFile(fullPath)
469
+ const localHash = createHash('sha1').update(fileBuffer).digest('hex')
470
+ localFileHashes.set(filePath, localHash)
471
+ }
472
+
473
+ console.log(chalk.cyan(`\n📄 Files to be published:`))
474
+ if (publishedInfo && publishedFiles.size > 0) {
475
+ console.log(chalk.gray(` Legend: ${chalk.green('● new')} ${chalk.yellow('● modified')} ${chalk.gray('● unchanged')}\n`))
476
+ } else {
477
+ console.log(chalk.gray(` Legend: ${chalk.green('● new')}\n`))
478
+ }
479
+
480
+ for (const filePath of localFilePaths) {
481
+ const localHash = localFileHashes.get(filePath)
482
+ const publishedHash = publishedFiles.get(filePath)
483
+
484
+ const stats = await readFile(join(projectProjectionDir, filePath))
485
+ const sizeKB = (stats.length / 1024).toFixed(1) + 'kB'
486
+ const sizeB = stats.length + 'B'
487
+ const displaySize = stats.length >= 1024 ? sizeKB : sizeB
488
+
489
+ let status = ''
490
+ let color = chalk.gray
491
+
492
+ if (!publishedHash) {
493
+ status = chalk.green('● ')
494
+ color = chalk.green
495
+ } else if (publishedHash !== localHash) {
496
+ status = chalk.yellow('● ')
497
+ color = chalk.yellow
498
+ } else {
499
+ status = chalk.gray('● ')
500
+ color = chalk.gray
501
+ }
502
+
503
+ console.log(` ${status}${color(displaySize.padEnd(8))} ${color(filePath)}`)
504
+ }
505
+
506
+ if (publishedInfo && publishedFiles.size > 0) {
507
+ const deletedFiles = Array.from(publishedFiles.keys()).filter(f => !localFilePaths.includes(f as any))
508
+ if (deletedFiles.length > 0) {
509
+ console.log(chalk.red(`\n Removed files from previous version:`))
510
+ for (const fileName of deletedFiles) {
511
+ console.log(chalk.red(` ✗ ${fileName}`))
512
+ }
513
+ }
514
+ }
515
+ console.log('\n')
516
+
517
+ // Display package.json diff comparing our code to published package for same tag
518
+ if (publishedInfo && publishedFiles.size > 0) {
519
+ // Determine which tag to compare against based on local version
520
+ const isReleaseCandidate = localVersion.includes('-rc.')
521
+ const compareTag = isReleaseCandidate ? 'next' : 'latest'
522
+ const distTags = publishedInfo['dist-tags'] || {}
523
+ const compareVersion = distTags[compareTag]
524
+
525
+ if (compareVersion) {
526
+ const publishedDir = join(projectionDir, 'packages-published', `${name.replace(/[@:\/]/g, '~')}@${compareVersion}`)
527
+ const publishedPackageDir = join(publishedDir, 'package')
528
+ const publishedPackageJsonPath = join(publishedPackageDir, 'package.json')
529
+
530
+ // Check if published package.json exists
531
+ try {
532
+ await access(publishedPackageJsonPath, constants.F_OK)
533
+
534
+ // Read both package.json files
535
+ const publishedPackageJsonContent = await readFile(publishedPackageJsonPath, 'utf-8')
536
+ const localPackageJsonPath = join(projectSourceDir, 'package.json')
537
+ const localPackageJsonContent = await readFile(localPackageJsonPath, 'utf-8')
538
+
539
+ // Only show diff if they differ
540
+ if (publishedPackageJsonContent.trim() !== localPackageJsonContent.trim()) {
541
+ console.log(chalk.cyan(`\n📝 package.json changes (comparing to ${compareTag}@${compareVersion}):`))
542
+ console.log(chalk.gray('─'.repeat(80)))
543
+
544
+ try {
545
+ const diffResult = await $`diff -u ${publishedPackageJsonPath} ${localPackageJsonPath}`.quiet().nothrow()
546
+ const diffLines = diffResult.text().split('\n')
547
+
548
+ for (const line of diffLines) {
549
+ if (line.startsWith('---') || line.startsWith('+++')) {
550
+ console.log(chalk.gray(line))
551
+ } else if (line.startsWith('@@')) {
552
+ console.log(chalk.cyan(line))
553
+ } else if (line.startsWith('+')) {
554
+ console.log(chalk.green(line))
555
+ } else if (line.startsWith('-')) {
556
+ console.log(chalk.red(line))
557
+ } else {
558
+ console.log(chalk.gray(line))
559
+ }
560
+ }
561
+ } catch (e) {
562
+ // diff command failed
563
+ }
564
+
565
+ console.log(chalk.gray('─'.repeat(80)))
566
+ console.log('')
567
+ }
568
+ } catch (e) {
569
+ // Published package.json doesn't exist or can't be read
570
+ }
571
+ }
572
+ }
573
+
574
+ // Get anyTagMatches from the earlier section
575
+ const anyTagMatches = metadata.anyTagMatches || localMatchesAnyTag
576
+
577
+ if (anyTagMatches) {
578
+ console.log(chalk.green(`✓ Local package matches published version - no publish needed\n`))
579
+ return
580
+ } else if (versionExistsOnNpm && versionMatchesChecksum) {
581
+ console.log(chalk.green(`✓ Version ${localVersion} already published with matching content\n`))
582
+ return
583
+ } else if (versionExistsOnNpm && !versionMatchesChecksum) {
584
+ console.log(chalk.red(`\n✗ ERROR: Version ${localVersion} already exists on npm with different content!`))
585
+ console.log(chalk.magenta(` Run with --rc flag to bump the version and publish these changes`))
586
+ return
587
+ }
588
+
589
+ // Determine npm tag based on version
590
+ const isReleaseCandidate = localVersion.includes('-rc.')
591
+ const npmTag = isReleaseCandidate ? 'next' : 'latest'
592
+
593
+ if (publishedInfo && !versionExistsOnNpm) {
594
+ console.log(chalk.yellow(`\n⚠️ Ready to publish new package ${name} version ${localVersion} to npmjs.com`))
595
+ console.log(chalk.yellow(` Will be tagged as: ${chalk.bold(npmTag)}\n`))
596
+ } else {
597
+ console.log(chalk.yellow(`\n⚠️ Ready to publish new package ${name} version ${localVersion} to npmjs.com (first publish)`))
598
+ console.log(chalk.yellow(` Will be tagged as: ${chalk.bold(npmTag)}\n`))
599
+ }
600
+
601
+ try {
602
+ const otp = await this.WorkspacePrompt.input({
603
+ message: 'Enter your npmjs.com OTP (one-time password):',
604
+ defaultValue: '',
605
+ validate: (input: string) => {
606
+ if (!input || input.trim().length === 0) {
607
+ return 'OTP is required'
608
+ }
609
+ return true
610
+ }
611
+ })
612
+
613
+ console.log(chalk.cyan(`\n🚀 Publishing '${name}' version ${localVersion} to npm with tag '${npmTag}'...`))
614
+ await $`npm publish --access public --tag ${npmTag} --otp=${otp}`.cwd(projectProjectionDir)
615
+ console.log(chalk.green(`✅ Successfully published '${name}' version ${localVersion} to npm (tag: ${npmTag})\n`))
616
+
617
+ // Write fact files after successful publish
618
+ const npmFactName = name.replace(/[@:\/]/g, '~')
619
+
620
+ await this.$NpmFact.set('packages', npmFactName, 'NpmPackage', {
621
+ name,
622
+ version: localVersion,
623
+ private: false,
624
+ shasum: localShasum,
625
+ integrity: localIntegrity,
626
+ publishedAt: new Date().toISOString(),
627
+ npmUrl: `https://www.npmjs.com/package/${name}`
628
+ })
629
+
630
+ await this.$StatusFact.set('ProjectPublishingStatus', npmFactName, 'ProjectPublishingStatus', {
631
+ projectName: name,
632
+ provider: 'npmjs.com',
633
+ status: 'PUBLISHED',
634
+ publicUrl: `https://www.npmjs.com/package/${name}`,
635
+ updatedAt: new Date().toISOString()
636
+ })
637
+ } catch (error: any) {
638
+ if (error.message?.includes('force closed') || error.message?.includes('SIGINT')) {
639
+ console.log(chalk.red(`\nABORTED\n`))
640
+ process.exit(1)
641
+ }
642
+ throw error
643
+ }
644
+ }
645
+ },
646
+ }
647
+ }
648
+ }, {
649
+ // @ts-ignore - import.meta is supported in Bun
650
+ importMeta: import.meta,
651
+ importStack: makeImportStack(),
652
+ capsuleName: capsule['#'],
653
+ })
654
+ }
655
+ capsule['#'] = 't44/caps/providers/npmjs.com/ProjectPublishing.v0'
656
+
657
+
658
+
659
+ async function buildWorkspacePackageMaps(repositoriesConfig: any) {
660
+ const publicNpmPackageNames: Record<string, string> = {}
661
+ const workspaceNpmPackageNames: Record<string, string> = {}
662
+ const workspacePackageSourceDirs: Record<string, string> = {}
663
+
664
+ if (repositoriesConfig?.repositories) {
665
+ for (const [repoKey, repoConfig] of Object.entries(repositoriesConfig.repositories as any)) {
666
+ const providers = (repoConfig as any).providers || ((repoConfig as any).provider ? [(repoConfig as any).provider] : [])
667
+
668
+ for (const provider of providers) {
669
+ if (provider.capsule === 't44/caps/providers/npmjs.com/ProjectPublishing.v0') {
670
+ const sourceDir = (repoConfig as any).sourceDir
671
+ const packageJsonPath = join(sourceDir, 'package.json')
672
+
673
+ try {
674
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
675
+ const packageJson = JSON.parse(packageJsonContent)
676
+ const workspacePackageName = packageJson.name
677
+ const publicPackageName = provider.config.PackageSettings.name
678
+
679
+ publicNpmPackageNames[workspacePackageName] = publicPackageName
680
+ workspaceNpmPackageNames[publicPackageName] = workspacePackageName
681
+ workspacePackageSourceDirs[workspacePackageName] = sourceDir
682
+ } catch (error) {
683
+ console.warn(`Could not read package.json from ${packageJsonPath}:`, error)
684
+ }
685
+ }
686
+ }
687
+ }
688
+ }
689
+
690
+ return { publicNpmPackageNames, workspaceNpmPackageNames, workspacePackageSourceDirs }
691
+ }
692
+
693
+ async function updateWorkspaceDependencies(
694
+ packageJson: any,
695
+ workspaceNpmPackageNames: Record<string, string>,
696
+ workspacePackageSourceDirs: Record<string, string>,
697
+ publicNpmPackageNames: Record<string, string>
698
+ ) {
699
+ const dependencyFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
700
+ const currentPackageName = packageJson.name
701
+
702
+ for (const depField of dependencyFields) {
703
+ if (packageJson[depField]) {
704
+ const updatedDeps: Record<string, string> = {}
705
+
706
+ for (const [depName, depVersion] of Object.entries(packageJson[depField])) {
707
+ // Skip self-referencing dependencies
708
+ if (depName === currentPackageName) {
709
+ continue
710
+ }
711
+
712
+ if (typeof depVersion === 'string' && depVersion.startsWith('workspace:')) {
713
+ try {
714
+ const workspaceDepName = workspaceNpmPackageNames[depName] || depName
715
+ const depSourceDir = workspacePackageSourceDirs[workspaceDepName]
716
+
717
+ if (!depSourceDir) {
718
+ console.warn(`Could not find source directory for workspace dependency ${depName} (${workspaceDepName})`)
719
+ continue
720
+ }
721
+
722
+ const depPackageJsonPath = join(depSourceDir, 'package.json')
723
+ const depPackageJsonContent = await readFile(depPackageJsonPath, 'utf-8')
724
+ const depPackageJson = JSON.parse(depPackageJsonContent)
725
+
726
+ // Replace workspace package name with public package name
727
+ const publicDepName = publicNpmPackageNames[workspaceDepName] || depName
728
+ updatedDeps[publicDepName] = `^${depPackageJson.version}`
729
+ } catch (error) {
730
+ console.warn(`Could not resolve workspace dependency ${depName}:`, error)
731
+ }
732
+ } else {
733
+ // Keep non-workspace dependencies as-is
734
+ updatedDeps[depName] = depVersion as string
735
+ }
736
+ }
737
+
738
+ packageJson[depField] = updatedDeps
739
+ }
740
+ }
741
+ }