Skip to content
Merged
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
6 changes: 5 additions & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ WORKDIR /app
ENV NODE_ENV=development
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# Prefer sharp's prebuilt libvips instead of probing a host/global libvips
# installation. This keeps dependency installation portable on builders whose
# libvips ABI is newer than the sharp binary expects.
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1

# Install pnpm
RUN corepack enable && corepack prepare pnpm@8.15.4 --activate && npm i -g npm@latest
Expand All @@ -42,4 +46,4 @@ ENV NODE_ENV=production

COPY . .

CMD ["pnpm", "run", "build:release"]
CMD ["pnpm", "run", "build:release"]
5 changes: 4 additions & 1 deletion packages/unraid-api-plugin-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "vitest run",
"prepare": "npm run build"
},
"dependencies": {
Expand All @@ -27,8 +28,10 @@
"@types/inquirer": "9.0.9",
"@types/node": "22.18.0",
"@types/validate-npm-package-name": "4.0.2",
"graphql": "16.11.0",
"class-transformer": "0.5.1",
"class-validator": "0.15.1",
"typescript": "5.9.2"
"typescript": "5.9.2",
"vitest": "3.2.6"
}
}
43 changes: 43 additions & 0 deletions packages/unraid-api-plugin-generator/src/create-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createPlugin } from "./create-plugin";

Copy link
Copy Markdown
Contributor

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:

#!/bin/bash
set -euo pipefail

echo "== file exists =="
git ls-files | rg '(^|/)create-plugin(\.test)?\.ts$|package(-lock)?\.json$|tsconfig.*\.json$|vitest.config.*\.(ts|js|mjs|cjs)$' || true

echo "== imports of create-plugin in test =="
if [ -f packages/unraid-api-plugin-generator/src/create-plugin.test.ts ]; then
  nl -ba packages/unraid-api-plugin-generator/src/create-plugin.test.ts | sed -n '1,40p'
fi

echo "== matching local import styles in same package tests =="
rg -n 'import\s*\{\s*createPlugin\s*\}\s*from' packages/unraid-api-plugin-generator/src -g '*.test.ts' || true

echo "== tsconfig/package config relevant fields =="
fd -a '^(tsconfig|package(-lock)?|vitest.config).*$' packages/unraid-api-plugin-generator . | sed 's#^\./##' | while read -r f; do
  echo "--- $f"
  case "$f" in
    *.json) python3 - <<'PY' "$f"
import json, sys
p=sys.argv[1]
try:
    data=json.load(open(p))
except Exception as e:
    print(e); raise SystemExit(0)
for k in ["type","scripts","moduleResolution","compilerOptions"]:
    v=data
    for sub in k.split("."):
        if isinstance(v, dict): v=v.get(sub)
    if v is not None:
        print(f"{k}: {v}")
PY
        ;;
    *.ts|*.js|*.mjs|*.cjs) sed -n '1,140p' "$f" | nl -ba ;;
    *) true ;;
  esac
done

echo "== repo-wide extensionless TS imports under packages/unraid-api-plugin-generator/src tests? =="
python3 - <<'PY'
import os, re
root = "packages/unraid-api-plugin-generator/src"
pattern = re.compile(r'import\s*\{([^}]*)\}\s*from\s*[\'"]((?!https?://).+)[\'"];\s*$')
for dirpath, _, files in os.walk(root):
    for file in files:
        if not file.endswith(".test.ts"):
            continue
        path = os.path.join(dirpath, file)
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            for lineno, line in enumerate(f, 1):
                m = pattern.match(line.strip())
                if not m:
                    continue
                imp = m.group(2)
                if not imp.lower().endswith((".js", ".js.map")):
                    print(f"{path}:{lineno}:{line.strip()}")
PY

Repository: unraid/api

Length of output: 1150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test import and surrounding lines =="
python3 - <<'PY'
from pathlib import Path
p = Path("packages/unraid-api-plugin-generator/src/create-plugin.test.ts")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if i <= 45 or "createPlugin" in line:
        print(f"{i}: {line}")
PY

echo "== local createPlugin import usages in create-plugin.test.ts =="
python3 - <<'PY'
from pathlib import Path
p = Path("packages/unraid-api-plugin-generator/src/create-plugin.test.ts")
text = p.read_text()
for line in text.splitlines():
    if "createPlugin" in line:
        print(line)
PY

echo "== config json relevant fields =="
for f in package.json packages/unraid-api-plugin-generator/package.json packages/unraid-api-plugin-generator/tsconfig.json packages/unraid-api-plugin-generator/tsconfig.build.json; do
  [ -f "$f" ] || continue
  echo "--- $f"
  python3 - <<'PY' "$f"
import json, sys
p = sys.argv[1]
data = json.load(open(p))
for k in ["type", "scripts", "module", "main"]:
    print(k, ":", data.get(k))
print("compilerOptions:")
co = data.get("compilerOptions", {})
for key in ["module", "moduleResolution", "target", "esModuleInterop", "allowSyntheticDefaultImports", "resolveJsonModule"]:
    print("  " + key + ":", co.get(key))
PY
done

echo "== tsconfig vite/vitest related fields =="
for f in packages/unraid-api-plugin-generator/tsconfig.json packages/unraid-api-plugin-generator/tsconfig.build.json; do
  [ -f "$f" ] || continue
  echo "--- $f"
  sed -n '1,120p' "$f"
done

echo "== root/package vitest references =="
rg -n 'vitest|typescript|moduleResolution|module:|type: "module"' -g 'package.json' -g 'tsconfig*.json' -g 'vitest.*' . || true

Repository: unraid/api

Length of output: 10016


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package lock tsconfig typescript vitest version snippets =="
python3 - <<'PY'
from pathlib import Path
p = Path("package-lock.json")
if not p.exists():
    print("NO package-lock.json")
    raise SystemExit(0)
import json
data=json.load(open(p))
pkgs=data.get("packages", {})
for key in ("node_modules/typescript","node_modules/vitest"):
    if key in pkgs:
        print(key)
        for k in ["version","files","dependencies","peerDependencies"]:
            print(f"  {k}: {pkgs[key].get(k)}")
print()
for key, pkg in pkgs.items():
    if not (key.startswith("packages/unraid-api-plugin-generator") or key == "node_modules/@unraid/api-plugin-generator"):
        continue
    for k in ["packageJSON","version","peerDependencies","dependencies","resolved"]:
        v=pkg.get(k)
        if k == "packageJSON" and isinstance(v, dict):
            print(f"{key}.packageJSON:")
            for subk in ["type","module","scripts","dependencies","devDependencies"]:
                print(f"  {subk}: {v.get(subk)}")
        else:
            print(f"{key}.{k}: {v}")
PY

echo "== lockfile create-plugin package path =="
python3 - <<'PY'
from pathlib import Path
import json
p=Path("package-lock.json")
if not p.exists(): raise SystemExit(0)
data=json.load(open(p))
for key,pkg in data.get("packages",{}).items():
    if "create-plugin" in key or "api-plugin-generator" in (key or ""):
        print(key)
        if isinstance(pkg.get("packageJSON"), dict):
            for k,v in pkg["packageJSON"].items():
                if k in ["type","module","main","exports","scripts","dependencies","devDependencies"]:
                    print(f"  {k}: {v}")
                elif k in ("version","resolved","dependencies","peerDependencies"):
                    print(f"  {k}: {v}")
PY

echo "== lock metadata top lockfileVersion =="
python3 - <<'PY'
from pathlib import Path
import json
print(json.load(open("package-lock.json"))["lockfileVersion"])
PY

Repository: unraid/api

Length of output: 463


Use the .js extension for the local ESM import.

packages/unraid-api-plugin-generator/tsconfig.json uses module: NodeNext / moduleResolution: nodenext, so relative imports in src/**/*.test.ts need file extensions.

Proposed fix
-import { createPlugin } from "./create-plugin";
+import { createPlugin } from "./create-plugin.js";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createPlugin } from "./create-plugin";
import { createPlugin } from "./create-plugin.js";
🤖 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/unraid-api-plugin-generator/src/create-plugin.test.ts` at line 5,
Update the createPlugin import in create-plugin.test.ts to use the .js
extension, preserving the existing local module target and NodeNext-compatible
ESM resolution.

Source: Coding guidelines


const generatedDirectories: string[] = [];

afterEach(async () => {
await Promise.all(
generatedDirectories.splice(0).map((directory) =>
rm(directory, { recursive: true, force: true }),
),
);
});

describe("createPlugin", () => {
it("generates a self-contained scaffold with build and validation commands", async () => {
const targetDirectory = await mkdtemp(join(tmpdir(), "unraid-plugin-generator-"));
generatedDirectories.push(targetDirectory);

const pluginDirectory = await createPlugin("example-plugin", targetDirectory);
const packageJson = JSON.parse(
await readFile(join(pluginDirectory, "package.json"), "utf8"),
);
const tsconfig = JSON.parse(
await readFile(join(pluginDirectory, "tsconfig.json"), "utf8"),
);
const indexSource = await readFile(join(pluginDirectory, "src", "index.ts"), "utf8");
const generatedFiles = await Promise.all(
["config.entity.ts", "index.ts", "example-plugin.resolver.ts"].map((file) =>
readFile(join(pluginDirectory, "src", file), "utf8"),
),
);

expect(packageJson.scripts.validate).toBe("npm run build && npm pack --dry-run");
expect(packageJson.devDependencies.graphql).toBeDefined();
expect(packageJson.peerDependencies.graphql).toBeDefined();
expect(tsconfig.compilerOptions.skipLibCheck).toBe(true);
expect(indexSource).not.toContain("ConfigPersister");
expect(generatedFiles.join("\n")).not.toContain("@unraid/shared");
});
});
21 changes: 9 additions & 12 deletions packages/unraid-api-plugin-generator/src/create-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export async function createPlugin(pluginName: string, targetDir: string = proce
type: "module",
files: ["dist"],
scripts: {
test: "echo \"Error: no test specified\" && exit 1",
build: "tsc",
validate: "npm run build && npm pack --dry-run",
prepare: "npm run build"
},
keywords: [],
Expand All @@ -53,29 +53,25 @@ export async function createPlugin(pluginName: string, targetDir: string = proce
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"@types/ini": "^4.1.1",
"@types/node": "^22.14.0",
"camelcase-keys": "^9.1.3",
"@types/ws": "^8.18.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ini": "^5.0.0",
"nest-authz": "^2.14.0",
"graphql": "^16.11.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"typescript": "^5.8.2",
"zod": "^3.23.8"
"typescript": "^5.8.2"
},
peerDependencies: {
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"camelcase-keys": "^9.1.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ini": "^5.0.0",
"nest-authz": "^2.14.0",
"rxjs": "^7.8.2",
"zod": "^3.23.8"
"graphql": "^16.11.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2"
}
};

Expand All @@ -93,6 +89,7 @@ export async function createPlugin(pluginName: string, targetDir: string = proce
emitDecoratorMetadata: true,
esModuleInterop: true,
strict: true,
skipLibCheck: true,
outDir: "dist",
rootDir: "src"
},
Expand Down

This file was deleted.

3 changes: 1 addition & 2 deletions packages/unraid-api-plugin-generator/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Module, Logger, Inject } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { PluginNameConfigPersister } from "./config.persistence.js";
import { configFeature } from "./config.entity.js";
import { PluginNameResolver } from "./plugin-name.resolver.js";

export const adapter = "nestjs";

@Module({
imports: [ConfigModule.forFeature(configFeature)],
providers: [PluginNameResolver, PluginNameConfigPersister],
providers: [PluginNameResolver],
})
class PluginNamePluginModule {
logger = new Logger(PluginNamePluginModule.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export class PluginNameResolver {
const currentStatus = this.configService.get("plugin-name.enabled", true);
const newStatus = !currentStatus;
this.configService.set("plugin-name.enabled", newStatus);
// The config persister will automatically save the changes.
// This starter mutation is intentionally process-local. Add a host-backed
// persistence service before treating this value as durable configuration.
return newStatus;
}
}
2 changes: 1 addition & 1 deletion packages/unraid-api-plugin-generator/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "src/templates/**/*"]
"exclude": ["node_modules", "dist", "src/templates/**/*", "src/**/*.test.ts"]
}
5 changes: 3 additions & 2 deletions plugin/builder/build-txz.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { join } from "path";
import { $, cd } from "zx";
import { existsSync } from "node:fs";
import { readdir, writeFile } from "node:fs/promises";
import { readdir } from "node:fs/promises";
import { getTxzName, pluginName, startingDir } from "./utils/consts";
import { ensureNodeJs } from "./utils/nodejs-helper";

Expand All @@ -11,6 +11,7 @@ import { apiDir } from "./utils/paths";
import { getVendorBundleName, getVendorFullPath } from "./build-vendor-store";
import { getAssetUrl } from "./utils/bucket-urls";
import { validateStandaloneManifest, getStandaloneManifestPath } from "./utils/manifest-validator";
import { writeVendorArchiveInfo } from "./utils/vendor-archive";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
if rg -nP 'from "\./(?:utils/)?vendor-archive";' \
  plugin/builder/build-txz.ts \
  plugin/builder/utils/vendor-archive.test.ts; then
  exit 1
fi

Repository: unraid/api

Length of output: 349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files:\n'
git ls-files plugin/builder/build-txz.ts plugin/builder/utils/vendor-archive.ts plugin/builder/utils/vendor-archive.test.ts

printf '\nRelevant imports (with file context):\n'
for f in plugin/builder/build-txz.ts plugin/builder/utils/vendor-archive.test.ts; do
  echo "--- $f"
  sed -n '1,30p' "$f"
done

printf '\nPackage / TS config hints:\n'
for f in package.json tsconfig.json; do
  if [ -f "$f" ]; then
    echo "--- $f"
    sed -n '1,220p' "$f"
  fi
done

Repository: unraid/api

Length of output: 6134


Add .js extensions to the local ESM imports.

These imports follow the repository’s ESM convention, where TypeScript imports should include the .js extension to resolve correctly at runtime.

  • plugin/builder/build-txz.ts#L14-L14: import from ./utils/vendor-archive.js.
  • plugin/builder/utils/vendor-archive.test.ts#L5-L5: import from ./vendor-archive.js.
📍 Affects 2 files
  • plugin/builder/build-txz.ts#L14-L14 (this comment)
  • plugin/builder/utils/vendor-archive.test.ts#L5-L5
🤖 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 `@plugin/builder/build-txz.ts` at line 14, Update the local ESM imports in
plugin/builder/build-txz.ts (lines 14-14) and
plugin/builder/utils/vendor-archive.test.ts (lines 5-5) to include the .js
extension, targeting writeVendorArchiveInfo and the vendor-archive import
respectively; no other changes are needed.

Source: Coding guidelines



// Check for manifest files in expected locations
Expand Down Expand Up @@ -89,7 +90,7 @@ const storeVendorArchiveInfo = async (version: string, vendorUrl: string, vendor
});

const configPath = join(configDir, "vendor_archive.json");
await writeFile(configPath, JSON.stringify(configData, null, 2));
await writeVendorArchiveInfo(configPath, configData);

console.log(`Vendor archive information stored in ${configPath}`);
console.log(`API Version: ${version}`);
Expand Down
28 changes: 28 additions & 0 deletions plugin/builder/utils/vendor-archive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { mkdtemp, readFile, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { writeVendorArchiveInfo } from "./vendor-archive";

describe("writeVendorArchiveInfo", () => {
it("keeps generated content and archive metadata deterministic", async () => {
const directory = await mkdtemp(join(tmpdir(), "vendor-archive-"));
const configPath = join(directory, "config", "vendor_archive.json");
Comment on lines +9 to +10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove the temporary directory after the test.

mkdtemp leaves a directory behind on every run. Wrap the test body in try/finally and remove the directory to prevent repeated CI/local runs from accumulating temporary files.

🤖 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 `@plugin/builder/utils/vendor-archive.test.ts` around lines 9 - 10, Update the
test using mkdtemp to wrap its body in a try/finally block, and remove the
created directory in the finally cleanup via the existing filesystem cleanup
utility. Ensure cleanup runs whether the test passes or fails.

const config = {
vendor_store_path: "/boot/config/plugins/dynamix.my.servers/vendor-4.36.0.tar.zst",
api_version: "4.36.0",
};

await writeVendorArchiveInfo(configPath, config);
const firstContents = await readFile(configPath);
const firstStat = await stat(configPath);

await writeVendorArchiveInfo(configPath, config);
const secondContents = await readFile(configPath);
const secondStat = await stat(configPath);

expect(secondContents).toEqual(firstContents);
Comment on lines +16 to +24

Copy link
Copy Markdown
Contributor

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

Exercise the existing-file update path.

Both writes use the same configuration, so a regression that fails to update stale metadata still passes. Add a second configuration with a changed version/path and assert that the existing file is updated.

🤖 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 `@plugin/builder/utils/vendor-archive.test.ts` around lines 16 - 24, Add a
distinct second configuration in the writeVendorArchiveInfo test with changed
version and path values, then use it for the second write and assert the
existing file contents reflect the updated configuration rather than the
original metadata.

expect(firstStat.mtimeMs).toBe(0);
expect(secondStat.mtimeMs).toBe(0);
});
});
37 changes: 37 additions & 0 deletions plugin/builder/utils/vendor-archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { mkdir, readFile, utimes, writeFile } from "node:fs/promises";
import { dirname } from "node:path";

export interface VendorArchiveInfo {
vendor_store_path: string;
api_version: string;
}

// vendor_archive.json is generated during every TXZ build. A wall-clock mtime
// on that one file made otherwise unchanged package trees produce different
// tar streams and hashes. Epoch is valid for tar and gives every builder the
// same metadata without depending on checkout or invocation time.
const DETERMINISTIC_MTIME = new Date(0);

export async function writeVendorArchiveInfo(
configPath: string,
configData: VendorArchiveInfo,
): Promise<void> {
const contents = `${JSON.stringify(configData, null, 2)}\n`;

await mkdir(dirname(configPath), { recursive: true });

let currentContents: string | undefined;
try {
currentContents = await readFile(configPath, "utf8");
} catch (error) {
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
throw error;
}
}

if (currentContents !== contents) {
await writeFile(configPath, contents, "utf8");
}

await utimes(configPath, DETERMINISTIC_MTIME, DETERMINISTIC_MTIME);
Comment on lines +33 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C3 'writeFile|chmod|mode|makepkg|chown|tar' plugin/builder api

Repository: unraid/api

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file =="
wc -l plugin/builder/utils/vendor-archive.ts
cat -n plugin/builder/utils/vendor-archive.ts

echo "== focused plugin/builder files =="
git ls-files 'plugin/builder/**' | sed -n '1,200p'
rg -n "vendor_archive|vendor-archive|txz|tar|cpio|compress|makepkg|chmod|chmod|utimes|writeFile|appendFile|mkdir" plugin/builder -S --glob '!**/node_modules/**' --glob '!**/dist/**'

echo "== focused api scripts references =="
rg -n "vendor_archive|vendor-archive|txz|makepkg|plugin.*builder|builder/utils/vendor-archive" api -S --max-count 100

Repository: unraid/api

Length of output: 25756


🌐 Web query:

Node.js fs.promises writeFile default mode permissions umask documentation

💡 Result:

In Node.js, the fs.promises.writeFile method uses a default file mode of 0o666 when creating a new file [1][2][3]. This mode specifies that the file is readable and writable by the owner, group, and others [4]. Important considerations regarding this mode include: 1. Interaction with umask: The mode you specify (or the default 0o666) is not necessarily the final permission set for the file [4]. When a file is created, the operating system applies the process umask, which removes specific permission bits [4]. For example, if the process umask is 0o022, a file created with the default 0o666 mode will result in final permissions of 0o644 (read/write for the owner, and read-only for the group and others) [4]. 2. Scope of the mode option: The mode option only affects the file if it is newly created [1][3]. If the file already exists and is being overwritten by writeFile, the existing file permissions remain unchanged [1][3]. 3. Thread-safety warning: Relying on the current process umask or modifying it using process.umask can be problematic [4]. Node.js has deprecated the no-argument form of process.umask (DEP0139) because it is not thread-safe; calling it involves a temporary change to the process mask that could affect concurrent file operations in a multi-threaded or asynchronous environment [4]. Instead of modifying the umask, it is generally recommended to explicitly pass the desired mode in the options object if specific permissions are required for the new file [4].

Citations:


Normalize vendor_archive.json permissions to a fixed mode.

Normalize the file permissions after writing, for example by setting the mode to 0o644, so TXZ builds do not embed differing metadata for a newly created JSON file when builders run under different umasks.

🤖 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 `@plugin/builder/utils/vendor-archive.ts` around lines 33 - 36, After writing
vendor_archive.json in the archive-generation flow, explicitly set its
permissions to the fixed 0o644 mode before or alongside the deterministic
timestamp update. Update the code around writeFile and utimes so builds produce
consistent file metadata regardless of the process umask.

}
2 changes: 1 addition & 1 deletion plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"build:watch": "./scripts/dc.sh pnpm run build:watcher",
"docker:build": "docker compose build",
"docker:run": "SKIP_HOST_BUILD=true ./scripts/dc.sh bash -c 'pnpm build && exec bash'",
"predocker:build-and-run": "pnpm install && pnpm --filter @unraid/ui run build:wc && pnpm --filter @unraid/web run build && pnpm --filter @unraid/api run build:release",
"predocker:build-and-run": "SHARP_IGNORE_GLOBAL_LIBVIPS=1 pnpm install && pnpm --filter @unraid/ui run build:wc && pnpm --filter @unraid/web run build && pnpm --filter @unraid/api run build:release",
"docker:build-and-run": "pnpm run docker:build && pnpm run docker:run",
"// Environment management": "",
"env:init": "cp .env.example .env",
Expand Down
16 changes: 12 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.