Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/doctor/src/sys-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,39 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo {
tempDirectory,
);
const xcodeProjectDir = path.join(tempDirectory, "cocoapods");

// If asdf version manager is installed, get the current Ruby version for the project directory and write it to the temporary project directory.
// Resolve relative to the directory `ns doctor` was invoked from, since it can be run outside of a project directory.
const asdfResult = await this.childProcess.spawnFromEvent(
"asdf",
["current", "ruby"],
"exit",
{ ignoreError: true, spawnOptions: { cwd: process.cwd() } },
);

if (asdfResult.exitCode === 0) {
const asdfVersionMatch = (asdfResult.stdout as string).match(
SysInfo.VERSION_REGEXP,
);

if (asdfVersionMatch?.[0]) {
const asdfVersion = asdfVersionMatch[0];
const asdfConfigPath = path.join(
xcodeProjectDir,
".tool-versions",
);
const wroteASDFConfig = this.fileSystem.appendFile(
asdfConfigPath,
`ruby ${asdfVersion}\n`,
Comment on lines +437 to +450

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the locally installed asdf CLI's current Ruby output without changing config.
if command -v asdf >/dev/null 2>&1; then
  asdf current ruby || true
fi

Repository: NativeScript/nativescript-cli

Length of output: 167


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Relevant file locations:\n'
git ls-files | rg '(^|/)sys-info\.ts$|(^|/)sys-info\.test\.ts$|(^|/)sys-info\.ts$' || true

printf '\nPackage and outlines:\n'
for f in $(git ls-files | rg '(^|/)sys-info\.ts$'); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" --match VERSION_REGEXP --view expanded || true
  ast-grep outline "$f" --match isCocoaPodsWorkingCorrectly --view expanded || true
done

printf '\nRegexp definitions/usages:\n'
rg -n "VERSION_REGEXP|current ruby|tool-versions|ruby .*\n|asdf" packages/doctor/src packages/doctor/test -S

Repository: NativeScript/nativescript-cli

Length of output: 1070


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '(^|/)sys-info\.ts$|(^|/)sys-info\.test\.ts$' || true

printf '\nVERSION_REGEXP and asdf config sections:\n'
rg -n -U 'VERSION_REGEXP|versionRegexp|appendFile\(|asdf current ruby|\\.tool-versions|ruby ' packages/doctor/src/sys-info.ts packages/doctor/test/sys-info.ts -S

printf '\nSource lines 400-470:\n'
sed -n '400,470p' packages/doctor/src/sys-info.ts | nl -ba -v400

printf '\nTest lines 960-1045:\n'
sed -n '960,1045p' packages/doctor/test/sys-info.ts | nl -ba -v960

Repository: NativeScript/nativescript-cli

Length of output: 2644


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Source lines 400-470:\n'
python3 - <<'PY'
from pathlib import Path
p = Path('packages/doctor/src/sys-info.ts')
lines = p.read_text().splitlines()
for i in range(400, min(471, len(lines))):
    print(f"{i+1:5d}\t{lines[i]}")
PY

printf '\nTest lines 960-1045:\n'
python3 - <<'PY'
from pathlib import Path
p = Path('packages/doctor/test/sys-info.ts')
lines = p.read_text().splitlines()
for i in range(960, min(1046, len(lines))):
    print(f"{i+1:5d}\t{lines[i]}")
PY

printf '\nBehavioral probe of current regexp against asdf-like lines:\n'
python3 - <<'PY'
import re
VERSION_REGEXP = re.compile(r'(\d{1,})\.(\d{1,})\.*([\w-]{0,})', re.M)
samples = [
    "ruby            3.2.1           /Users/user/app/.tool-versions",
    "ruby system",
    "ruby 3.2.1-jruby",
    "ruby 3.2.1+system",
    "ruby 3.2.1~provider",
]
for s in samples:
    m = s.split(', ', 1)[0].match(VERSION_REGEXP) if callable(s.split(', ', 1)[0].match) else VERSION_REGEXP.search(s)
    # actual Python regex behavior:
    print(repr(s), ">", VERSION_REGEXP.search(s).group(0) if VERSION_REGEXP.search(s) else None)
PY

Repository: NativeScript/nativescript-cli

Length of output: 5896


🌐 Web query:

asdf current <plugin> output format asdf current ruby system

💡 Result:

The asdf current command displays the version of a tool (or all tools) that asdf is currently configured to use based on the.tool-versions file resolution process [1][2]. In recent versions of asdf (0.16.0 and later), the output format for asdf current has been updated to four columns [3]: 1. Plugin Name: The name of the tool plugin (e.g., ruby) [1][3]. 2. Version: The version currently set [1][3]. 3. Source: The origin of the version setting (e.g., the path to the.tool-versions file where it was defined) [3]. 4. Status: A boolean or descriptive indicator stating whether the specified version is installed [3]. If the version is not installed, the output typically includes a suggested command to install it [3][4]. Regarding the specific output example asdf current ruby system: When you see system as the version, it indicates that you have configured asdf to use the version of the tool installed on your system (outside of asdf management) rather than an asdf-managed version [5][6]. This is often used for tools like Python or Ruby where a system-provided version may be required [5][6]. The entry system appears in the Version column because it is a valid keyword that tells asdf to pass execution to the system binary [5]. If the output says system and adds a message like Not installed, it typically means asdf is looking for a system binary that may not be present or is not correctly identified as the system fallback in the current context [4]. Top results: [1] https://asdf-vm.com/manage/versions.html [3] https://asdf-vm.com/guide/upgrading-to-v0-16.html [5] https://github.com/asdf-vm/asdf/blob/master/docs/manage/configuration.md [4] asdf-vm/asdf#1696 [6] https://github.com/asdf-vm/asdf/blob/master/docs/guide/getting-started.md

Citations:


Preserve asdf Ruby selectors instead of requiring semver.

asdf current ruby can use selectors like ruby system; SysInfo.VERSION_REGEXP does not match that output, so this branch skips writing .tool-versions instead of preserving the selector. Parse the asdf current ruby version token with a validator instead of \d{1,}(\.\d{1,})..., and add tests for ruby system plus provider-qualified versions such as JRuby/TruffleRuby.

📍 Affects 2 files
  • packages/doctor/src/sys-info.ts#L437-L450 (this comment)
  • packages/doctor/test/sys-info.ts#L1002-L1031
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/doctor/src/sys-info.ts` around lines 437 - 450, Update the asdf Ruby
parsing in SysInfo so it validates and preserves non-semver selectors, including
“system” and provider-qualified versions such as JRuby or TruffleRuby, instead
of relying on VERSION_REGEXP. Keep writing the parsed token to .tool-versions
through the existing appendFile flow. Add or update coverage in
packages/doctor/test/sys-info.ts at lines 1002-1031 for ruby system and
provider-qualified version outputs.

);
if (!wroteASDFConfig) {
console.warn(
`CocoaPods invocation may fail, check asdf config`,
);
}
}
}

const spawnResult = await this.childProcess.spawnFromEvent(
"pod",
["install"],
Expand All @@ -432,6 +465,7 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo {
);
return !spawnResult.exitCode;
} catch (err) {
console.log(`Pod command failed - ${err}`);
return false;
} finally {
this.fileSystem.deleteEntry(tempDirectory);
Expand Down
15 changes: 13 additions & 2 deletions packages/doctor/src/wrappers/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export class FileSystem {
return fs.existsSync(path.resolve(filePath));
}

public appendFile(filePath: string, text: string): boolean {
let success = false;
try {
fs.appendFileSync(path.resolve(filePath), text);
success = true;
} catch (err) {
console.error(`appendFile failed with ${err}`);
}
return success;
}

public extractZip(pathToZip: string, outputDir: string): Promise<void> {
return new Promise((resolve, reject) => {
yauzl.open(
Expand Down Expand Up @@ -46,7 +57,7 @@ export class FileSystem {
zipFile.once("end", () => resolve());

zipFile.readEntry();
}
},
);
});
}
Expand All @@ -57,7 +68,7 @@ export class FileSystem {

public readJson<T>(
filePath: string,
options?: { encoding?: null; flag?: string }
options?: { encoding?: null; flag?: string },
): T {
const content = fs.readFileSync(filePath, options);
return JSON.parse(content.toString());
Expand Down
131 changes: 131 additions & 0 deletions packages/doctor/test/sys-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as assert from "assert";
import * as fs from "fs";
import * as path from "path";
import { EOL } from "os";
import { SysInfo } from "../src/sys-info";
Expand Down Expand Up @@ -910,4 +911,134 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)`),
});
});
});

describe("isCocoaPodsWorkingCorrectly", () => {
interface ICocoaPodsMockOptions {
// Mimics the ChildProcess result for `asdf current ruby`.
// When omitted, asdf is treated as not installed.
asdfResult?: {
stdout?: string;
stderr?: string;
exitCode?: number | string;
};
podExitCode?: number;
}

const createCocoaPodsSysInfo = (options: ICocoaPodsMockOptions) => {
const appendedFiles: { filePath: string; text: string }[] = [];
const spawnCalls: {
command: string;
options?: ISpawnFromEventOptions;
}[] = [];

const childProcess: any = {
spawnFromEvent: async (
command: string,
args: string[],
event: string,
spawnFromEventOptions?: ISpawnFromEventOptions,
) => {
const fullCommand = `${command} ${args.join(" ")}`;
spawnCalls.push({
command: fullCommand,
options: spawnFromEventOptions,
});

if (fullCommand === "asdf current ruby") {
// Mirror the ChildProcess wrapper: with `ignoreError` it always
// resolves, surfacing a non-zero exitCode instead of throwing when
// asdf is missing/misconfigured.
return (
options.asdfResult || {
stdout: "",
stderr: "spawn asdf ENOENT",
exitCode: "ENOENT",
}
);
}

return {
stdout: "",
stderr: "",
exitCode: options.podExitCode ?? 0,
};
},
exec: async () => ({ stdout: "", stderr: "" }),
execFile: async (): Promise<any> => undefined,
execSync: (): string => null,
};

const fileSystem: any = {
exists: () => true,
extractZip: () => Promise.resolve(),
readDirectory: () => [],
appendFile: (filePath: string, text: string) => {
appendedFiles.push({ filePath, text });
return true;
},
deleteEntry: (filePath: string) =>
fs.rmSync(filePath, { recursive: true, force: true }),
};

const hostInfo: any = {
isDarwin: true,
isWindows: false,
isLinux: false,
};

const helpers = new Helpers(hostInfo);
const sysInfo = new SysInfo(
childProcess,
fileSystem,
helpers,
hostInfo,
null,
androidToolsInfo,
);

return { sysInfo, appendedFiles, spawnCalls };
};

it("writes the active Ruby version to .tool-versions when asdf is available", async () => {
const { sysInfo, appendedFiles, spawnCalls } = createCocoaPodsSysInfo({
asdfResult: {
stdout:
"ruby 3.2.1 /Users/user/app/.tool-versions",
exitCode: 0,
},
});

const result = await sysInfo.isCocoaPodsWorkingCorrectly();

assert.deepEqual(result, true);
assert.deepEqual(appendedFiles.length, 1);
assert.ok(
appendedFiles[0].filePath.endsWith(
path.join("cocoapods", ".tool-versions"),
),
);
// The entry must be newline-terminated so it does not merge with existing content.
assert.deepEqual(appendedFiles[0].text, "ruby 3.2.1\n");

const asdfCall = spawnCalls.find(
(c) => c.command === "asdf current ruby",
);
assert.ok(asdfCall, "expected asdf to be probed");
// The probe must not throw when asdf is missing/misconfigured...
assert.deepEqual(asdfCall.options.ignoreError, true);
// ...and it must resolve the version relative to the invocation directory.
assert.deepEqual(asdfCall.options.spawnOptions.cwd, process.cwd());
});

it("does not write .tool-versions and stays healthy when asdf is not installed", async () => {
const { sysInfo, appendedFiles } = createCocoaPodsSysInfo({
// asdf missing -> wrapper resolves with a non-zero (ENOENT) exit code.
});

const result = await sysInfo.isCocoaPodsWorkingCorrectly();

assert.deepEqual(result, true);
assert.deepEqual(appendedFiles.length, 0);
});
});
});