diff --git a/docs/resources/(resources)/go/goenv.mdx b/docs/resources/(resources)/go/goenv.mdx new file mode 100644 index 00000000..91a8a97c --- /dev/null +++ b/docs/resources/(resources)/go/goenv.mdx @@ -0,0 +1,56 @@ +--- +title: goenv +description: A reference page for the goenv resource +--- + +The goenv resource installs [goenv](https://github.com/go-nv/goenv), a Go version manager modeled after pyenv and rbenv that lets you install and switch between multiple Go versions. On macOS it is installed via Homebrew; on Linux it is cloned from GitHub into `~/.goenv`. + +## Parameters: + +- **goVersions**: *(array[string])* Go versions to install via goenv (e.g. `["1.22.0", "1.21.5"]`). Versions must match those available from `goenv install --list`. Codify adds missing versions and removes versions that are no longer listed. + +- **global**: *(string)* The Go version to set as the global default (equivalent to running `goenv global `). The version must be in `goVersions` or already installed on the system. + +## Example usage: + +### Install goenv with a single Go version + +```json title="codify.jsonc" +[ + { + "type": "goenv", + "goVersions": ["1.22.0"], + "global": "1.22.0" + } +] +``` + +### Install goenv with multiple Go versions + +```json title="codify.jsonc" +[ + { + "type": "goenv", + "goVersions": ["1.21.0", "1.22.0", "1.23.0"], + "global": "1.23.0" + } +] +``` + +### Install goenv without installing any Go versions + +```json title="codify.jsonc" +[ + { + "type": "goenv" + } +] +``` + +## Notes: + +- On macOS, Homebrew must be installed before applying the goenv resource. The [homebrew](/docs/resources/package-managers/homebrew) resource can install it. +- On Linux, `git` is required and installed automatically via the system package manager before cloning goenv. +- On Linux, goenv is cloned to `~/.goenv` and added to `PATH` in your shell RC file. On macOS, Homebrew manages the installation path. +- After applying, open a new terminal session (or source your shell RC file) for the `goenv` shims and `GOROOT`/`GOPATH` environment variables to become active. +- To find available Go versions, run `goenv install --list` after installing goenv. diff --git a/docs/resources/(resources)/go/meta.json b/docs/resources/(resources)/go/meta.json new file mode 100644 index 00000000..a4a2b7c7 --- /dev/null +++ b/docs/resources/(resources)/go/meta.json @@ -0,0 +1,4 @@ +{ + "title": "go", + "pages": ["goenv"] +} diff --git a/docs/resources/index.mdx b/docs/resources/index.mdx index 6373739f..a812bdb4 100644 --- a/docs/resources/index.mdx +++ b/docs/resources/index.mdx @@ -33,6 +33,7 @@ Install and manage multiple versions of programming languages: - **[rbenv](/docs/resources/ruby/rbenv)** - Ruby version management - **[jenv](/docs/resources/jenv)** - Java version management - **[asdf](/docs/resources/asdf/asdf)** - Universal version manager for multiple languages +- **[goenv](/docs/resources/go/goenv)** - Go version management ### Programming Languages & Tools diff --git a/package.json b/package.json index c8721afd..fd66d017 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.1.0-beta.18", + "version": "1.1.0-beta.20", "description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux", "main": "dist/index.js", "scripts": { diff --git a/src/index.ts b/src/index.ts index b7f1cec1..5f68f6a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { AsdfPluginResource } from './resources/asdf/asdf-plugin.js'; import { AwsCliResource } from './resources/aws-cli/cli/aws-cli.js'; import { AwsProfileResource } from './resources/aws-cli/profile/aws-profile.js'; import { DnfResource } from './resources/dnf/dnf.js'; +import { GoenvResource } from './resources/go/goenv/goenv.js'; import { DockerResource } from './resources/docker/docker.js'; import { FileResource } from './resources/file/file.js'; import { RemoteFileResource } from './resources/file/remote-file.js'; @@ -73,6 +74,7 @@ runPlugin(Plugin.create( new TerraformResource(), new NvmResource(), new JenvResource(), + new GoenvResource(), new PgcliResource(), new VscodeResource(), new GitRepositoryResource(), diff --git a/src/resources/go/goenv/global-parameter.ts b/src/resources/go/goenv/global-parameter.ts new file mode 100644 index 00000000..22170bee --- /dev/null +++ b/src/resources/go/goenv/global-parameter.ts @@ -0,0 +1,40 @@ +import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core'; + +import { GoenvConfig } from './goenv.js'; + +export class GoenvGlobalParameter extends StatefulParameter { + getSettings(): ParameterSetting { + return { + type: 'version', + }; + } + + override async refresh(): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe('goenv global'); + if (status === SpawnStatus.ERROR) { + return null; + } + return parseGlobalVersion(data); + } + + override async add(valueToAdd: string): Promise { + const $ = getPty(); + await $.spawn(`goenv global ${valueToAdd}`, { interactive: true }); + } + + override async modify(newValue: string): Promise { + const $ = getPty(); + await $.spawn(`goenv global ${newValue}`, { interactive: true }); + } + + override async remove(): Promise { + const $ = getPty(); + await $.spawn('goenv global system', { interactive: true }); + } +} + +function parseGlobalVersion(output: string): string | null { + const version = output.trim(); + return version === 'system' ? null : version; +} diff --git a/src/resources/go/goenv/go-versions-parameter.ts b/src/resources/go/goenv/go-versions-parameter.ts new file mode 100644 index 00000000..871152f3 --- /dev/null +++ b/src/resources/go/goenv/go-versions-parameter.ts @@ -0,0 +1,28 @@ +import { ArrayStatefulParameter, getPty } from '@codifycli/plugin-core'; + +import { GoenvConfig } from './goenv.js'; + +export class GoVersionsParameter extends ArrayStatefulParameter { + override async refresh(_desired: string[] | null): Promise { + const $ = getPty(); + const { data } = await $.spawnSafe('goenv versions --bare'); + return parseInstalledVersions(data); + } + + override async addItem(version: string): Promise { + const $ = getPty(); + await $.spawn(`goenv install ${version}`, { interactive: true }); + } + + override async removeItem(version: string): Promise { + const $ = getPty(); + await $.spawn(`goenv uninstall --force ${version}`, { interactive: true }); + } +} + +function parseInstalledVersions(output: string): string[] { + return output + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); +} diff --git a/src/resources/go/goenv/goenv.ts b/src/resources/go/goenv/goenv.ts new file mode 100644 index 00000000..e7d4e3ae --- /dev/null +++ b/src/resources/go/goenv/goenv.ts @@ -0,0 +1,155 @@ +import { + ExampleConfig, + FileUtils, + getPty, + Resource, + ResourceSettings, + SpawnStatus, + Utils, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import os from 'node:os'; +import path from 'node:path'; + +import { GoenvGlobalParameter } from './global-parameter.js'; +import { GoVersionsParameter } from './go-versions-parameter.js'; + +const GOENV_ROOT = path.join(os.homedir(), '.goenv'); +const GOENV_ROOT_EXPORT = 'export GOENV_ROOT="$HOME/.goenv"'; +const GOENV_PATH_EXPORT = 'export PATH="$GOENV_ROOT/bin:$PATH"'; +const GOENV_INIT = 'eval "$(goenv init -)"'; + +const schema = z + .object({ + goVersions: z + .array(z.string()) + .describe('Go versions to install via goenv (e.g. ["1.22.0", "1.21.5"])') + .optional(), + global: z + .string() + .describe('The global Go version set by goenv.') + .optional(), + }) + .describe('goenv resource — install and manage multiple Go versions'); + +export type GoenvConfig = z.infer; + +const defaultConfig: Partial = { + goVersions: [], +}; + +const exampleBasic: ExampleConfig = { + title: 'Install goenv with a global Go version', + description: 'Install goenv, download Go 1.22.0, and set it as the default global version.', + configs: [ + { + type: 'goenv', + goVersions: ['1.22.0'], + global: '1.22.0', + }, + ], +}; + +const exampleMultiVersion: ExampleConfig = { + title: 'Manage multiple Go versions', + description: 'Install goenv with multiple Go versions for cross-version testing, setting the latest as the default.', + configs: [ + { + type: 'goenv', + goVersions: ['1.21.0', '1.22.0', '1.23.0'], + global: '1.23.0', + }, + ], +}; + +export class GoenvResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'goenv', + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleMultiVersion, + }, + operatingSystems: [OS.Darwin, OS.Linux], + schema, + parameterSettings: { + goVersions: { type: 'stateful', definition: new GoVersionsParameter(), order: 1 }, + global: { type: 'stateful', definition: new GoenvGlobalParameter(), order: 2 }, + }, + }; + } + + override async refresh(): Promise | null> { + const $ = getPty(); + const { status } = await $.spawnSafe('goenv --version'); + if (status === SpawnStatus.SUCCESS) return {}; + + // goenv may be installed via git clone but not yet on PATH in the current session + const { status: binStatus } = await $.spawnSafe( + `test -f ${path.join(GOENV_ROOT, 'bin', 'goenv')}` + ); + return binStatus === SpawnStatus.SUCCESS ? {} : null; + } + + override async create(): Promise { + if (Utils.isMacOS()) { + await installOnMacOS(); + } else { + await installOnLinux(); + } + } + + override async destroy(): Promise { + if (Utils.isMacOS()) { + await uninstallOnMacOS(); + } else { + await uninstallOnLinux(); + } + } +} + +async function installOnMacOS(): Promise { + const $ = getPty(); + await $.spawn('brew install goenv', { + interactive: true, + env: { HOMEBREW_NO_AUTO_UPDATE: '1' }, + }); + await FileUtils.addToShellRc(GOENV_INIT); +} + +async function installOnLinux(): Promise { + await Utils.installViaPkgMgr('git'); + + const $ = getPty(); + await $.spawn(`git clone https://github.com/go-nv/goenv.git ${GOENV_ROOT}`, { + interactive: true, + }); + + await FileUtils.addAllToShellRc([GOENV_ROOT_EXPORT, GOENV_PATH_EXPORT, GOENV_INIT]); +} + +async function uninstallOnMacOS(): Promise { + const $ = getPty(); + await $.spawnSafe('brew uninstall goenv', { + env: { HOMEBREW_NO_AUTO_UPDATE: '1' }, + }); + await removeGoenvFromShellRc([GOENV_INIT]); +} + +async function uninstallOnLinux(): Promise { + const $ = getPty(); + await $.spawnSafe(`rm -rf ${GOENV_ROOT}`); + await removeGoenvFromShellRc([GOENV_ROOT_EXPORT, GOENV_PATH_EXPORT, GOENV_INIT]); +} + +async function removeGoenvFromShellRc(lines: string[]): Promise { + const shellRc = Utils.getPrimaryShellRc(); + if (!(await FileUtils.fileExists(shellRc))) { + return; + } + for (const line of lines) { + await FileUtils.removeLineFromShellRc(line); + } +} diff --git a/test/go/goenv.test.ts b/test/go/goenv.test.ts new file mode 100644 index 00000000..e112b4de --- /dev/null +++ b/test/go/goenv.test.ts @@ -0,0 +1,43 @@ +import { SpawnStatus } from '@codifycli/plugin-core'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import { describe, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { TestUtils } from '../test-utils.js'; + +describe('Goenv resource integration tests', () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Installs goenv, installs a Go version, and sets a global', { timeout: 600000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { + type: 'goenv', + goVersions: ['1.22.0'], + global: '1.22.0', + }, + ], { + validateApply: async () => { + const goenvCheck = await testSpawn('goenv --version'); + expect(goenvCheck.status).toBe(SpawnStatus.SUCCESS); + + const { data: versions } = await testSpawn('goenv versions'); + expect(versions).toContain('1.22.0'); + + const { data: globalVersion } = await testSpawn('goenv global'); + expect(globalVersion.trim()).toBe('1.22.0'); + + const { data: goVersion, status: goStatus } = await testSpawn('go version'); + expect(goStatus).toBe(SpawnStatus.SUCCESS); + expect(goVersion).toContain('go1.22.0'); + }, + validateDestroy: () => { + const shellRc = TestUtils.getPrimaryShellRc(); + if (fs.existsSync(shellRc)) { + const shellRcContents = fs.readFileSync(shellRc, 'utf-8'); + expect(shellRcContents).not.toContain('goenv init'); + } + }, + }); + }); +});