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,536 @@
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
+ function detectIndent(content: string): number {
11
+ const match = content.match(/^\{\s*\n([ \t]+)/)
12
+ if (match) {
13
+ return match[1].length
14
+ }
15
+ return 2
16
+ }
17
+
18
+ export async function capsule({
19
+ encapsulate,
20
+ CapsulePropertyTypes,
21
+ makeImportStack
22
+ }: {
23
+ encapsulate: any
24
+ CapsulePropertyTypes: any
25
+ makeImportStack: any
26
+ }) {
27
+ return encapsulate({
28
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
29
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
30
+ '#t44/structs/providers/npmjs.com/ProjectPublishingFact': {
31
+ as: '$NpmFact'
32
+ },
33
+ '#t44/structs/ProjectPublishingFact': {
34
+ as: '$StatusFact'
35
+ },
36
+ '#': {
37
+ WorkspacePrompt: {
38
+ type: CapsulePropertyTypes.Mapping,
39
+ value: 't44/caps/WorkspacePrompt'
40
+ },
41
+ ProjectCatalogs: {
42
+ type: CapsulePropertyTypes.Mapping,
43
+ value: 't44/caps/ProjectCatalogs'
44
+ },
45
+ prepare: {
46
+ type: CapsulePropertyTypes.Function,
47
+ value: async function (this: any, { projectionDir, config }: { projectionDir: string, config: any }) {
48
+
49
+ const name = config.provider.config.PackageSettings.name
50
+ const projectSourceDir = join(config.sourceDir)
51
+ const stageDir = join(projectionDir, 'stage', name.replace(/[\/]/g, '~'))
52
+
53
+ await mkdir(stageDir, { recursive: true })
54
+
55
+ const gitignorePath = join(projectSourceDir, '.gitignore')
56
+ const npmignorePath = join(projectSourceDir, '.npmignore')
57
+
58
+ let gitignoreExists = false
59
+ let npmignoreExists = false
60
+
61
+ try {
62
+ await access(gitignorePath, constants.F_OK)
63
+ gitignoreExists = true
64
+ } catch { }
65
+
66
+ try {
67
+ await access(npmignorePath, constants.F_OK)
68
+ npmignoreExists = true
69
+ } catch { }
70
+
71
+ const rsyncArgs = ['rsync', '-a', '--delete', '--delete-excluded', '--exclude', '.git']
72
+ if (gitignoreExists) rsyncArgs.push('--exclude-from=' + gitignorePath)
73
+ if (npmignoreExists) rsyncArgs.push('--exclude-from=' + npmignorePath)
74
+ rsyncArgs.push(projectSourceDir + '/', stageDir + '/')
75
+ await $`${rsyncArgs}`
76
+
77
+ const packageJsonPath = join(stageDir, 'package.json')
78
+ const packageJsonContent = await readFile(packageJsonPath, 'utf-8')
79
+ const indent = detectIndent(packageJsonContent)
80
+ const packageJson = JSON.parse(packageJsonContent)
81
+
82
+ // Replace package name with public npm name
83
+ packageJson.name = name
84
+
85
+ const modifiedPackageJsonContent = JSON.stringify(packageJson, null, indent) + '\n'
86
+ if (modifiedPackageJsonContent !== packageJsonContent) {
87
+ await writeFile(packageJsonPath, modifiedPackageJsonContent, 'utf-8')
88
+ }
89
+
90
+
91
+ const localVersion = packageJson.version
92
+
93
+ let publishedInfo: any = null
94
+ let publishedFiles: Map<string, string> = new Map()
95
+ let hasChanges = false
96
+
97
+ try {
98
+ const viewResult = await $`npm view ${name} --json`.cwd(stageDir).quiet()
99
+ const result = viewResult.text()
100
+
101
+ if (result && result.trim()) {
102
+ publishedInfo = JSON.parse(result)
103
+
104
+ if (publishedInfo && publishedInfo['dist-tags']) {
105
+ try {
106
+ // Determine which tag to compare against based on local version
107
+ const isReleaseCandidate = localVersion.includes('-rc.')
108
+ const compareTag = isReleaseCandidate ? 'next' : 'latest'
109
+ const distTags = publishedInfo['dist-tags']
110
+ const compareVersion = distTags[compareTag]
111
+
112
+ if (compareVersion) {
113
+ // Include version in directory name for proper caching
114
+ const mirrorDir = join(projectionDir, 'mirror', `${name.replace(/[\/]/g, '~')}@${compareVersion}`)
115
+ const publishedPackageDir = join(mirrorDir, 'package')
116
+
117
+ // Check if already downloaded
118
+ let alreadyDownloaded = false
119
+ try {
120
+ await access(publishedPackageDir, constants.F_OK)
121
+ alreadyDownloaded = true
122
+ } catch { }
123
+
124
+ if (!alreadyDownloaded) {
125
+ await mkdir(mirrorDir, { recursive: true })
126
+
127
+ await $`npm pack ${name}@${compareVersion}`.cwd(mirrorDir).quiet()
128
+
129
+ const tarballFiles = await glob('*.tgz', { cwd: mirrorDir, absolute: false })
130
+ if (tarballFiles.length > 0) {
131
+ await $`tar -xzf ${tarballFiles[0]}`.cwd(mirrorDir).quiet()
132
+ await $`rm ${tarballFiles[0]}`.cwd(mirrorDir).quiet()
133
+ }
134
+ }
135
+
136
+ // Read file hashes from extracted package
137
+ const publishedFilePaths = await glob('**/*', {
138
+ cwd: publishedPackageDir,
139
+ absolute: false,
140
+ onlyFiles: true
141
+ })
142
+
143
+ for (const filePath of publishedFilePaths) {
144
+ const fullPath = join(publishedPackageDir, filePath)
145
+ const fileBuffer = await readFile(fullPath)
146
+ const fileHash = createHash('sha1').update(fileBuffer).digest('hex')
147
+ publishedFiles.set(filePath, fileHash)
148
+ }
149
+ }
150
+ } catch (e) { }
151
+ }
152
+ }
153
+ } catch (error: any) {
154
+ hasChanges = true
155
+ }
156
+
157
+ const localPackResult = await $`npm pack`.cwd(stageDir).quiet()
158
+ const localTarballName = localPackResult.text().trim().split('\n').pop()?.trim()
159
+ const localTarballPath = join(stageDir, localTarballName || '')
160
+
161
+ const localTarballBuffer = await readFile(localTarballPath)
162
+ const localShasum = createHash('sha1').update(localTarballBuffer).digest('hex')
163
+ const localIntegrity = 'sha512-' + createHash('sha512').update(localTarballBuffer).digest('base64')
164
+
165
+ await $`rm ${localTarballPath}`.quiet()
166
+
167
+ let versionExistsOnNpm = false
168
+ let versionMatchesChecksum = false
169
+ let localMatchesAnyTag = false
170
+
171
+ if (publishedInfo) {
172
+ // Check if local version exists in any tag
173
+ const distTags = publishedInfo['dist-tags'] || {}
174
+ for (const [tag, version] of Object.entries(distTags)) {
175
+ if (version === localVersion) {
176
+ versionExistsOnNpm = true
177
+
178
+ // Fetch version-specific info to check shasum
179
+ try {
180
+ const versionResult = await $`npm view ${name}@${version as string} --json`.cwd(stageDir).quiet()
181
+ const versionInfo = JSON.parse(versionResult.text())
182
+ const publishedShasum = versionInfo.dist?.shasum || ''
183
+ const publishedIntegrity = versionInfo.dist?.integrity || ''
184
+
185
+ if (publishedShasum === localShasum || publishedIntegrity === localIntegrity) {
186
+ versionMatchesChecksum = true
187
+ localMatchesAnyTag = true
188
+ }
189
+ } catch (e) { }
190
+
191
+ break
192
+ }
193
+ }
194
+ }
195
+
196
+ if (!publishedInfo || !localMatchesAnyTag) {
197
+ hasChanges = true
198
+ }
199
+
200
+ return {
201
+ name,
202
+ localVersion,
203
+ projectSourceDir,
204
+ stageDir,
205
+ publishedInfo,
206
+ localShasum,
207
+ localIntegrity,
208
+ versionExistsOnNpm,
209
+ versionMatchesChecksum,
210
+ localMatchesAnyTag,
211
+ hasChanges,
212
+ publishedFiles
213
+ }
214
+ }
215
+ },
216
+ push: {
217
+ type: CapsulePropertyTypes.Function,
218
+ value: async function (this: any, { projectionDir, config, metadata }: { projectionDir: string, config: any, metadata?: any }) {
219
+
220
+ if (!metadata) {
221
+ throw new Error('Push method requires metadata from prepare phase')
222
+ }
223
+
224
+ // Check if package.json has private: true
225
+ const stagePackageJsonPath = join(metadata.stageDir, 'package.json')
226
+ const stagePackageJson = JSON.parse(await readFile(stagePackageJsonPath, 'utf-8'))
227
+ const publicSetting = config.provider.config.PackageSettings?.public
228
+
229
+ if (stagePackageJson.private === true) {
230
+ if (publicSetting === undefined) {
231
+ const sourcePackageJsonPath = join(metadata.projectSourceDir, 'package.json')
232
+ console.log(chalk.yellow(`\n⚠️ Skipping npm publish — private: true in ${sourcePackageJsonPath}\n`))
233
+ } else {
234
+ console.log(chalk.yellow(`\n⚠️ Skipping npm publish — package is private\n`))
235
+ }
236
+ return
237
+ }
238
+
239
+ const {
240
+ name,
241
+ localVersion,
242
+ projectSourceDir,
243
+ stageDir,
244
+ publishedInfo,
245
+ localShasum,
246
+ localIntegrity,
247
+ versionExistsOnNpm,
248
+ versionMatchesChecksum,
249
+ localMatchesAnyTag,
250
+ hasChanges,
251
+ publishedFiles
252
+ } = metadata
253
+
254
+ console.log(chalk.cyan(`\n📋 Package Details:`))
255
+ console.log(chalk.gray(` Package: ${chalk.white(name)}`))
256
+ console.log(chalk.gray(` Version: ${chalk.white(localVersion)}`))
257
+ console.log(chalk.gray(` Shasum: ${chalk.white(localShasum)}`))
258
+ console.log(chalk.gray(` Source: ${chalk.white(projectSourceDir)}`))
259
+ console.log(chalk.gray(` Build: ${chalk.white(stageDir)}`))
260
+
261
+ if (publishedInfo) {
262
+ const npmUrl = `https://www.npmjs.com/package/${name}`
263
+ const distTags = publishedInfo['dist-tags'] || {}
264
+
265
+ console.log(chalk.cyan(`\n📦 Published package: ${chalk.underline(npmUrl)}`))
266
+
267
+ // Fetch and display info for each tag
268
+ const tagInfos: Array<{ tag: string, version: string, shasum: string, matches: boolean }> = []
269
+ let anyTagMatches = false
270
+
271
+ for (const [tag, version] of Object.entries(distTags)) {
272
+ try {
273
+ const tagVersionResult = await $`npm view ${name}@${version as string} --json`.cwd(stageDir).quiet()
274
+ const tagVersionInfo = JSON.parse(tagVersionResult.text())
275
+
276
+ const publishedShasum = tagVersionInfo.dist?.shasum || ''
277
+ const publishedIntegrity = tagVersionInfo.dist?.integrity || ''
278
+ const matches = publishedShasum === localShasum || publishedIntegrity === localIntegrity
279
+
280
+ if (matches) anyTagMatches = true
281
+
282
+ tagInfos.push({ tag, version: version as string, shasum: publishedShasum, matches })
283
+ } catch (e) {
284
+ tagInfos.push({ tag, version: version as string, shasum: '', matches: false })
285
+ }
286
+ }
287
+
288
+ console.log(chalk.gray(` Versions:`))
289
+ for (const { tag, version, shasum } of tagInfos) {
290
+ console.log(chalk.white(` ${tag.padEnd(10)} v${version}`))
291
+ console.log(chalk.gray(` ${shasum}`))
292
+ }
293
+ console.log('')
294
+
295
+ // Store for later use
296
+ metadata.anyTagMatches = anyTagMatches
297
+ }
298
+
299
+ // Get anyTagMatches from the earlier section
300
+ const anyTagMatches = metadata.anyTagMatches || localMatchesAnyTag
301
+
302
+ if (anyTagMatches) {
303
+ console.log(chalk.green(`\n✓ Local package matches published version - no publish needed\n`))
304
+ return
305
+ } else if (versionExistsOnNpm && versionMatchesChecksum) {
306
+ console.log(chalk.green(`\n✓ Version ${localVersion} already published with matching content\n`))
307
+ return
308
+ } else if (versionExistsOnNpm && !versionMatchesChecksum) {
309
+ console.log(chalk.red(`\n✗ ERROR: Version ${localVersion} already exists on npm with different content!`))
310
+ console.log(chalk.magenta(` Run with --rc flag to bump the version and publish these changes`))
311
+ return
312
+ }
313
+
314
+ // Show file list only when publishing
315
+ const localFilePaths = await glob('**/*', {
316
+ cwd: stageDir,
317
+ absolute: false,
318
+ onlyFiles: true
319
+ })
320
+
321
+ const localFileHashes = new Map<string, string>()
322
+ for (const filePath of localFilePaths) {
323
+ const fullPath = join(stageDir, filePath)
324
+ const fileBuffer = await readFile(fullPath)
325
+ const localHash = createHash('sha1').update(fileBuffer).digest('hex')
326
+ localFileHashes.set(filePath, localHash)
327
+ }
328
+
329
+ console.log(chalk.cyan(`\n📄 Files to be published:`))
330
+ if (publishedInfo && publishedFiles.size > 0) {
331
+ console.log(chalk.gray(` Legend: ${chalk.green('● new')} ${chalk.yellow('● modified')} ${chalk.gray('● unchanged')}\n`))
332
+ } else {
333
+ console.log(chalk.gray(` Legend: ${chalk.green('● new')}\n`))
334
+ }
335
+
336
+ for (const filePath of localFilePaths) {
337
+ const localHash = localFileHashes.get(filePath)
338
+ const publishedHash = publishedFiles.get(filePath)
339
+
340
+ const stats = await readFile(join(stageDir, filePath))
341
+ const sizeKB = (stats.length / 1024).toFixed(1) + 'kB'
342
+ const sizeB = stats.length + 'B'
343
+ const displaySize = stats.length >= 1024 ? sizeKB : sizeB
344
+
345
+ let status = ''
346
+ let color = chalk.gray
347
+
348
+ if (!publishedHash) {
349
+ status = chalk.green('● ')
350
+ color = chalk.green
351
+ } else if (publishedHash !== localHash) {
352
+ status = chalk.yellow('● ')
353
+ color = chalk.yellow
354
+ } else {
355
+ status = chalk.gray('● ')
356
+ color = chalk.gray
357
+ }
358
+
359
+ console.log(` ${status}${color(displaySize.padEnd(8))} ${color(filePath)}`)
360
+ }
361
+
362
+ if (publishedInfo && publishedFiles.size > 0) {
363
+ const deletedFiles = Array.from(publishedFiles.keys()).filter(f => !localFilePaths.includes(f as any))
364
+ if (deletedFiles.length > 0) {
365
+ console.log(chalk.red(`\n Removed files from previous version:`))
366
+ for (const fileName of deletedFiles) {
367
+ console.log(chalk.red(` ✗ ${fileName}`))
368
+ }
369
+ }
370
+ }
371
+ console.log('\n')
372
+
373
+ // Display package.json diff comparing our code to published package for same tag
374
+ if (publishedInfo && publishedFiles.size > 0) {
375
+ // Determine which tag to compare against based on local version
376
+ const isReleaseCandidate = localVersion.includes('-rc.')
377
+ const compareTag = isReleaseCandidate ? 'next' : 'latest'
378
+ const distTags = publishedInfo['dist-tags'] || {}
379
+ const compareVersion = distTags[compareTag]
380
+
381
+ if (compareVersion) {
382
+ const mirrorDir = join(projectionDir, 'mirror', `${name.replace(/[\/]/g, '~')}@${compareVersion}`)
383
+ const publishedPackageDir = join(mirrorDir, 'package')
384
+ const publishedPackageJsonPath = join(publishedPackageDir, 'package.json')
385
+
386
+ // Check if published package.json exists
387
+ try {
388
+ await access(publishedPackageJsonPath, constants.F_OK)
389
+
390
+ // Read both package.json files
391
+ const publishedPackageJsonContent = await readFile(publishedPackageJsonPath, 'utf-8')
392
+ const localPackageJsonPath = join(projectSourceDir, 'package.json')
393
+ const localPackageJsonContent = await readFile(localPackageJsonPath, 'utf-8')
394
+
395
+ // Only show diff if they differ
396
+ if (publishedPackageJsonContent.trim() !== localPackageJsonContent.trim()) {
397
+ console.log(chalk.cyan(`\n📝 package.json changes (comparing to ${compareTag}@${compareVersion}):`))
398
+ console.log(chalk.gray('─'.repeat(80)))
399
+
400
+ try {
401
+ const diffResult = await $`diff -u ${publishedPackageJsonPath} ${localPackageJsonPath}`.quiet().nothrow()
402
+ const diffLines = diffResult.text().split('\n')
403
+
404
+ for (const line of diffLines) {
405
+ if (line.startsWith('---') || line.startsWith('+++')) {
406
+ console.log(chalk.gray(line))
407
+ } else if (line.startsWith('@@')) {
408
+ console.log(chalk.cyan(line))
409
+ } else if (line.startsWith('+')) {
410
+ console.log(chalk.green(line))
411
+ } else if (line.startsWith('-')) {
412
+ console.log(chalk.red(line))
413
+ } else {
414
+ console.log(chalk.gray(line))
415
+ }
416
+ }
417
+ } catch (e) {
418
+ // diff command failed
419
+ }
420
+
421
+ console.log(chalk.gray('─'.repeat(80)))
422
+ console.log('')
423
+ }
424
+ } catch (e) {
425
+ // Published package.json doesn't exist or can't be read
426
+ }
427
+ }
428
+ }
429
+
430
+ // Determine npm tag based on version
431
+ const isReleaseCandidate = localVersion.includes('-rc.')
432
+ const npmTag = isReleaseCandidate ? 'next' : 'latest'
433
+
434
+ if (publishedInfo && !versionExistsOnNpm) {
435
+ console.log(chalk.yellow(`\n⚠️ Ready to publish new package ${name} version ${localVersion} to npmjs.com`))
436
+ console.log(chalk.yellow(` Will be tagged as: ${chalk.bold(npmTag)}\n`))
437
+ } else {
438
+ console.log(chalk.yellow(`\n⚠️ Ready to publish new package ${name} version ${localVersion} to npmjs.com (first publish)`))
439
+ console.log(chalk.yellow(` Will be tagged as: ${chalk.bold(npmTag)}\n`))
440
+ }
441
+
442
+ // Check if logged in to npm
443
+ try {
444
+ await $`npm whoami`.quiet()
445
+ } catch {
446
+ console.log(chalk.yellow(`\n⚠️ You must login to npmjs.com to proceed.\n`))
447
+ const loginProc = Bun.spawn(['npm', 'login'], {
448
+ stdin: 'inherit',
449
+ stdout: 'inherit',
450
+ stderr: 'inherit'
451
+ })
452
+ await loginProc.exited
453
+ }
454
+
455
+ try {
456
+ const otp = await this.WorkspacePrompt.input({
457
+ message: 'Enter your npmjs.com OTP (one-time password):',
458
+ defaultValue: '',
459
+ validate: (input: string) => {
460
+ if (!input || input.trim().length === 0) {
461
+ return 'OTP is required'
462
+ }
463
+ return true
464
+ }
465
+ })
466
+
467
+ console.log(chalk.cyan(`\n🚀 Publishing '${name}' version ${localVersion} to npm with tag '${npmTag}'...`))
468
+ await $`npm publish --access public --tag ${npmTag} --otp=${otp}`.cwd(stageDir)
469
+ console.log(chalk.green(`✅ Successfully published '${name}' version ${localVersion} to npm (tag: ${npmTag})\n`))
470
+
471
+ // Write fact files after successful publish
472
+ const npmFactName = name.replace(/[\/]/g, '~')
473
+
474
+ await this.$NpmFact.set(npmFactName, {
475
+ name,
476
+ version: localVersion,
477
+ private: false,
478
+ shasum: localShasum,
479
+ integrity: localIntegrity,
480
+ publishedAt: new Date().toISOString(),
481
+ npmUrl: `https://www.npmjs.com/package/${name}`
482
+ })
483
+
484
+ await this.$StatusFact.set(npmFactName, {
485
+ projectName: name,
486
+ provider: 'npmjs.com',
487
+ status: 'PUBLISHED',
488
+ publicUrl: `https://www.npmjs.com/package/${name}`
489
+ })
490
+ } catch (error: any) {
491
+ if (error.message?.includes('force closed') || error.message?.includes('SIGINT')) {
492
+ console.log(chalk.red(`\nABORTED\n`))
493
+ process.exit(1)
494
+ }
495
+ throw error
496
+ }
497
+ }
498
+ },
499
+ afterPush: {
500
+ type: CapsulePropertyTypes.Function,
501
+ value: async function (this: any, { repoName, metadata }: {
502
+ repoName: string
503
+ metadata?: any
504
+ }): Promise<void> {
505
+ if (!metadata) return
506
+
507
+ const isReleaseCandidate = metadata.localVersion?.includes('-rc.')
508
+ const npmTag = isReleaseCandidate ? 'next' : 'latest'
509
+
510
+ const npmData: Record<string, any> = {
511
+ distTags: {
512
+ [npmTag]: {
513
+ version: metadata.localVersion,
514
+ shasum: metadata.localShasum,
515
+ integrity: metadata.localIntegrity,
516
+ }
517
+ }
518
+ }
519
+
520
+ await this.ProjectCatalogs.updateCatalogRepository({
521
+ repoName,
522
+ providerKey: '#' + capsule['#'],
523
+ providerData: npmData,
524
+ })
525
+ }
526
+ },
527
+ }
528
+ }
529
+ }, {
530
+ // @ts-ignore - import.meta is supported in Bun
531
+ importMeta: import.meta,
532
+ importStack: makeImportStack(),
533
+ capsuleName: capsule['#'],
534
+ })
535
+ }
536
+ capsule['#'] = 't44/caps/providers/npmjs.com/ProjectPublishing'