Skip to content

Commit 3fcb30c

Browse files
committed
fix: Fixed a bunch of resources not pre-installing curl and unzip if it doesn't exist. Added fixes for alias, android-studio (linux support), asdf, aws-cli, npm, npm-login, nvm, ollama, pyenv, terraform, uv
1 parent 89d3443 commit 3fcb30c

26 files changed

Lines changed: 277 additions & 327 deletions

CLAUDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,16 @@ const { data } = await $.spawn('command', {
247247
})
248248
```
249249

250+
**Never use `sudo` inside `$.spawn` or `$.spawnSafe`.** Use `{ requiresRoot: true }` in the options instead. The framework handles privilege escalation through the parent process.
251+
252+
```typescript
253+
// Wrong
254+
await $.spawn('sudo rm -f /usr/local/bin/ollama');
255+
256+
// Correct
257+
await $.spawn('rm -f /usr/local/bin/ollama', { requiresRoot: true });
258+
```
259+
250260
**File Operations:**
251261
```typescript
252262
await FileUtils.addToStartupFile(lineToAdd)
@@ -264,6 +274,34 @@ Utils.isLinux()
264274
Utils.isWindows()
265275
```
266276

277+
**Package Installation:**
278+
279+
Always use `Utils.installViaPkgMgr(pkg)` from `@codifycli/plugin-core` to install system packages. This is platform-agnostic and automatically dispatches to the correct package manager (Homebrew on macOS, apt on Debian/Ubuntu, etc.). Never hardcode package manager calls like `brew install`, `apt-get install -y`, or `sudo apt install` in resource code.
280+
281+
```typescript
282+
// Correct — works on macOS and Linux
283+
await Utils.installViaPkgMgr('curl');
284+
await Utils.uninstallViaPkgMgr('curl');
285+
286+
// Wrong — hardcoded to a specific platform/package manager
287+
await $.spawn('sudo apt-get install -y curl');
288+
await $.spawn('brew install curl');
289+
```
290+
291+
This applies to prerequisite checks too. When a resource needs a system dependency (e.g. `curl`, `git`, `make`), always install via `Utils.installViaPkgMgr` rather than spawning a package manager directly.
292+
293+
**Imports — `Utils` from plugin-core vs local utils:**
294+
295+
Always import `Utils` from `@codifycli/plugin-core`, not from `../../utils` or `../../../utils`. The local `src/utils/` module contains macOS-specific helpers (`findApplication`, `isArmArch`, `isRosetta2Installed`, `downloadUrlIntoFile`, etc.) that are only needed when those specific capabilities are required. For everything else — OS detection, package management, shell utilities — use the plugin-core `Utils`.
296+
297+
```typescript
298+
// Correct
299+
import { Utils } from '@codifycli/plugin-core';
300+
301+
// Only use local utils when you specifically need macOS/spotlight helpers
302+
import { Utils as LocalUtils } from '../../../utils/index.js';
303+
```
304+
267305
## Build Process
268306

269307
The build process (`scripts/build.ts`) does:

scripts/init.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ tart set codify-test-vm --memory 6124 --cpu 4
44
tart clone ghcr.io/cirruslabs/ubuntu:latest codify-test-vm-linux
55
tart set codify-test-vm-linux --memory 6124 --cpu 4
66

7+
## Will need to manually install nodeJS on the vm
8+
tart run codify-test-vm-linux
9+
tart exec -i codify-test-vm-linux bash -c -i "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash"
10+
tart exec -i codify-test-vm-linux bash -c -i "nvm install 24; nvm alias default 24"
711

812
# tart clone ghcr.io/kevinwang5658/sonoma-codify:v0.0.3 codify-sonoma

scripts/run-tests.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ async function main(argument: string, args: {
3636
}
3737

3838
if (args.persistent) {
39-
if (!argument) {
40-
throw new Error('No test specified for persistent mode');
41-
}
39+
// if (!argument) {
40+
// throw new Error('No test specified for persistent mode');
41+
// }
4242

4343
await launchPersistentTest(argument, debug, args.operatingSystem);
4444
return process.exit(0);
@@ -98,7 +98,7 @@ async function launchPersistentTest(test: string, debug: boolean, operatingSyste
9898

9999
console.log('Done refreshing files on VM. Starting tests...');
100100
VerbosityLevel.set(3);
101-
await codifySpawn(`tart exec -i ${vmName} ${shell} -c -i 'cd ${dir} && XDG_RUNTIME_DIR="/run/user/$(id -u)" DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" FORCE_COLOR=true npm run test -- ${test} --disable-console-intercept ${debugFlag} --no-file-parallelism'`, { throws: false });
101+
await codifySpawn(`tart exec -i ${vmName} ${shell} -c -i 'cd ${dir} && XDG_RUNTIME_DIR="/run/user/$(id -u)" DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" FORCE_COLOR=true npm run test -- ${test ? test : ''} --disable-console-intercept ${debugFlag} --no-file-parallelism'`, { throws: false });
102102
// }
103103
}
104104

src/resources/android/android-studio.test.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/resources/android/android-studio.ts

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { CreatePlan, Resource, ResourceSettings, getPty, z } from '@codifycli/plugin-core';
1+
import { CreatePlan, DestroyPlan, Resource, ResourceSettings, Utils, getPty, z } from '@codifycli/plugin-core';
22
import { OS } from '@codifycli/schemas';
33
import * as fs from 'node:fs/promises';
44
import os from 'node:os';
55
import path from 'node:path';
66
import plist from 'plist';
77

8-
import { Utils } from '../../utils/index.js';
8+
import { Utils as LocalUtils } from '../../utils/index.js';
99
import { AndroidStudioPlist, AndroidStudioVersionData } from './types.js';
1010

1111
export const schema = z.object({
@@ -18,71 +18,131 @@ export const schema = z.object({
1818
directory: z
1919
.string()
2020
.describe(
21-
'The directory to install Android Studios into. Defaults to /Applications'
21+
'The directory to install Android Studios into. Defaults to /Applications on macOS, /opt on Linux'
2222
)
2323
.optional(),
2424
}).meta({ $comment: 'https://codifycli.com/docs/resources/android-studio' })
2525

2626
export type AndroidStudioConfig = z.infer<typeof schema>;
2727

28+
const LINUX_INSTALL_DIR = '/opt';
29+
const MACOS_INSTALL_DIR = '/Applications';
30+
const LINUX_STUDIO_DIR = 'android-studio';
31+
2832
export class AndroidStudioResource extends Resource<AndroidStudioConfig> {
2933

3034
allAndroidStudioVersions?: AndroidStudioVersionData[];
3135

3236
override getSettings(): ResourceSettings<AndroidStudioConfig> {
37+
const defaultDir = Utils.isMacOS() ? MACOS_INSTALL_DIR : LINUX_INSTALL_DIR;
3338
return {
3439
id: 'android-studio',
35-
operatingSystems: [OS.Darwin],
40+
operatingSystems: [OS.Darwin, OS.Linux],
3641
schema,
3742
parameterSettings: {
38-
directory: { type: 'directory', default: '/Applications' },
43+
directory: { type: 'directory', default: defaultDir },
3944
version: { type: 'version' }
4045
}
4146
};
4247
}
4348

4449
override async refresh(parameters: Partial<AndroidStudioConfig>): Promise<Partial<AndroidStudioConfig> | null> {
45-
// Attempt to fetch all versions. The plist doesn't give detailed info on the version
4650
this.allAndroidStudioVersions = await this.fetchAllAndroidStudioVersions()
4751

48-
const installedVersions = (await Utils.findApplication('Android Studio')
52+
if (Utils.isMacOS()) {
53+
return this.refreshMacOS(parameters);
54+
}
55+
56+
return this.refreshLinux(parameters);
57+
}
58+
59+
override async create(plan: CreatePlan<AndroidStudioConfig>): Promise<void> {
60+
if (!this.allAndroidStudioVersions) {
61+
this.allAndroidStudioVersions = await this.fetchAllAndroidStudioVersions()
62+
}
63+
64+
if (Utils.isMacOS()) {
65+
return this.createMacOS(plan);
66+
}
67+
68+
return this.createLinux(plan);
69+
}
70+
71+
override async destroy(plan: DestroyPlan<AndroidStudioConfig>): Promise<void> {
72+
if (Utils.isMacOS()) {
73+
const directory = plan.currentConfig.directory ?? MACOS_INSTALL_DIR;
74+
await fs.rm(path.join(directory, 'Android Studio.app'), { force: true, recursive: true });
75+
} else {
76+
const $ = getPty();
77+
const directory = plan.currentConfig.directory ?? LINUX_INSTALL_DIR;
78+
await $.spawnSafe(`rm -rf "${path.join(directory, LINUX_STUDIO_DIR)}"`, { requiresRoot: true });
79+
}
80+
}
81+
82+
private async refreshMacOS(parameters: Partial<AndroidStudioConfig>): Promise<Partial<AndroidStudioConfig> | null> {
83+
const installedVersions = (await LocalUtils.findApplication('Android Studio')
4984
.then((locations) => Promise.all(
5085
locations.map((l) => this.addPlistData(l))
5186
)))
5287
.filter(Boolean)
5388
.map((l) => l!)
5489
.map((installed) => this.addWebInfo(installed, this.allAndroidStudioVersions!))
5590

56-
const match = this.matchVersionAndDirectory(parameters, installedVersions);
57-
if (match) {
58-
return match;
91+
return this.matchVersionAndDirectory(parameters, installedVersions);
92+
}
93+
94+
private async refreshLinux(parameters: Partial<AndroidStudioConfig>): Promise<Partial<AndroidStudioConfig> | null> {
95+
const directory = parameters.directory ?? LINUX_INSTALL_DIR;
96+
const studioDir = path.join(directory, LINUX_STUDIO_DIR);
97+
const studioBin = path.join(studioDir, 'bin', 'studio');
98+
99+
try {
100+
await fs.access(studioBin);
101+
} catch {
102+
return null;
59103
}
60104

61-
return null;
62-
}
105+
// Read product-info.json to determine the installed version
106+
let installedVersion: string | undefined;
107+
try {
108+
const productInfoRaw = await fs.readFile(path.join(studioDir, 'product-info.json'), 'utf8');
109+
const productInfo = JSON.parse(productInfoRaw) as { dataDirectoryName?: string; version?: string; buildNumber?: string };
110+
installedVersion = productInfo.version;
63111

64-
override async create(plan: CreatePlan<AndroidStudioConfig>): Promise<void> {
65-
const $ = getPty();
112+
if (!installedVersion && productInfo.buildNumber) {
113+
const matched = this.allAndroidStudioVersions?.find((v) => v.build === productInfo.buildNumber);
114+
installedVersion = matched?.version;
115+
}
116+
} catch {
117+
// product-info.json not readable — still report as installed, version unknown
118+
}
66119

67-
if (!this.allAndroidStudioVersions) {
68-
this.allAndroidStudioVersions = await this.fetchAllAndroidStudioVersions()
120+
if (parameters.version && installedVersion && !installedVersion.includes(parameters.version)) {
121+
return null;
69122
}
70123

71-
const versionToDownload = this.getVersionData(plan.desiredConfig.version, this.allAndroidStudioVersions)
124+
return {
125+
directory,
126+
version: installedVersion,
127+
};
128+
}
129+
130+
private async createMacOS(plan: CreatePlan<AndroidStudioConfig>): Promise<void> {
131+
const $ = getPty();
132+
133+
const versionToDownload = this.getVersionData(plan.desiredConfig.version, this.allAndroidStudioVersions!)
72134
if (!versionToDownload) {
73135
throw new Error(`Unable to find desired version: ${plan.desiredConfig.version}`);
74136
}
75137

76-
const downloadLink = await Utils.isArmArch()
138+
const isArm = await LocalUtils.isArmArch();
139+
const downloadLink = isArm
77140
? versionToDownload.download.find((v) => v.link.includes('mac_arm.dmg'))!
78141
: versionToDownload.download.find((v) => v.link.includes('mac.dmg'))!
79142

80-
// Create a temporary tmp dir
81143
const temporaryDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codify-android-'))
82144

83145
try {
84-
85-
// Download and unzip the terraform binary
86146
await $.spawn(`curl -fsSL ${downloadLink.link} -o android-studio.dmg`, { cwd: temporaryDir });
87147

88148
const { data } = await $.spawn('hdiutil attach android-studio.dmg', { cwd: temporaryDir });
@@ -98,26 +158,46 @@ export class AndroidStudioResource extends Resource<AndroidStudioConfig> {
98158

99159
try {
100160
const contents = await fs.readdir(mountedDir);
101-
102-
// Depending on it's preview or regular the name is different
103-
const appName = contents
104-
.find((l) => l.includes('Android'))
161+
const appName = contents.find((l) => l.includes('Android'))
105162

106163
// Must rsync because mounted dirs are read-only (can't delete via mv)
107164
await $.spawn(`rsync -rl "${appName}" Applications/`, { cwd: mountedDir })
108165
} finally {
109-
// Unmount
110166
await $.spawnSafe(`hdiutil detach "${mountedDir}"`)
111167
}
112-
113168
} finally {
114-
// Delete the tmp directory
115169
await fs.rm(temporaryDir, { recursive: true, force: true });
116170
}
117171
}
118172

119-
override async destroy(): Promise<void> {
120-
await fs.rm('/Applications/Android Studio.app', { force: true, recursive: true });
173+
private async createLinux(plan: CreatePlan<AndroidStudioConfig>): Promise<void> {
174+
const $ = getPty();
175+
176+
const versionToDownload = this.getVersionData(plan.desiredConfig.version, this.allAndroidStudioVersions!)
177+
if (!versionToDownload) {
178+
throw new Error(`Unable to find desired version: ${plan.desiredConfig.version}`);
179+
}
180+
181+
const downloadLink = versionToDownload.download.find((v) => v.link.includes('-linux.tar.gz'));
182+
183+
if (!downloadLink) {
184+
throw new Error(`Unable to find a Linux download link for version: ${plan.desiredConfig.version}`);
185+
}
186+
187+
const directory = plan.desiredConfig.directory ?? LINUX_INSTALL_DIR;
188+
const temporaryDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codify-android-'))
189+
190+
try {
191+
await $.spawn(`curl -fsSL ${downloadLink.link} -o android-studio.tar.gz`, { cwd: temporaryDir });
192+
await $.spawn(`tar -xzf android-studio.tar.gz`, { cwd: temporaryDir });
193+
194+
// Remove existing install if present
195+
await fs.rm(path.join(directory, LINUX_STUDIO_DIR), { force: true, recursive: true });
196+
197+
await $.spawn(`mv android-studio "${directory}/"`, { cwd: temporaryDir, requiresRoot: true });
198+
} finally {
199+
await fs.rm(temporaryDir, { recursive: true, force: true });
200+
}
121201
}
122202

123203
private async fetchAllAndroidStudioVersions(): Promise<AndroidStudioVersionData[]> {

src/resources/asdf/asdf.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CreatePlan, ExampleConfig, FileUtils, Resource, ResourceSettings, SpawnStatus, getPty, z } from '@codifycli/plugin-core';
1+
import { CreatePlan, ExampleConfig, FileUtils, Resource, ResourceSettings, SpawnStatus, Utils as CoreUtils, getPty, z } from '@codifycli/plugin-core';
22
import { OS } from '@codifycli/schemas';
33
import fs from 'node:fs/promises';
44
import os from 'node:os';
@@ -88,6 +88,11 @@ export class AsdfResource extends Resource<AsdfConfig> {
8888
}
8989

9090
if (Utils.isLinux()) {
91+
const curlCheck = await $.spawnSafe('which curl');
92+
if (curlCheck.status === SpawnStatus.ERROR) {
93+
await CoreUtils.installViaPkgMgr('curl');
94+
}
95+
9196
const { data: latestVersion } = await $.spawn('curl -s https://api.github.com/repos/asdf-vm/asdf/releases/latest | grep \'"tag_name":\' | sed -E \'s/.*"([^"]+)".*/\\1/\'');
9297

9398
// Create .asdf directory if it doesn't exist

src/resources/aws-cli/cli/aws-cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ softwareupdate --install-rosetta
8484
: 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip';
8585

8686
console.log(`Installing AWS CLI for Linux (${isArmArch ? 'ARM64' : 'x86_64'})...`);
87+
const unzipCheck = await $.spawnSafe('which unzip');
88+
if (unzipCheck.status === SpawnStatus.ERROR) {
89+
await Utils.installViaPkgMgr('unzip');
90+
}
91+
8792
await FileUtils.downloadFile(downloadUrl, path.join(tmpDir, 'awscliv2.zip'));
8893
await $.spawn('unzip -q awscliv2.zip', { cwd: tmpDir });
8994
await $.spawn('./aws/install', { cwd: tmpDir, requiresRoot: true });

src/resources/javascript/npm/npm-schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"description": "Install and manage packages using NPM.",
77
"type": "object",
88
"properties": {
9-
"install": {
9+
"globalInstall": {
1010
"type": "array",
1111
"description": "An array of npm packages to install globally. Use the npm@version syntax to pin a specific version (e.g. \"nodemon@3.1.10\").",
1212
"items": {

0 commit comments

Comments
 (0)