#
tokens: 7876/50000 2/86 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/jamsocket/forevervm?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       ├── javascript-checks.yml
│       ├── python-checks.yml
│       ├── release.yml
│       └── rust-checks.yml
├── javascript
│   ├── .gitignore
│   ├── .prettierrc
│   ├── example
│   │   ├── index.ts
│   │   ├── package.json
│   │   └── README.md
│   ├── forevervm
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── get-binary.js
│   │   │   └── run.js
│   │   └── tsconfig.json
│   ├── mcp-server
│   │   ├── .gitignore
│   │   ├── .prettierrc
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── index.ts
│   │   │   └── install
│   │   │       ├── claude.ts
│   │   │       ├── goose.ts
│   │   │       ├── index.ts
│   │   │       └── windsurf.ts
│   │   ├── tmp.json
│   │   └── tsconfig.json
│   ├── package-lock.json
│   ├── package.json
│   ├── sdk
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── env.ts
│   │   │   ├── index.ts
│   │   │   ├── repl.ts
│   │   │   └── types.ts
│   │   └── tsconfig.json
│   ├── tsconfig.base.json
│   ├── tsup.config.ts
│   └── vitest.config.ts
├── LICENSE.md
├── publishing.md
├── python
│   ├── .gitignore
│   ├── forevervm
│   │   ├── forevervm
│   │   │   └── __init__.py
│   │   ├── pyproject.toml
│   │   └── uv.lock
│   └── sdk
│       ├── forevervm_sdk
│       │   ├── __init__.py
│       │   ├── config.py
│       │   ├── repl.py
│       │   └── types.py
│       ├── pyproject.toml
│       ├── README.md
│       ├── tests
│       │   ├── __init__.py
│       │   └── test_connect.py
│       └── uv.lock
├── README.md
├── rust
│   ├── .gitignore
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── forevervm
│   │   ├── .gitignore
│   │   ├── Cargo.lock
│   │   ├── Cargo.toml
│   │   ├── README.md
│   │   └── src
│   │       ├── commands
│   │       │   ├── auth.rs
│   │       │   ├── machine.rs
│   │       │   ├── mod.rs
│   │       │   └── repl.rs
│   │       ├── config.rs
│   │       ├── lib.rs
│   │       ├── main.rs
│   │       └── util.rs
│   └── forevervm-sdk
│       ├── Cargo.lock
│       ├── Cargo.toml
│       ├── README.md
│       ├── src
│       │   ├── api
│       │   │   ├── api_types.rs
│       │   │   ├── http_api.rs
│       │   │   ├── id_types.rs
│       │   │   ├── mod.rs
│       │   │   ├── protocol.rs
│       │   │   └── token.rs
│       │   ├── client
│       │   │   ├── error.rs
│       │   │   ├── mod.rs
│       │   │   ├── repl.rs
│       │   │   ├── typed_socket.rs
│       │   │   └── util.rs
│       │   ├── lib.rs
│       │   └── util.rs
│       └── tests
│           └── basic_sdk_tests.rs
└── scripts
    ├── .gitignore
    ├── .prettierrc
    ├── bump-versions.ts
    ├── package-lock.json
    ├── package.json
    ├── publish.ts
    ├── README.md
    └── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/javascript/sdk/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Repl } from './repl'
  2 | import { env } from './env'
  3 | import type {
  4 |   ApiExecResponse,
  5 |   ApiExecResultResponse,
  6 |   ApiExecResultStreamResponse,
  7 |   CreateMachineRequest,
  8 |   CreateMachineResponse,
  9 |   ListMachinesRequest,
 10 |   ListMachinesResponse,
 11 |   WhoamiResponse,
 12 | } from './types'
 13 | 
 14 | export * from './types'
 15 | export * from './repl'
 16 | 
 17 | interface ForeverVMOptions {
 18 |   token?: string
 19 |   baseUrl?: string
 20 |   timeoutSec?: number
 21 | }
 22 | 
 23 | const DEFAULT_TIMEOUT_SEC = 15
 24 | 
 25 | export class ForeverVM {
 26 |   #token = env.FOREVERVM_TOKEN || ''
 27 |   #baseUrl = 'https://api.forevervm.com'
 28 |   #timeoutSec = DEFAULT_TIMEOUT_SEC
 29 | 
 30 |   constructor(options: ForeverVMOptions = {}) {
 31 |     if (options.token) this.#token = options.token
 32 |     if (options.baseUrl) this.#baseUrl = options.baseUrl
 33 |     if (options.timeoutSec) this.#timeoutSec = options.timeoutSec
 34 |   }
 35 | 
 36 |   get #headers() {
 37 |     return { 'authorization': `Bearer ${this.#token}`, 'x-forevervm-sdk': 'javascript' }
 38 |   }
 39 | 
 40 |   async #get(path: string) {
 41 |     const response = await fetch(`${this.#baseUrl}${path}`, { headers: this.#headers })
 42 |     if (!response.ok) {
 43 |       const text = await response.text().catch(() => 'Unknown error')
 44 |       throw new Error(`HTTP ${response.status}: ${text}`)
 45 |     }
 46 |     return await response.json()
 47 |   }
 48 | 
 49 |   async *#getStream(path: string) {
 50 |     const response = await fetch(`${this.#baseUrl}${path}`, { headers: this.#headers })
 51 |     if (!response.ok) {
 52 |       const text = await response.text().catch(() => 'Unknown error')
 53 |       throw new Error(`HTTP ${response.status}: ${text}`)
 54 |     }
 55 | 
 56 |     if (!response.body) return
 57 | 
 58 |     const decoder = new TextDecoderStream()
 59 |     const reader = response.body.pipeThrough(decoder).getReader()
 60 | 
 61 |     // buffer JSON just in case an object is split across multiple stream chunks
 62 |     let buffer = ''
 63 | 
 64 |     while (true) {
 65 |       let { done, value = '' } = await reader.read()
 66 |       if (done) return
 67 | 
 68 |       // loop until we've read all the data in this chunk
 69 |       while (value) {
 70 |         // find the next newline character
 71 |         const newline = value.indexOf('\n')
 72 | 
 73 |         // if there are no more newlines, add the remaining data to the buffer and break
 74 |         if (newline === -1) {
 75 |           buffer += value
 76 |           break
 77 |         }
 78 | 
 79 |         // parse and yield the next line from the data
 80 |         const line = value.slice(0, newline)
 81 |         yield JSON.parse(buffer + line)
 82 | 
 83 |         // remove the just-processed line from the value and reset the buffer
 84 |         value = value.slice(newline + 1)
 85 |         buffer = ''
 86 |       }
 87 |     }
 88 |   }
 89 | 
 90 |   async #post(path: string, body?: object) {
 91 |     let response
 92 |     try {
 93 |       response = await fetch(`${this.#baseUrl}${path}`, {
 94 |         method: 'POST',
 95 |         headers: { ...this.#headers, 'Content-Type': 'application/json' },
 96 |         body: body ? JSON.stringify(body) : undefined,
 97 |       })
 98 |     } catch (error) {
 99 |       throw new Error(`Failed to fetch: ${error}`)
100 |     }
101 |     if (!response.ok) {
102 |       const text = await response.text().catch(() => 'Unknown error')
103 |       throw new Error(`HTTP ${response.status}: ${text}`)
104 |     }
105 | 
106 |     return await response.json()
107 |   }
108 | 
109 |   async whoami(): Promise<WhoamiResponse> {
110 |     return await this.#get('/v1/whoami')
111 |   }
112 | 
113 |   async createMachine(request: CreateMachineRequest = {}): Promise<CreateMachineResponse> {
114 |     return await this.#post('/v1/machine/new', request)
115 |   }
116 | 
117 |   async listMachines(request: ListMachinesRequest = {}): Promise<ListMachinesResponse> {
118 |     return await this.#post('/v1/machine/list', request)
119 |   }
120 | 
121 |   async exec(
122 |     code: string,
123 |     machineName?: string,
124 |     interrupt: boolean = false,
125 |     timeoutSec: number = this.#timeoutSec,
126 |   ): Promise<ApiExecResponse> {
127 |     if (!machineName) {
128 |       const createResponse = await this.createMachine()
129 |       machineName = createResponse.machine_name
130 |     }
131 | 
132 |     return await this.#post(`/v1/machine/${machineName}/exec`, {
133 |       instruction: { code, timeout_seconds: timeoutSec },
134 |       interrupt,
135 |     })
136 |   }
137 | 
138 |   async execResult(machineName: string, instructionSeq: number): Promise<ApiExecResultResponse> {
139 |     return await this.#get(`/v1/machine/${machineName}/exec/${instructionSeq}/result`)
140 |   }
141 | 
142 |   async *execResultStream(
143 |     machineName: string,
144 |     instructionSeq: number,
145 |   ): AsyncGenerator<ApiExecResultStreamResponse> {
146 |     yield* this.#getStream(`/v1/machine/${machineName}/exec/${instructionSeq}/stream-result`)
147 |   }
148 | 
149 |   repl(machineName?: string): Repl {
150 |     return new Repl({
151 |       machine: machineName,
152 |       token: this.#token,
153 |       baseUrl: this.#baseUrl.replace(/^http/, 'ws'),
154 |     })
155 |   }
156 | }
157 | 
158 | if (import.meta.vitest) {
159 |   const { test, expect } = import.meta.vitest
160 | 
161 |   const FOREVERVM_API_BASE = process.env.FOREVERVM_API_BASE || ''
162 |   const FOREVERVM_TOKEN = process.env.FOREVERVM_TOKEN || ''
163 | 
164 |   test('whoami', async () => {
165 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
166 | 
167 |     const whoami = await fvm.whoami()
168 |     expect(whoami.account).toBeDefined()
169 |   })
170 | 
171 |   test('createMachine and listMachines', async () => {
172 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
173 | 
174 |     const machine = await fvm.createMachine()
175 |     expect(machine.machine_name).toBeDefined()
176 | 
177 |     const machines = await fvm.listMachines()
178 |     const found = machines.machines.find(({ name }) => name === machine.machine_name)
179 |     expect(found).toBeDefined()
180 |   })
181 | 
182 |   test('exec and execResult', async () => {
183 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
184 |     const { machine_name } = await fvm.createMachine()
185 |     const { instruction_seq } = await fvm.exec('print(123) or 567', machine_name)
186 |     expect(instruction_seq).toBe(0)
187 |     const result = await fvm.execResult(machine_name, instruction_seq as number)
188 |     expect(result.result.value).toBe('567')
189 |   })
190 | 
191 |   test('execResultStream', async () => {
192 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
193 |     const { machine_name } = await fvm.createMachine()
194 |     const { instruction_seq } = await fvm.exec('for i in range(10): print(i)\n"done"', machine_name)
195 |     expect(instruction_seq).toBe(0)
196 | 
197 |     let i = 0
198 |     for await (const item of fvm.execResultStream(machine_name, instruction_seq as number)) {
199 |       if (item.type === 'result') {
200 |         expect(item.result.value).toBe("'done'")
201 |       } else if (item.type === 'output') {
202 |         expect(item.chunk.stream).toBe('stdout')
203 |         expect(item.chunk.data).toBe(`${i}`)
204 |         expect(item.chunk.seq).toBe(i)
205 |         i += 1
206 |       }
207 |     }
208 |   })
209 | 
210 |   test('execResultStream with image', { timeout: 10000 }, async () => {
211 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
212 |     const { machine_name } = await fvm.createMachine()
213 | 
214 |     const code = `import matplotlib.pyplot as plt
215 | plt.plot([0, 1, 2], [0, 1, 2])
216 | plt.title('Simple Plot')
217 | plt.show()`
218 | 
219 |     const { instruction_seq } = await fvm.exec(code, machine_name)
220 |     expect(instruction_seq).toBe(0)
221 | 
222 |     for await (const _ of fvm.execResultStream(machine_name, instruction_seq as number)) {
223 |     }
224 | 
225 |     // if we reach this point, it means all the stream chunks were valid JSON
226 |   })
227 | 
228 |   test('createMachine with tags', async () => {
229 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
230 | 
231 |     // Create machine with tags
232 |     const taggedMachine = await fvm.createMachine({
233 |       tags: { env: 'test', purpose: 'sdk-test' },
234 |     })
235 |     expect(taggedMachine.machine_name).toBeDefined()
236 | 
237 |     // List machines and verify tags
238 |     const machines = await fvm.listMachines()
239 |     const foundTagged = machines.machines.find(({ name }) => name === taggedMachine.machine_name)
240 |     expect(foundTagged).toBeDefined()
241 |     expect(foundTagged?.tags).toBeDefined()
242 |     expect(foundTagged?.tags?.env).toBe('test')
243 |     expect(foundTagged?.tags?.purpose).toBe('sdk-test')
244 |   })
245 | 
246 |   test('listMachines with tag filter', async () => {
247 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
248 | 
249 |     // Create an untagged machine
250 |     const untaggedMachine = await fvm.createMachine()
251 |     expect(untaggedMachine.machine_name).toBeDefined()
252 | 
253 |     // Create a uniquely tagged machine
254 |     const uniqueTag = `test-${Date.now()}`
255 |     const taggedMachine = await fvm.createMachine({
256 |       tags: { unique: uniqueTag },
257 |     })
258 |     expect(taggedMachine.machine_name).toBeDefined()
259 | 
260 |     // List machines with the unique tag filter
261 |     const filteredMachines = await fvm.listMachines({
262 |       tags: { unique: uniqueTag },
263 |     })
264 | 
265 |     // Verify only our machine with the unique tag is returned
266 |     expect(filteredMachines.machines.length).toBe(1)
267 |     expect(filteredMachines.machines[0].tags?.unique).toBe(uniqueTag)
268 |   })
269 | 
270 |   test('createMachine with memory limit', async () => {
271 |     const fvm = new ForeverVM({ token: FOREVERVM_TOKEN, baseUrl: FOREVERVM_API_BASE })
272 | 
273 |     // Create machine with memory limit
274 |     const machine = await fvm.createMachine({
275 |       memory_mb: 512,
276 |     })
277 |     expect(machine.machine_name).toBeDefined()
278 | 
279 |     // Verify the machine was created (note: we can't directly verify the memory limit
280 |     // through the API as it doesn't return this value in machine details)
281 |     const machines = await fvm.listMachines()
282 |     const found = machines.machines.find(({ name }) => name === machine.machine_name)
283 |     expect(found).toBeDefined()
284 |   })
285 | }
286 | 
```

--------------------------------------------------------------------------------
/scripts/bump-versions.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as fs from 'fs'
  2 | import * as path from 'path'
  3 | import * as TOML from '@iarna/toml'
  4 | import { Command } from 'commander'
  5 | import chalk from 'chalk'
  6 | import { execSync } from 'child_process'
  7 | 
  8 | class SemverVersion {
  9 |   constructor(
 10 |     public major: number,
 11 |     public minor: number,
 12 |     public patch: number,
 13 |   ) {}
 14 | 
 15 |   static parse(version: string): SemverVersion {
 16 |     const [major, minor, patch] = version.split('.')
 17 |     return new SemverVersion(parseInt(major), parseInt(minor), parseInt(patch))
 18 |   }
 19 | 
 20 |   toString() {
 21 |     return `${this.major}.${this.minor}.${this.patch}`
 22 |   }
 23 | 
 24 |   bump(type: 'major' | 'minor' | 'patch') {
 25 |     switch (type) {
 26 |       case 'major':
 27 |         return new SemverVersion(this.major + 1, 0, 0)
 28 |       case 'minor':
 29 |         return new SemverVersion(this.major, this.minor + 1, 0)
 30 |       case 'patch':
 31 |         return new SemverVersion(this.major, this.minor, this.patch + 1)
 32 |       default:
 33 |         throw new Error(`Invalid version bump type: ${type}`)
 34 |     }
 35 |   }
 36 | 
 37 |   equals(other?: SemverVersion | null) {
 38 |     if (other === null || other === undefined) return false
 39 |     return this.major === other.major && this.minor === other.minor && this.patch === other.patch
 40 |   }
 41 | 
 42 |   cmp(other: SemverVersion) {
 43 |     if (this.major < other.major) return -1
 44 |     if (this.major > other.major) return 1
 45 |     if (this.minor < other.minor) return -1
 46 |     if (this.minor > other.minor) return 1
 47 |     if (this.patch < other.patch) return -1
 48 |     if (this.patch > other.patch) return 1
 49 |     return 0
 50 |   }
 51 | }
 52 | 
 53 | interface Package {
 54 |   repo: PackageRepo
 55 |   path: string
 56 |   name: string
 57 |   currentVersion?: SemverVersion
 58 |   publishedVersion?: SemverVersion | null
 59 |   private: boolean
 60 | }
 61 | 
 62 | interface PackageRepo {
 63 |   repoName: string
 64 | 
 65 |   gatherPackages(): Package[]
 66 | 
 67 |   getDeployedVersion(name: string): Promise<SemverVersion | null>
 68 | 
 69 |   updateVersion(path: string, version: SemverVersion): void
 70 | }
 71 | 
 72 | class NpmPackageRepo implements PackageRepo {
 73 |   repoName = 'npm'
 74 | 
 75 |   gatherPackages(): Package[] {
 76 |     const jsDir = path.join(import.meta.dirname, '..', 'javascript')
 77 |     const packages: Package[] = []
 78 | 
 79 |     const crawlDir = (dir: string) => {
 80 |       const items = fs.readdirSync(dir)
 81 |       for (const item of items) {
 82 |         const fullPath = path.join(dir, item)
 83 | 
 84 |         if (item === 'node_modules') {
 85 |           continue
 86 |         }
 87 | 
 88 |         const stat = fs.statSync(fullPath)
 89 | 
 90 |         if (stat.isDirectory()) {
 91 |           crawlDir(fullPath)
 92 |         } else if (item === 'package.json') {
 93 |           const content = fs.readFileSync(fullPath, 'utf-8')
 94 |           const pkg = JSON.parse(content)
 95 |           packages.push({
 96 |             repo: this,
 97 |             path: fullPath,
 98 |             name: pkg.name,
 99 |             currentVersion: pkg.version && SemverVersion.parse(pkg.version),
100 |             private: pkg.private || false,
101 |           })
102 |         }
103 |       }
104 |     }
105 | 
106 |     crawlDir(jsDir)
107 |     return packages
108 |   }
109 | 
110 |   async getDeployedVersion(name: string): Promise<SemverVersion | null> {
111 |     try {
112 |       const response = await fetch(`https://registry.npmjs.org/${name}`)
113 |       if (!response.ok) {
114 |         if (response.status === 404) {
115 |           return null
116 |         }
117 |         throw new Error(`Failed to fetch package info: ${response.statusText}`)
118 |       }
119 |       const data = (await response.json()) as any
120 |       const version = data['dist-tags']?.latest
121 |       return version ? SemverVersion.parse(version) : null
122 |     } catch (error) {
123 |       console.error(`Error fetching version for ${name}:`, error)
124 |       return null
125 |     }
126 |   }
127 | 
128 |   updateVersion(packageFilePath: string, version: SemverVersion): void {
129 |     const content = fs.readFileSync(packageFilePath, 'utf-8')
130 |     const pkg = JSON.parse(content)
131 |     pkg.version = version.toString()
132 |     fs.writeFileSync(packageFilePath, JSON.stringify(pkg, null, 2) + '\n')
133 | 
134 |     // update lockfile by running `npm install`
135 |     execSync('npm install', { cwd: path.dirname(packageFilePath) })
136 |   }
137 | }
138 | 
139 | class PythonPackageRepo implements PackageRepo {
140 |   repoName = 'python'
141 | 
142 |   gatherPackages(): Package[] {
143 |     const pythonDir = path.join(import.meta.dirname, '..', 'python')
144 |     const packages: Package[] = []
145 | 
146 |     const crawlDir = (dir: string) => {
147 |       const items = fs.readdirSync(dir)
148 |       for (const item of items) {
149 |         const fullPath = path.join(dir, item)
150 |         const stat = fs.statSync(fullPath)
151 | 
152 |         if (stat.isDirectory()) {
153 |           crawlDir(fullPath)
154 |         } else if (item === 'pyproject.toml') {
155 |           const content = fs.readFileSync(fullPath, 'utf-8')
156 |           const parsed = TOML.parse(content)
157 |           const projectData = parsed.project as any
158 | 
159 |           if (projectData?.name && projectData?.version) {
160 |             packages.push({
161 |               repo: this,
162 |               path: fullPath,
163 |               name: projectData.name,
164 |               currentVersion: projectData.version && SemverVersion.parse(projectData.version),
165 |               private: false, // Note: implement this if we need private packages
166 |             })
167 |           }
168 |         }
169 |       }
170 |     }
171 | 
172 |     crawlDir(pythonDir)
173 |     return packages
174 |   }
175 | 
176 |   async getDeployedVersion(name: string): Promise<SemverVersion | null> {
177 |     try {
178 |       const response = await fetch(`https://pypi.org/pypi/${name}/json`)
179 |       if (!response.ok) {
180 |         if (response.status === 404) {
181 |           return null
182 |         }
183 |         throw new Error(`Failed to fetch package info: ${response.statusText}`)
184 |       }
185 |       const data = (await response.json()) as any
186 |       const version = data.info?.version
187 |       return version ? SemverVersion.parse(version) : null
188 |     } catch (error) {
189 |       console.error(`Error fetching version for ${name}:`, error)
190 |       return null
191 |     }
192 |   }
193 | 
194 |   updateVersion(path: string, version: SemverVersion): void {
195 |     const content = fs.readFileSync(path, 'utf-8')
196 |     const newContent = content.replace(/version\s*=\s*"[^"]*"/, `version = "${version}"`)
197 |     if (content === newContent) {
198 |       throw new Error(`Failed to update version in ${path}`)
199 |     }
200 |     fs.writeFileSync(path, newContent)
201 |   }
202 | }
203 | 
204 | class CargoPackageRepo implements PackageRepo {
205 |   repoName = 'cargo'
206 | 
207 |   gatherPackages(): Package[] {
208 |     const cargoDir = path.join(import.meta.dirname, '..', 'rust')
209 |     const packages: Package[] = []
210 | 
211 |     const crawlDir = (dir: string) => {
212 |       const entries = fs.readdirSync(dir, { withFileTypes: true })
213 | 
214 |       for (const entry of entries) {
215 |         if (entry.name === 'target') {
216 |           continue
217 |         }
218 | 
219 |         const fullPath = path.join(dir, entry.name)
220 | 
221 |         if (entry.isDirectory()) {
222 |           crawlDir(fullPath)
223 |         } else if (entry.name === 'Cargo.toml') {
224 |           const content = fs.readFileSync(fullPath, 'utf-8')
225 |           const data = TOML.parse(content) as any
226 | 
227 |           if (data.package && data.package.name && data.package.version) {
228 |             packages.push({
229 |               repo: this,
230 |               path: fullPath,
231 |               name: data.package.name,
232 |               currentVersion: data.package.version && SemverVersion.parse(data.package.version),
233 |               private: false, // Cargo packages are typically public
234 |             })
235 |           }
236 |         }
237 |       }
238 |     }
239 | 
240 |     crawlDir(cargoDir)
241 |     return packages
242 |   }
243 | 
244 |   async getDeployedVersion(name: string): Promise<SemverVersion | null> {
245 |     try {
246 |       const response = await fetch(`https://crates.io/api/v1/crates/${name}`, {
247 |         headers: {
248 |           // crates.io requires a non-default user agent.
249 |           'User-Agent': 'forevervm bump-versions script',
250 |         },
251 |       })
252 |       if (!response.ok) {
253 |         if (response.status === 404) {
254 |           return null
255 |         }
256 |         throw new Error(`Failed to fetch package info: ${response.statusText}`)
257 |       }
258 |       const data = (await response.json()) as any
259 |       const version = data.crate?.max_version
260 |       return version ? SemverVersion.parse(version) : null
261 |     } catch (error) {
262 |       console.error(`Error fetching version for ${name}:`, error)
263 |       return null
264 |     }
265 |   }
266 | 
267 |   updateVersion(path: string, version: SemverVersion): void {
268 |     const content = fs.readFileSync(path, 'utf-8')
269 |     const newContent = content.replace(/version\s*=\s*"[^"]*"/, `version = "${version}"`)
270 |     if (content === newContent) {
271 |       throw new Error(`Failed to update version in ${path}`)
272 |     }
273 |     fs.writeFileSync(path, newContent)
274 |   }
275 | }
276 | 
277 | function collectPackages(): Package[] {
278 |   const packages: Package[] = []
279 | 
280 |   packages.push(...new NpmPackageRepo().gatherPackages())
281 |   packages.push(...new PythonPackageRepo().gatherPackages())
282 |   packages.push(...new CargoPackageRepo().gatherPackages())
283 | 
284 |   return packages
285 | }
286 | 
287 | async function getCurrentVersions(packages: Package[]): Promise<Package[]> {
288 |   // filter out private packages
289 |   const filteredPackages = packages.filter((pkg) => !pkg.private)
290 |   const publishedVersions = await Promise.all(
291 |     filteredPackages.map(async (pkg) => {
292 |       const publishedVersion = await pkg.repo.getDeployedVersion(pkg.name)
293 |       return { ...pkg, publishedVersion }
294 |     }),
295 |   )
296 |   return publishedVersions
297 | }
298 | 
299 | async function main() {
300 |   const program = new Command()
301 | 
302 |   program.name('bump-versions').description('CLI to manage package versions')
303 | 
304 |   program
305 |     .command('info')
306 |     .description('List all packages and their versions')
307 |     .action(async () => {
308 |       const packages = await getCurrentVersions(collectPackages())
309 | 
310 |       for (const pkg of packages) {
311 |         const same = pkg.currentVersion?.equals(pkg.publishedVersion)
312 | 
313 |         if (same) {
314 |           console.log(
315 |             `${chalk.cyan(pkg.repo.repoName)} ${chalk.yellow(pkg.name)}: local ${chalk.green(pkg.currentVersion)} == published ${chalk.green(pkg.publishedVersion)}`,
316 |           )
317 |         } else {
318 |           console.log(
319 |             `${chalk.cyan(pkg.repo.repoName)} ${chalk.yellow(pkg.name)}: local ${chalk.green(pkg.currentVersion)} != published ${chalk.red(pkg.publishedVersion || 'N/A')}`,
320 |           )
321 |         }
322 |       }
323 |     })
324 | 
325 |   program
326 |     .command('bump [type]')
327 |     .description('Bump all packages versions')
328 |     .argument('[type]', 'Type of version bump', 'patch')
329 |     .option('-d, --dry-run', 'Perform a dry run without making changes')
330 |     .action(async (type: 'major' | 'minor' | 'patch', _, options: { dryRun: boolean }) => {
331 |       if (type !== 'major' && type !== 'minor' && type !== 'patch') {
332 |         console.error('Invalid version type; must be one of "major", "minor", or "patch"')
333 |         process.exit(1)
334 |       }
335 | 
336 |       const dryRun = options.dryRun
337 | 
338 |       const packages = await getCurrentVersions(collectPackages())
339 | 
340 |       const validVersions = packages
341 |         .map((pkg) => pkg.currentVersion)
342 |         .filter((v) => v !== null && v !== undefined)
343 |       const maxLocalVersion = validVersions.reduce((a, b) => (a.cmp(b) > 0 ? a : b))
344 | 
345 |       const maxPublishedVersion = packages
346 |         .map((pkg) => pkg.publishedVersion)
347 |         .filter((v) => v !== null && v !== undefined)
348 |         .reduce((a, b) => (a!.cmp(b!) > 0 ? a : b))
349 | 
350 |       const maxVersion =
351 |         maxLocalVersion!.cmp(maxPublishedVersion!) > 0 ? maxLocalVersion : maxPublishedVersion
352 | 
353 |       const bumpedVersion = maxVersion!.bump(type)
354 | 
355 |       const plan = []
356 | 
357 |       for (const pkg of packages) {
358 |         if (pkg.currentVersion !== bumpedVersion) {
359 |           plan.push({
360 |             package: pkg,
361 |             from: pkg.currentVersion,
362 |             to: bumpedVersion,
363 |           })
364 |         }
365 |       }
366 | 
367 |       console.log('Plan:')
368 |       for (const item of plan) {
369 |         console.log(
370 |           `${chalk.blue(item.package.repo.repoName)} ${chalk.cyan(item.package.name)}: ${chalk.red(item.from)} ${chalk.gray('->')} ${chalk.green(item.to)}`,
371 |         )
372 |       }
373 | 
374 |       if (dryRun) {
375 |         console.log('Dry run; no changes will be made.')
376 |       } else {
377 |         for (const item of plan) {
378 |           console.log(
379 |             `Updating ${chalk.blue(item.package.repo.repoName)} ${chalk.cyan(item.package.name)}: ${chalk.red(item.from)} ${chalk.gray('->')} ${chalk.green(item.to)}`,
380 |           )
381 |           item.package.repo.updateVersion(item.package.path, item.to)
382 |         }
383 |       }
384 |     })
385 | 
386 |   program.parse()
387 | }
388 | 
389 | main()
390 | 
```
Page 2/2FirstPrevNextLast