fix(cli): an unreadable project folder is reported as unreadable, not as holding no composition

resolveProjectOrThrow checked index.html with existsSync, which is false for EACCES/EPERM as well as a missing file. A folder the process may not open (macOS Files and Folders, a cloud-synced drive, permissions) was reported as 'No composition found, run init', and agents retried it in a loop. Only ENOENT/ENOTDIR now mean no composition; any other access error says the folder cannot be read and why.
This commit is contained in:
Miguel Angel Simon Sierra
2026-10-29 03:17:51 -04:00
parent cf15892425
commit ffabeb89a4
2 changed files with 37 additions and 8 deletions
+18 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, basename } from "node:path";
import { InvalidProjectError, resolveLintTarget, resolveProjectOrThrow } from "./project.js";
@@ -42,6 +42,23 @@ describe("resolveProjectOrThrow", () => {
}
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"says a folder it may not open is unreadable, not that it holds no composition",
() => {
const dir = mkdtempSync(join(tmpdir(), "hf-unreadable-project-"));
try {
writeFileSync(join(dir, "index.html"), "<html></html>");
chmodSync(dir, 0o444);
expect(() => resolveProjectOrThrow(dir)).toThrowError(
expect.objectContaining({ title: "Cannot read " + dir }),
);
} finally {
chmodSync(dir, 0o755);
rmSync(dir, { recursive: true, force: true });
}
},
);
it("rejects a directory without index.html", () => {
const dir = mkdtempSync(join(tmpdir(), "hf-empty-project-"));
try {
+19 -7
View File
@@ -1,5 +1,5 @@
import { failCommand } from "./commandResult.js";
import { existsSync, statSync } from "node:fs";
import { accessSync, existsSync, statSync } from "node:fs";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import { errorBox } from "../ui/format.js";
import { trackCommandFailure } from "../telemetry/events.js";
@@ -55,12 +55,24 @@ export function resolveProjectOrThrow(
if (!existsSync(dir)) {
throw new InvalidProjectError("Not a directory: " + dir);
}
if (options.requireIndex !== false && !existsSync(indexPath)) {
throw new InvalidProjectError(
"No composition found in " + dir,
"No index.html file found.",
"Run npx hyperframes init to create a new composition.",
);
if (options.requireIndex !== false) {
try {
accessSync(indexPath);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== "ENOENT" && code !== "ENOTDIR") {
throw new InvalidProjectError(
"Cannot read " + dir,
`index.html could not be checked (${code}): the folder exists but this process may not open it.`,
"Check the folder's permissions, or on macOS allow your terminal or agent access to that folder (Files and Folders).",
);
}
throw new InvalidProjectError(
"No composition found in " + dir,
"No index.html file found.",
"Run npx hyperframes init to create a new composition.",
);
}
}
return { dir, name, indexPath };