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 |
```