publish-nightly.mjs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. #!/usr/bin/env node
  2. /**
  3. * Nightly publish script for VS Code extension
  4. * Converts package.json to testing version, packages, publishes, and restores
  5. *
  6. * This script:
  7. * 1. Backs up the original package.json
  8. * 2. Updates package.json with:
  9. * - New version (major.minor.timestamp format)
  10. * - Changes name to "cline-nightly"
  11. * - Changes displayName to "Cline (Nightly)"
  12. * 3. Packages the extension as a .vsix file
  13. * 4. Publishes to VS Code Marketplace (if VSCE_PAT is set)
  14. * 5. Publishes to OpenVSX Registry (if OVSX_PAT is set)
  15. * 6. Restores the original package.json
  16. *
  17. * Usage:
  18. * npm run publish:marketplace:nightly
  19. * npm run publish:marketplace:nightly -- --dry-run
  20. *
  21. * Environment variables:
  22. * VSCE_PAT - Personal Access Token for VS Code Marketplace
  23. * OVSX_PAT - Personal Access Token for OpenVSX Registry
  24. *
  25. * Dependencies:
  26. * - vsce (VS Code Extension Manager)
  27. * - ovsx (OpenVSX CLI)
  28. */
  29. import { execFileSync, execSync } from "node:child_process"
  30. import fs from "node:fs"
  31. import path from "node:path"
  32. import { fileURLToPath } from "node:url"
  33. // Get __dirname equivalent in ES modules
  34. const __filename = fileURLToPath(import.meta.url)
  35. const __dirname = path.dirname(__filename)
  36. // ANSI color codes for console output
  37. const colors = {
  38. reset: "\x1b[0m",
  39. red: "\x1b[31m",
  40. green: "\x1b[32m",
  41. yellow: "\x1b[33m",
  42. }
  43. // Logging utilities
  44. const log = {
  45. info: (msg) => console.log(`${colors.green}[INFO]${colors.reset} ${msg}`),
  46. warn: (msg) => console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`),
  47. error: (msg) => console.error(`${colors.red}[ERROR]${colors.reset} ${msg}`),
  48. }
  49. // Configuration
  50. const config = {
  51. // The name and display name for the nightly version
  52. nightlyName: "cline-nightly",
  53. nightlyDisplayName: "Cline (Nightly)",
  54. projectRoot: path.join(__dirname, ".."),
  55. get packageJsonPath() {
  56. return path.join(this.projectRoot, "package.json")
  57. },
  58. get packageBackupPath() {
  59. return path.join(this.projectRoot, "package.json.backup")
  60. },
  61. get distDir() {
  62. return path.join(this.projectRoot, "dist")
  63. },
  64. get vsixPath() {
  65. return path.join(this.distDir, "cline-nightly.vsix")
  66. },
  67. }
  68. // Utility class for managing the publish process
  69. class NightlyPublisher {
  70. constructor() {
  71. this.originalPackageJson = null
  72. this.hasBackup = false
  73. }
  74. /**
  75. * Check if required dependencies are installed
  76. */
  77. checkDependencies() {
  78. const dependencies = [
  79. { name: "vsce", check: "vsce --version" },
  80. { name: "npx", check: "npx --version" },
  81. ]
  82. const missing = []
  83. for (const dep of dependencies) {
  84. try {
  85. execSync(dep.check, { stdio: "ignore" })
  86. } catch {
  87. missing.push(dep.name)
  88. }
  89. }
  90. if (missing.length > 0) {
  91. throw new Error(
  92. `Missing required dependencies: ${missing.join(", ")}. Please install them before running this script.`,
  93. )
  94. }
  95. log.info("All dependencies are installed")
  96. }
  97. /**
  98. * Check if a command exists
  99. */
  100. commandExists(command) {
  101. try {
  102. execSync(`which ${command}`, { stdio: "ignore" })
  103. return true
  104. } catch {
  105. return false
  106. }
  107. }
  108. /**
  109. * Create backup of package.json
  110. */
  111. backupPackageJson() {
  112. if (!fs.existsSync(config.packageJsonPath)) {
  113. throw new Error(`package.json not found at ${config.packageJsonPath}`)
  114. }
  115. log.info("Backing up original package.json")
  116. this.originalPackageJson = fs.readFileSync(config.packageJsonPath, "utf-8")
  117. fs.writeFileSync(config.packageBackupPath, this.originalPackageJson)
  118. this.hasBackup = true
  119. }
  120. /**
  121. * Restore original package.json
  122. */
  123. restorePackageJson() {
  124. if (this.hasBackup && fs.existsSync(config.packageBackupPath)) {
  125. log.info("Restoring original package.json")
  126. fs.writeFileSync(config.packageJsonPath, this.originalPackageJson)
  127. fs.unlinkSync(config.packageBackupPath)
  128. this.hasBackup = false
  129. }
  130. }
  131. /**
  132. * Generate new version with timestamp
  133. * Format: major.minor.timestamp
  134. */
  135. generateVersion(currentVersion) {
  136. // Extract major.minor from current version (e.g., "3.27.1" -> "3.27")
  137. const versionParts = currentVersion.split(".")
  138. if (versionParts.length < 2) {
  139. throw new Error(`Invalid version format: ${currentVersion}`)
  140. }
  141. const major = versionParts[0]
  142. const minor = versionParts[1]
  143. const timestamp = Math.floor(Date.now() / 1000)
  144. return `${major}.${minor}.${timestamp}`
  145. }
  146. /**
  147. * Update package.json with nightly configuration
  148. */
  149. updatePackageJson() {
  150. // Replace any occurrences cline. or claude-dev with nightly name
  151. const rawContent = fs.readFileSync(config.packageJsonPath, "utf-8")
  152. const content = rawContent.replaceAll("claude-dev", config.nightlyName).replaceAll('"cline.', `"${config.nightlyName}.`)
  153. const pkg = JSON.parse(content)
  154. const currentVersion = pkg.version
  155. if (!currentVersion) {
  156. throw new Error("Could not read version from package.json")
  157. }
  158. log.info(`Current version: ${currentVersion}`)
  159. const newVersion = this.generateVersion(currentVersion)
  160. log.info(`New version: ${newVersion}`)
  161. // Update package.json fields
  162. pkg.version = newVersion
  163. pkg.name = config.nightlyName
  164. pkg.displayName = config.nightlyDisplayName
  165. pkg.contributes.viewsContainers.activitybar.title = config.nightlyDisplayName
  166. // Save updated package.json
  167. log.info("Updating package.json for nightly build")
  168. fs.writeFileSync(config.packageJsonPath, JSON.stringify(pkg, null, "\t"))
  169. return newVersion
  170. }
  171. /**
  172. * Package the extension
  173. */
  174. packageExtension() {
  175. // Ensure dist directory exists
  176. if (!fs.existsSync(config.distDir)) {
  177. fs.mkdirSync(config.distDir, { recursive: true })
  178. }
  179. log.info("Packaging extension")
  180. const args = [
  181. "package",
  182. "--pre-release",
  183. "--no-update-package-json",
  184. "--no-git-tag-version",
  185. "--allow-package-secrets",
  186. "sendgrid",
  187. "--out",
  188. config.vsixPath,
  189. ]
  190. try {
  191. execFileSync("vsce", args, {
  192. stdio: "inherit",
  193. cwd: config.projectRoot,
  194. })
  195. log.info(`Package created: ${config.vsixPath}`)
  196. } catch (error) {
  197. throw new Error(`Failed to package extension: ${error.message}`)
  198. }
  199. }
  200. /**
  201. * Publish to VS Code Marketplace
  202. */
  203. publishToVSCodeMarketplace() {
  204. const token = process.env.VSCE_PAT
  205. if (!token) {
  206. log.warn("VSCE_PAT not set, skipping VS Code Marketplace publish")
  207. return false
  208. }
  209. log.info("Publishing to VS Code Marketplace")
  210. const args = ["publish", "--pre-release", "--no-git-tag-version", "--packagePath", config.vsixPath]
  211. try {
  212. execFileSync("vsce", args, {
  213. env: { ...process.env, VSCE_PAT: token },
  214. stdio: "inherit",
  215. cwd: config.projectRoot,
  216. })
  217. log.info("Successfully published to VS Code Marketplace")
  218. return true
  219. } catch (error) {
  220. throw new Error(`Failed to publish to VS Code Marketplace: ${error.message}`)
  221. }
  222. }
  223. /**
  224. * Publish to OpenVSX Registry
  225. */
  226. publishToOpenVSX() {
  227. const token = process.env.OVSX_PAT
  228. if (!token) {
  229. log.warn("OVSX_PAT not set, skipping OpenVSX Registry publish")
  230. return false
  231. }
  232. log.info("Publishing to OpenVSX Registry")
  233. const args = ["ovsx", "publish", "--pre-release", "--packagePath", config.vsixPath, "--pat", token]
  234. try {
  235. execFileSync("npx", args, {
  236. stdio: "inherit",
  237. cwd: config.projectRoot,
  238. })
  239. log.info("Successfully published to OpenVSX Registry")
  240. return true
  241. } catch (error) {
  242. throw new Error(`Failed to publish to OpenVSX Registry: ${error.message}`)
  243. }
  244. }
  245. /**
  246. * Main execution flow
  247. */
  248. async run(isDryRun = false) {
  249. try {
  250. log.info(`Starting nightly publish process${isDryRun ? " (dry run)" : ""}`)
  251. // Step 1: Check dependencies
  252. this.checkDependencies()
  253. // Step 2: Backup package.json
  254. this.backupPackageJson()
  255. // Step 3: Update package.json
  256. const newVersion = this.updatePackageJson()
  257. // Step 4: Package extension
  258. this.packageExtension()
  259. // Step 5: Publish to marketplaces (skip if dry run)
  260. let vsCodePublished = false
  261. let openVSXPublished = false
  262. if (isDryRun) {
  263. log.info("Dry run mode: Skipping marketplace publishing")
  264. } else {
  265. vsCodePublished = this.publishToVSCodeMarketplace()
  266. openVSXPublished = this.publishToOpenVSX()
  267. }
  268. // Summary
  269. log.info(`Nightly publish process completed successfully${isDryRun ? " (dry run)" : ""}`)
  270. log.info(`Package created for v${newVersion}: ${config.vsixPath}`)
  271. if (!isDryRun && !vsCodePublished && !openVSXPublished) {
  272. log.warn("Extension was packaged but not published to any marketplace")
  273. log.warn("Set VSCE_PAT and/or OVSX_PAT environment variables to enable publishing")
  274. }
  275. } catch (error) {
  276. log.error(`Publish failed: ${error.message}`)
  277. process.exit(1)
  278. } finally {
  279. // Always restore package.json
  280. this.restorePackageJson()
  281. }
  282. }
  283. }
  284. // Handle cleanup on process exit
  285. const publisher = new NightlyPublisher()
  286. process.on("exit", () => {
  287. publisher.restorePackageJson()
  288. })
  289. process.on("SIGINT", () => {
  290. log.info("\nInterrupted, cleaning up...")
  291. publisher.restorePackageJson()
  292. process.exit(130)
  293. })
  294. process.on("SIGTERM", () => {
  295. log.info("\nTerminated, cleaning up...")
  296. publisher.restorePackageJson()
  297. process.exit(143)
  298. })
  299. // Parse command line arguments
  300. const args = process.argv.slice(2)
  301. const isDryRun = args.includes("--dry-run") || args.includes("-n")
  302. const showHelp = args.includes("--help") || args.includes("-h")
  303. if (showHelp) {
  304. console.log(`
  305. Nightly publish script for VS Code extension
  306. Usage:
  307. npm run publish:marketplace:nightly [options]
  308. Options:
  309. --dry-run, -n Run without actually publishing (package only)
  310. --help, -h Show this help message
  311. Environment variables:
  312. VSCE_PAT Personal Access Token for VS Code Marketplace
  313. OVSX_PAT Personal Access Token for OpenVSX Registry
  314. Examples:
  315. npm run publish:marketplace:nightly # Full publish
  316. npm run publish:marketplace:nightly -- --dry-run # Package only
  317. VSCE_PAT="token" npm run publish:marketplace:nightly # Publish to VS Code only
  318. `)
  319. process.exit(0)
  320. }
  321. // Run the publisher
  322. publisher.run(isDryRun).catch((error) => {
  323. log.error(error.message)
  324. process.exit(1)
  325. })