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
13 changes: 7 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,12 @@ repos:
files: ^(nemoclaw-blueprint/.*\.yaml$|nemoclaw/openclaw\.plugin\.json$|schemas/.*\.json$)
priority: 10

- id: direct-credential-env
name: Direct credential env guard
entry: npx tsx scripts/check-direct-credential-env.ts
- id: repository-checks
name: Repository checks
entry: npm run checks
language: system
files: ^src/lib/onboard\.ts$
pass_filenames: true
files: ^(bin/.*\.(cjs|js|mjs)$|src/.*\.tsx?$|scripts/.*\.(cjs|js|mjs|ts|tsx)$|test/.*\.(cjs|js|mjs|ts|tsx)$|nemoclaw/src/.*\.tsx?$)
pass_filenames: false
priority: 10

- repo: https://github.com/shellcheck-py/shellcheck-py
Expand Down Expand Up @@ -266,7 +266,8 @@ repos:
hooks:
- id: test-cli
name: Test (CLI)
entry: bash -c 'npm run clean:cli && npm run build:cli && npm run dist:sourcemaps:check && npx vitest run --project cli --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/cli --coverage.include="bin/**/*.js" --coverage.include="dist/lib/**/*.js" --coverage.exclude="test/**/*.js" --coverage.exclude="test/**/*.ts" && npx tsx scripts/check-coverage-ratchet.ts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json "CLI coverage"'
entry: >-
bash -c 'node -e "require(\"node:fs\").rmSync(\"dist\", { recursive: true, force: true })" && npm run build:cli && npx tsx scripts/check-dist-sourcemaps.ts dist && npx vitest run --project cli --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reportsDirectory=coverage/cli --coverage.include="bin/**/*.js" --coverage.include="dist/lib/**/*.js" --coverage.exclude="test/**/*.js" --coverage.exclude="test/**/*.ts" && npx tsx scripts/check-coverage-ratchet.ts coverage/cli/coverage-summary.json ci/coverage-threshold-cli.json "CLI coverage"'
language: system
pass_filenames: false
files: ^(bin/|src/|test/)
Expand Down
25 changes: 13 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
.PHONY: check lint format format-biome lint-ts format-ts check-installer-hash docs docs-strict docs-live docs-clean

check:
npx prek run --all-files
@echo "All checks passed."
npm run check

lint: check
lint:
npm run check

# Targeted subproject checks (not part of `make check` — use for focused runs).
lint-ts:
cd nemoclaw && npm run check
npm run lint:ts

format: format-biome
format:
npm run format

format-biome:
npx biome format --write .
npm run format

format-ts:
cd nemoclaw && npm run lint:fix && npm run format
npm run format:ts

# --- Integrity checks ---

check-installer-hash:
bash scripts/check-installer-hash.sh
npm run check:installer-hash

# --- Documentation ---

docs:
uv run --group docs sphinx-build -b html docs docs/_build/html
npm run docs

docs-strict:
uv run --group docs sphinx-build -W -b html docs docs/_build/html
npm run docs:strict

docs-live:
uv run --group docs sphinx-autobuild docs docs/_build/html --open-browser
npm run docs:live

docs-clean:
rm -rf docs/_build
npm run docs:clean
8 changes: 4 additions & 4 deletions ci/coverage-threshold-cli.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lines": 22,
"functions": 31.8,
"branches": 22,
"statements": 22
"lines": 33.2,
"functions": 29.8,
"branches": 23.4,
"statements": 32.7
}
20 changes: 13 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
"scripts": {
"preinstall": "node scripts/check-node-version.js",
"test": "vitest run",
"clean:cli": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
"dist:sourcemaps:check": "tsx scripts/check-dist-sourcemaps.ts dist",
"lint": "biome lint . && npm run check:credential-env",
"lint:fix": "biome lint --write . && npm run check:credential-env",
"format": "biome format --write .",
"format:check": "biome format .",
"check:credential-env": "tsx scripts/check-direct-credential-env.ts src/lib/onboard.ts",
"check": "npx prek run --all-files",
"checks": "tsx scripts/checks/run.ts",
"lint": "npx @biomejs/biome lint . && npm run checks",
"lint:fix": "npx @biomejs/biome lint --write . && npm run checks",
"lint:ts": "cd nemoclaw && npm run check",
"format": "npx @biomejs/biome format --write .",
"format:check": "npx @biomejs/biome format --check .",
"format:ts": "cd nemoclaw && npm run lint:fix && npm run format",
"check:installer-hash": "bash scripts/check-installer-hash.sh",
"typecheck": "tsc -p jsconfig.json",
"build:cli": "tsc -p tsconfig.src.json && tsc -p nemoclaw-blueprint/tsconfig.json",
"typecheck:cli": "tsc -p tsconfig.cli.json",
Expand All @@ -38,6 +40,10 @@
"source-shape:scan": "tsx scripts/find-source-shape-tests.ts --metrics",
"source-shape:check": "tsx scripts/find-source-shape-tests.ts --check",
"bump:version": "tsx scripts/bump-version.ts",
"docs": "uv run --group docs sphinx-build -b html docs docs/_build/html",
"docs:strict": "uv run --group docs sphinx-build -W -b html docs docs/_build/html",
"docs:live": "uv run --group docs sphinx-autobuild docs docs/_build/html --open-browser",
"docs:clean": "rm -rf docs/_build",
"prepare": "if command -v tsc >/dev/null 2>&1 || [ -x node_modules/.bin/tsc ]; then npm run build:cli; fi && (npm install --omit=dev --ignore-scripts 2>/dev/null || true) && if [ -d .git ]; then bash scripts/npm-link-or-shim.sh; if command -v prek >/dev/null 2>&1; then prek install; else echo \"Skipping git hook setup (prek not installed)\"; fi; fi",
"prepublishOnly": "git describe --tags --match 'v*' | sed 's/^v//' > .version && test -s .version && cd nemoclaw && env -u npm_config_global -u npm_config_prefix -u npm_config_omit npm install --ignore-scripts && ./node_modules/.bin/tsc"
},
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-coverage-ratchet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function main(): void {
const [summaryPath, thresholdPath, label = "coverage"] = process.argv.slice(2);
if (!summaryPath || !thresholdPath) {
throw new Error(
"Usage: check-coverage-ratchet.ts <coverage-summary.json> <coverage-threshold.json> [label]",
"Usage: coverage-ratchet.ts <coverage-summary.json> <coverage-threshold.json> [label]",
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ function scriptKindForPath(filePath: string): ts.ScriptKind {
function main(): void {
const filePaths = process.argv.slice(2).filter((arg) => arg !== "--");
if (filePaths.length === 0) {
console.error("Usage: tsx scripts/check-direct-credential-env.ts FILE...");
console.error("Usage: tsx scripts/checks/direct-credential-env.ts FILE...");
process.exitCode = 2;
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type ImportRef = {
column: number;
};

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const SRC_LIB_ROOT = path.join(REPO_ROOT, "src", "lib");
const SKIP_DIRS = new Set([".git", "coverage", "dist", "node_modules"]);

Expand Down
121 changes: 121 additions & 0 deletions scripts/checks/no-coverage-ignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* Guard source files against coverage tool exclusion directives.
*
* Coverage reports should reflect executable code honestly. If a path is only
* covered by subprocess integration tests, keep it visible in coverage and let
* the ratchet baseline account for that instead of hiding it from the report.
*/

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const SCAN_ROOTS = ["bin", "src", "scripts", "test", "nemoclaw/src"];
const SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".mjs", ".ts", ".tsx"]);
const SKIP_DIRS = new Set([".git", "coverage", "dist", "node_modules"]);
const FORBIDDEN_DIRECTIVE = ["v8", "ignore"].join(" ");
const FORBIDDEN_DIRECTIVE_PATTERN = new RegExp(
String.raw`(?:\/\/|\/\*)\s*${FORBIDDEN_DIRECTIVE}\b`,
);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
export interface CoverageIgnoreViolation {
filePath: string;
line: number;
column: number;
text: string;
}

export function findCoverageIgnoreDirectives(
sourceText: string,
filePath: string,
): CoverageIgnoreViolation[] {
const violations: CoverageIgnoreViolation[] = [];
for (const [index, lineText] of sourceText.split(/\r?\n/).entries()) {
const match = FORBIDDEN_DIRECTIVE_PATTERN.exec(lineText);
if (match) {
violations.push({
filePath,
line: index + 1,
column: match.index + 1,
text: lineText.trim(),
});
}
}
return violations;
}

export function checkFiles(filePaths: readonly string[]): CoverageIgnoreViolation[] {
return filePaths.flatMap((filePath) => {
const absolutePath = path.resolve(REPO_ROOT, filePath);
return findCoverageIgnoreDirectives(
fs.readFileSync(absolutePath, "utf-8"),
path.relative(REPO_ROOT, absolutePath),
);
});
}

export function formatViolations(violations: readonly CoverageIgnoreViolation[]): string {
const directive = FORBIDDEN_DIRECTIVE;
return [
`Coverage exclusion directives are not allowed (${directive}).`,
"Keep code visible to coverage reports and ratchet the honest baseline instead.",
"",
...violations.map(
(violation) =>
`${violation.filePath}:${violation.line}:${violation.column} ${violation.text}`,
),
].join("\n");
}

function sourceFiles(): string[] {
return SCAN_ROOTS.flatMap((root) => [...walkSourceFiles(path.join(REPO_ROOT, root))]).map(
(filePath) => path.relative(REPO_ROOT, filePath),
);
}

function* walkSourceFiles(dir: string): Generator<string> {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) yield* walkSourceFiles(fullPath);
continue;
}
if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
yield fullPath;
}
}
}

function isScannedSourcePath(filePath: string): boolean {
return (
filePath.length > 0 &&
SCAN_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`)) &&
SOURCE_EXTENSIONS.has(path.extname(filePath))
);
}

function normalizeCliPaths(args: readonly string[]): string[] {
return args
.filter((arg) => arg !== "--")
.map((arg) => path.relative(REPO_ROOT, path.resolve(arg)))
.filter(isScannedSourcePath);
}

function main(): void {
const cliPaths = normalizeCliPaths(process.argv.slice(2));
const filePaths = cliPaths.length > 0 ? cliPaths : sourceFiles();
const violations = checkFiles(filePaths);
if (violations.length > 0) {
console.error(formatViolations(violations));
process.exitCode = 1;
}
}

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? "")) {
main();
}
50 changes: 50 additions & 0 deletions scripts/checks/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/** Runs local repository checks that are not first-class Biome rules. */

import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";

type CheckCommand = {
name: string;
command: string;
args: string[];
};

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const TSX = process.platform === "win32" ? "tsx.cmd" : "tsx";
const CHECKS: readonly CheckCommand[] = [
{
name: "direct-credential-env",
command: TSX,
args: ["scripts/checks/direct-credential-env.ts", "src/lib/onboard.ts"],
},
{
name: "no-coverage-ignore",
command: TSX,
args: ["scripts/checks/no-coverage-ignore.ts"],
},
{
name: "layer-import-boundaries",
command: TSX,
args: ["scripts/checks/layer-import-boundaries.ts"],
},
];

function main(): void {
for (const check of CHECKS) {
const result = spawnSync(check.command, check.args, {
cwd: REPO_ROOT,
encoding: "utf-8",
stdio: "inherit",
});
if (result.status !== 0) {
console.error(`Check failed: ${check.name}`);
process.exit(result.status ?? 1);
}
}
}

main();
1 change: 0 additions & 1 deletion src/lib/actions/maintenance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- exercised through CLI subprocess maintenance tests. */

import { prompt as askPrompt } from "../credentials/store";
import {
Expand Down
1 change: 0 additions & 1 deletion src/lib/actions/sandbox/connect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- exercised through CLI subprocess connect tests. */

import { spawnSync } from "node:child_process";
import os from "node:os";
Expand Down
1 change: 0 additions & 1 deletion src/lib/actions/sandbox/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- exercised through CLI subprocess destroy/rebuild tests. */

import fs from "node:fs";
import path from "node:path";
Expand Down
1 change: 0 additions & 1 deletion src/lib/actions/sandbox/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- exercised through CLI subprocess doctor tests. */

import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
Expand Down
1 change: 0 additions & 1 deletion src/lib/actions/sandbox/gateway-state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/* v8 ignore start -- extracted legacy sandbox liveness paths are covered through CLI subprocess tests. */

import fs from "node:fs";
import os from "node:os";
Expand Down
Loading
Loading