From b1bfecf9e7dd6bfe92af06a972d0d794f418139e Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Wed, 13 May 2026 12:39:51 +0000 Subject: [PATCH 1/5] feat: Add fast-node-manager resource (auto-generated from issue #30) --- .../javascript/fast-node-manager.mdx | 50 ++++++++ .../(resources)/javascript/meta.json | 2 +- src/index.ts | 2 + .../default-version-parameter.ts | 43 +++++++ .../fast-node-manager/fast-node-manager.ts | 113 ++++++++++++++++++ .../node-versions-parameter.ts | 43 +++++++ .../fast-node-manager.test.ts | 40 +++++++ 7 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 docs/resources/(resources)/javascript/fast-node-manager.mdx create mode 100644 src/resources/javascript/fast-node-manager/default-version-parameter.ts create mode 100644 src/resources/javascript/fast-node-manager/fast-node-manager.ts create mode 100644 src/resources/javascript/fast-node-manager/node-versions-parameter.ts create mode 100644 test/node/fast-node-manager/fast-node-manager.test.ts diff --git a/docs/resources/(resources)/javascript/fast-node-manager.mdx b/docs/resources/(resources)/javascript/fast-node-manager.mdx new file mode 100644 index 00000000..ffe2e4ea --- /dev/null +++ b/docs/resources/(resources)/javascript/fast-node-manager.mdx @@ -0,0 +1,50 @@ +--- +title: fast-node-manager +description: A reference page for the fast-node-manager resource +--- +import { Step, Steps } from 'fumadocs-ui/components/steps'; + +The fast-node-manager resource installs [fnm](https://github.com/Schniz/fnm) — a fast, cross-platform Node.js version manager built in Rust. fnm lets you install and switch between multiple Node.js versions and respects `.nvmrc` and `.node-version` files. + +## Parameters + +- **nodeVersions**: *(array[string])* Node.js versions to install. Supports partial semver (`"20"`), exact versions (`"20.18.0"`), and aliases (`"lts"`, `"latest"`). + +- **defaultVersion**: *(string)* The global default Node.js version set via `fnm default`. + +## Example usage + +```json title="codify.json" +[ + { + "type": "fast-node-manager", + "nodeVersions": ["20", "18"], + "defaultVersion": "20" + } +] +``` + +### Setting up Node.js with fnm + + + Create a `codify.json` file anywhere. + Open `codify.json` and paste in the following config. + + +```json title="codify.json" +[ + { + "type": "fast-node-manager", + "nodeVersions": ["lts"], + "defaultVersion": "lts" + } +] +``` + + + Run `codify apply` in the directory of the file. Open a new terminal and run `node -v` — the installed LTS version should be returned. Node.js is now managed by fnm. + + +```sh title="terminal" +codify apply +``` diff --git a/docs/resources/(resources)/javascript/meta.json b/docs/resources/(resources)/javascript/meta.json index 7622cdb9..8e63b54f 100644 --- a/docs/resources/(resources)/javascript/meta.json +++ b/docs/resources/(resources)/javascript/meta.json @@ -1,4 +1,4 @@ { "title": "javascript", - "pages": ["npm", "npm-login", "nvm", "pnpm"] + "pages": ["fast-node-manager", "npm", "npm-login", "nvm", "pnpm"] } diff --git a/src/index.ts b/src/index.ts index 5f68f6a3..076647c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { HomebrewResource } from './resources/homebrew/homebrew.js'; import { JenvResource } from './resources/java/jenv/jenv.js'; import { Npm } from './resources/javascript/npm/npm.js'; import { NpmLoginResource } from './resources/javascript/npm/npm-login.js'; +import { FnmResource } from './resources/javascript/fast-node-manager/fast-node-manager.js'; import { NvmResource } from './resources/javascript/nvm/nvm.js'; import { Pnpm } from './resources/javascript/pnpm/pnpm.js'; import { MacportsResource } from './resources/macports/macports.js'; @@ -73,6 +74,7 @@ runPlugin(Plugin.create( new AwsProfileResource(), new TerraformResource(), new NvmResource(), + new FnmResource(), new JenvResource(), new GoenvResource(), new PgcliResource(), diff --git a/src/resources/javascript/fast-node-manager/default-version-parameter.ts b/src/resources/javascript/fast-node-manager/default-version-parameter.ts new file mode 100644 index 00000000..a1a6ee72 --- /dev/null +++ b/src/resources/javascript/fast-node-manager/default-version-parameter.ts @@ -0,0 +1,43 @@ +import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core'; + +import { FnmConfig } from './fast-node-manager.js'; + +export class FnmDefaultVersionParameter extends StatefulParameter { + getSettings(): ParameterSetting { + return { + type: 'version', + }; + } + + override async refresh(): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe('fnm list', { interactive: true }); + + if (status === SpawnStatus.ERROR) { + return null; + } + + for (const line of data.split('\n')) { + if (line.includes('default')) { + const match = line.match(/v?(\d+\.\d+\.\d+)/); + if (match) return match[1]; + } + } + + return null; + } + + override async add(valueToAdd: string): Promise { + const $ = getPty(); + await $.spawn(`fnm default ${valueToAdd}`, { interactive: true }); + } + + override async modify(newValue: string): Promise { + const $ = getPty(); + await $.spawn(`fnm default ${newValue}`, { interactive: true }); + } + + override async remove(valueToRemove: string): Promise { + console.warn(`fnm does not support unsetting the default version. Node.js will remain at ${valueToRemove}. Skipping...`); + } +} diff --git a/src/resources/javascript/fast-node-manager/fast-node-manager.ts b/src/resources/javascript/fast-node-manager/fast-node-manager.ts new file mode 100644 index 00000000..89faba3e --- /dev/null +++ b/src/resources/javascript/fast-node-manager/fast-node-manager.ts @@ -0,0 +1,113 @@ +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 { FnmDefaultVersionParameter } from './default-version-parameter.js'; +import { FnmNodeVersionsParameter } from './node-versions-parameter.js'; + +const FNM_DIR = path.join(os.homedir(), '.fnm'); +const FNM_PATH_EXPORT = 'export PATH="$HOME/.fnm:$PATH"'; +const FNM_EVAL = 'eval "$(fnm env --use-on-cd)"'; + +const schema = z.object({ + nodeVersions: z + .array(z.string()) + .describe('Node.js versions to install via fnm (e.g. ["20", "18.20.0", "lts"])') + .optional(), + defaultVersion: z + .string() + .describe('The default (global) Node.js version set by fnm.') + .optional(), +}) + .describe('fast-node-manager resource — install and manage multiple Node.js versions via fnm'); + +export type FnmConfig = z.infer; + +const defaultConfig: Partial = { + nodeVersions: [], +}; + +const exampleLts: ExampleConfig = { + title: 'Install Node.js LTS via fnm', + description: 'Install fnm and set the latest LTS release as the global Node.js version.', + configs: [{ + type: 'fast-node-manager', + nodeVersions: ['lts'], + defaultVersion: 'lts', + }], +}; + +const exampleMultiVersion: ExampleConfig = { + title: 'Install multiple Node.js versions via fnm', + description: 'Install fnm with multiple Node.js versions side by side, using Node.js 22 as the global default.', + configs: [{ + type: 'fast-node-manager', + nodeVersions: ['18', '20', '22'], + defaultVersion: '22', + }], +}; + +export class FnmResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'fast-node-manager', + operatingSystems: [OS.Darwin, OS.Linux], + schema, + defaultConfig, + exampleConfigs: { + example1: exampleLts, + example2: exampleMultiVersion, + }, + parameterSettings: { + nodeVersions: { type: 'stateful', definition: new FnmNodeVersionsParameter(), order: 1 }, + defaultVersion: { type: 'stateful', definition: new FnmDefaultVersionParameter(), order: 2 }, + }, + }; + } + + override async refresh(): Promise | null> { + const $ = getPty(); + const { status } = await $.spawnSafe('fnm --version', { interactive: true }); + return status === 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 { + await Utils.installViaPkgMgr('fnm'); + await FileUtils.addToShellRc(FNM_EVAL); +} + +async function installOnLinux(): Promise { + const $ = getPty(); + await $.spawn('curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell', { interactive: true }); + await FileUtils.addAllToShellRc([FNM_PATH_EXPORT, FNM_EVAL]); +} + +async function uninstallOnMacOS(): Promise { + await Utils.uninstallViaPkgMgr('fnm'); + await FileUtils.removeLineFromShellRc(FNM_EVAL); +} + +async function uninstallOnLinux(): Promise { + const $ = getPty(); + await $.spawnSafe(`rm -rf ${FNM_DIR}`); + await FileUtils.removeLineFromShellRc(FNM_PATH_EXPORT); + await FileUtils.removeLineFromShellRc(FNM_EVAL); +} diff --git a/src/resources/javascript/fast-node-manager/node-versions-parameter.ts b/src/resources/javascript/fast-node-manager/node-versions-parameter.ts new file mode 100644 index 00000000..3779c6a8 --- /dev/null +++ b/src/resources/javascript/fast-node-manager/node-versions-parameter.ts @@ -0,0 +1,43 @@ +import { ArrayParameterSetting, ArrayStatefulParameter, getPty, SpawnStatus } from '@codifycli/plugin-core'; + +import { FnmConfig } from './fast-node-manager.js'; + +export class FnmNodeVersionsParameter extends ArrayStatefulParameter { + getSettings(): ArrayParameterSetting { + return { + type: 'array', + isElementEqual: (desired, current) => current.includes(desired), + }; + } + + override async refresh(_desired: string[] | null): Promise { + const $ = getPty(); + const { data, status } = await $.spawnSafe('fnm list', { interactive: true }); + + if (status === SpawnStatus.ERROR) { + return null; + } + + return parseInstalledVersions(data); + } + + override async addItem(version: string): Promise { + const $ = getPty(); + await $.spawn(`fnm install ${version}`, { interactive: true }); + } + + override async removeItem(version: string): Promise { + const $ = getPty(); + await $.spawn(`fnm uninstall ${version}`, { interactive: true }); + } +} + +function parseInstalledVersions(output: string): string[] { + return output + .split('\n') + .map((line) => { + const match = line.match(/v?(\d+\.\d+\.\d+)/); + return match ? match[1] : null; + }) + .filter((v): v is string => v !== null); +} diff --git a/test/node/fast-node-manager/fast-node-manager.test.ts b/test/node/fast-node-manager/fast-node-manager.test.ts new file mode 100644 index 00000000..b39b8ef2 --- /dev/null +++ b/test/node/fast-node-manager/fast-node-manager.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import path from 'node:path'; +import { SpawnStatus } from '@codifycli/plugin-core'; + +describe('fast-node-manager tests', () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Can install fnm and node', { timeout: 500000, skip: true }, async () => { + await PluginTester.fullTest(pluginPath, [ + { + type: 'fast-node-manager', + defaultVersion: '20', + nodeVersions: ['20', '18'], + }, + ], { + validateApply: async () => { + expect(testSpawn('fnm --version', { interactive: true })).resolves.toMatchObject({ status: SpawnStatus.SUCCESS }); + expect(testSpawn('node --version', { interactive: true })).resolves.toMatchObject({ data: expect.stringContaining('20') }); + + const { data: installedVersions } = await testSpawn('fnm list', { interactive: true }); + expect(installedVersions).toContain('20'); + expect(installedVersions).toContain('18'); + }, + testModify: { + modifiedConfigs: [{ + type: 'fast-node-manager', + defaultVersion: '22', + nodeVersions: ['22'], + }], + validateModify: async () => { + expect(testSpawn('node --version', { interactive: true })).resolves.toMatchObject({ data: expect.stringContaining('22') }); + }, + }, + validateDestroy: async () => { + expect(testSpawn('fnm --version', { interactive: true })).resolves.toMatchObject({ status: SpawnStatus.ERROR }); + }, + }); + }); +}); From 2d1ddc3e61f8dd2bff2e976db07ebb7d5cbcdc75 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 13 May 2026 23:49:35 -0400 Subject: [PATCH 2/5] fix: fixes for fnm --- .../fast-node-manager/fast-node-manager.ts | 62 ++++++++++--------- .../fast-node-manager.test.ts | 12 ++-- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/resources/javascript/fast-node-manager/fast-node-manager.ts b/src/resources/javascript/fast-node-manager/fast-node-manager.ts index 89faba3e..5c8fdafc 100644 --- a/src/resources/javascript/fast-node-manager/fast-node-manager.ts +++ b/src/resources/javascript/fast-node-manager/fast-node-manager.ts @@ -7,8 +7,7 @@ import { FnmDefaultVersionParameter } from './default-version-parameter.js'; import { FnmNodeVersionsParameter } from './node-versions-parameter.js'; const FNM_DIR = path.join(os.homedir(), '.fnm'); -const FNM_PATH_EXPORT = 'export PATH="$HOME/.fnm:$PATH"'; -const FNM_EVAL = 'eval "$(fnm env --use-on-cd)"'; +const FNM_MULTISHELL_EXPORT = 'export FNM_MULTISHELL_PATH="${TMPDIR:-/tmp}/fnm_multishells"'; const schema = z.object({ nodeVersions: z @@ -73,41 +72,48 @@ export class FnmResource extends Resource { } override async create(): Promise { - if (Utils.isMacOS()) { - await installOnMacOS(); - } else { - await installOnLinux(); - } + await install(); } override async destroy(): Promise { - if (Utils.isMacOS()) { - await uninstallOnMacOS(); - } else { - await uninstallOnLinux(); - } + await uninstall(); } } -async function installOnMacOS(): Promise { - await Utils.installViaPkgMgr('fnm'); - await FileUtils.addToShellRc(FNM_EVAL); -} +async function install(): Promise { + if (Utils.isLinux()) { + await Utils.installViaPkgMgr('curl unzip'); + } -async function installOnLinux(): Promise { const $ = getPty(); - await $.spawn('curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell', { interactive: true }); - await FileUtils.addAllToShellRc([FNM_PATH_EXPORT, FNM_EVAL]); + await $.spawn('curl -fsSL https://fnm.vercel.app/install | bash', { interactive: true }); + await FileUtils.addToShellRc(FNM_MULTISHELL_EXPORT); } -async function uninstallOnMacOS(): Promise { - await Utils.uninstallViaPkgMgr('fnm'); - await FileUtils.removeLineFromShellRc(FNM_EVAL); -} - -async function uninstallOnLinux(): Promise { +async function uninstall(): Promise { const $ = getPty(); - await $.spawnSafe(`rm -rf ${FNM_DIR}`); - await FileUtils.removeLineFromShellRc(FNM_PATH_EXPORT); - await FileUtils.removeLineFromShellRc(FNM_EVAL); + + const { status: brewStatus } = await $.spawnSafe('brew list fnm', { + env: { HOMEBREW_NO_AUTO_UPDATE: '1' }, + }); + + if (brewStatus === SpawnStatus.SUCCESS) { + await Utils.uninstallViaPkgMgr('fnm'); + } else { + await $.spawnSafe(`rm -rf ${FNM_DIR}`); + } + + // Remove the block the installer appends to the shell rc file + for (const line of [ + '# fnm', + `FNM_PATH="${FNM_DIR}"`, + 'if [ -d "$FNM_PATH" ]; then', + ' export PATH="$FNM_PATH:$PATH"', + ' eval "$(fnm env --shell zsh)"', + ' eval "$(fnm env --shell bash)"', + 'fi', + FNM_MULTISHELL_EXPORT, + ]) { + await FileUtils.removeLineFromShellRc(line); + } } diff --git a/test/node/fast-node-manager/fast-node-manager.test.ts b/test/node/fast-node-manager/fast-node-manager.test.ts index b39b8ef2..8eb4d8eb 100644 --- a/test/node/fast-node-manager/fast-node-manager.test.ts +++ b/test/node/fast-node-manager/fast-node-manager.test.ts @@ -6,7 +6,7 @@ import { SpawnStatus } from '@codifycli/plugin-core'; describe('fast-node-manager tests', () => { const pluginPath = path.resolve('./src/index.ts'); - it('Can install fnm and node', { timeout: 500000, skip: true }, async () => { + it('Can install fnm and node', { timeout: 500000 }, async () => { await PluginTester.fullTest(pluginPath, [ { type: 'fast-node-manager', @@ -15,10 +15,10 @@ describe('fast-node-manager tests', () => { }, ], { validateApply: async () => { - expect(testSpawn('fnm --version', { interactive: true })).resolves.toMatchObject({ status: SpawnStatus.SUCCESS }); - expect(testSpawn('node --version', { interactive: true })).resolves.toMatchObject({ data: expect.stringContaining('20') }); + expect(testSpawn('fnm --version')).resolves.toMatchObject({ status: SpawnStatus.SUCCESS }); + expect(testSpawn('fnm exec --using=20 node --version')).resolves.toMatchObject({ data: expect.stringContaining('20') }); - const { data: installedVersions } = await testSpawn('fnm list', { interactive: true }); + const { data: installedVersions } = await testSpawn('fnm list'); expect(installedVersions).toContain('20'); expect(installedVersions).toContain('18'); }, @@ -29,11 +29,11 @@ describe('fast-node-manager tests', () => { nodeVersions: ['22'], }], validateModify: async () => { - expect(testSpawn('node --version', { interactive: true })).resolves.toMatchObject({ data: expect.stringContaining('22') }); + expect(testSpawn('fnm exec --using=22 node --version')).resolves.toMatchObject({ data: expect.stringContaining('22') }); }, }, validateDestroy: async () => { - expect(testSpawn('fnm --version', { interactive: true })).resolves.toMatchObject({ status: SpawnStatus.ERROR }); + expect(testSpawn('fnm --version')).resolves.toMatchObject({ status: SpawnStatus.ERROR }); }, }); }); From d268724c0437c8dbd11c0ebbcdbc1d5db7e3c49e Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 13 May 2026 23:56:57 -0400 Subject: [PATCH 3/5] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd66d017..c3d48652 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.1.0-beta.20", + "version": "1.1.0-beta.21", "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": { From 2c1494741dc6454db6b42ae54c20f62ec7f499f8 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 14 May 2026 00:03:45 -0400 Subject: [PATCH 4/5] feat: switch type to 'fnm' instead --- docs/resources/(resources)/javascript/fast-node-manager.mdx | 4 ++-- .../javascript/fast-node-manager/fast-node-manager.ts | 6 +++--- test/node/fast-node-manager/fast-node-manager.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/resources/(resources)/javascript/fast-node-manager.mdx b/docs/resources/(resources)/javascript/fast-node-manager.mdx index ffe2e4ea..21b50c0a 100644 --- a/docs/resources/(resources)/javascript/fast-node-manager.mdx +++ b/docs/resources/(resources)/javascript/fast-node-manager.mdx @@ -17,7 +17,7 @@ The fast-node-manager resource installs [fnm](https://github.com/Schniz/fnm) — ```json title="codify.json" [ { - "type": "fast-node-manager", + "type": "fnm", "nodeVersions": ["20", "18"], "defaultVersion": "20" } @@ -34,7 +34,7 @@ The fast-node-manager resource installs [fnm](https://github.com/Schniz/fnm) — ```json title="codify.json" [ { - "type": "fast-node-manager", + "type": "fnm", "nodeVersions": ["lts"], "defaultVersion": "lts" } diff --git a/src/resources/javascript/fast-node-manager/fast-node-manager.ts b/src/resources/javascript/fast-node-manager/fast-node-manager.ts index 5c8fdafc..771d4907 100644 --- a/src/resources/javascript/fast-node-manager/fast-node-manager.ts +++ b/src/resources/javascript/fast-node-manager/fast-node-manager.ts @@ -31,7 +31,7 @@ const exampleLts: ExampleConfig = { title: 'Install Node.js LTS via fnm', description: 'Install fnm and set the latest LTS release as the global Node.js version.', configs: [{ - type: 'fast-node-manager', + type: 'fnm', nodeVersions: ['lts'], defaultVersion: 'lts', }], @@ -41,7 +41,7 @@ const exampleMultiVersion: ExampleConfig = { title: 'Install multiple Node.js versions via fnm', description: 'Install fnm with multiple Node.js versions side by side, using Node.js 22 as the global default.', configs: [{ - type: 'fast-node-manager', + type: 'fnm', nodeVersions: ['18', '20', '22'], defaultVersion: '22', }], @@ -50,7 +50,7 @@ const exampleMultiVersion: ExampleConfig = { export class FnmResource extends Resource { getSettings(): ResourceSettings { return { - id: 'fast-node-manager', + id: 'fnm', operatingSystems: [OS.Darwin, OS.Linux], schema, defaultConfig, diff --git a/test/node/fast-node-manager/fast-node-manager.test.ts b/test/node/fast-node-manager/fast-node-manager.test.ts index 8eb4d8eb..e581c19a 100644 --- a/test/node/fast-node-manager/fast-node-manager.test.ts +++ b/test/node/fast-node-manager/fast-node-manager.test.ts @@ -9,7 +9,7 @@ describe('fast-node-manager tests', () => { it('Can install fnm and node', { timeout: 500000 }, async () => { await PluginTester.fullTest(pluginPath, [ { - type: 'fast-node-manager', + type: 'fnm', defaultVersion: '20', nodeVersions: ['20', '18'], }, @@ -24,7 +24,7 @@ describe('fast-node-manager tests', () => { }, testModify: { modifiedConfigs: [{ - type: 'fast-node-manager', + type: 'fnm', defaultVersion: '22', nodeVersions: ['22'], }], From 9825779651116382a8f4a2d6be040e37971faf1f Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 14 May 2026 10:17:48 -0400 Subject: [PATCH 5/5] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c3d48652..ef83727c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.1.0-beta.21", + "version": "1.1.0-beta.22", "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": {