Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
4b7a4a2
refactor(cli): extract sandbox live state helpers
cv May 2, 2026
2ce87dd
refactor(cli): extract sandbox skill install action
cv May 2, 2026
4cd3cf3
refactor(cli): extract sandbox connect action
cv May 3, 2026
e9dd46e
refactor(cli): extract sandbox status action
cv May 3, 2026
aab6c86
refactor(cli): extract sandbox doctor action
cv May 3, 2026
8908521
refactor(cli): extract sandbox destroy action
cv May 3, 2026
56e4f05
refactor(cli): extract sandbox rebuild action
cv May 3, 2026
8bf1958
refactor(cli): extract upgrade sandboxes action
cv May 3, 2026
38eb84d
refactor(cli): remove runtime bridge
cv May 3, 2026
b2ad5da
refactor(cli): remove legacy dispatch fallbacks
cv May 3, 2026
edd2650
refactor(cli): expose explicit main entrypoint
cv May 3, 2026
a15da95
refactor(cli): add oclif examples for utility commands
cv May 3, 2026
4f57ebb
refactor(cli): validate logs flags with oclif
cv May 3, 2026
8b2d077
refactor(cli): improve sandbox diagnostic command metadata
cv May 3, 2026
a09cc51
refactor(cli): tighten policy and channel parser validation
cv May 3, 2026
75857dc
refactor(cli): improve snapshot command metadata
cv May 3, 2026
11c0676
refactor(cli): require skill install path in oclif
cv May 3, 2026
05f9eca
refactor(cli): add lifecycle confirmation flag aliases
cv May 3, 2026
4ebeae4
refactor(cli): split share into oclif subcommands
cv May 3, 2026
7d72437
Revert "refactor(cli): split share into oclif subcommands"
cv May 3, 2026
0ee5ca5
refactor(cli): split share into oclif subcommands
cv May 3, 2026
928219c
refactor(cli): model debug flags with oclif
cv May 3, 2026
36cce5e
refactor(cli): model onboard flags with oclif
cv May 3, 2026
702afb1
docs: sync oclif UX command reference
cv May 3, 2026
3224350
refactor(cli): extract public argv normalizer
cv May 3, 2026
cd4cdb8
refactor(cli): rename oclif dispatch module
cv May 3, 2026
9499ca6
refactor(cli): normalize policy command ids
cv May 3, 2026
42028ef
test(cli): require oclif command metadata
cv May 3, 2026
a05c6b3
refactor(cli): return typed debug parse results
cv May 3, 2026
1875fe9
refactor(cli): add public command display ids
cv May 3, 2026
36f1dbe
refactor(cli): use oclif summaries in root help
cv May 3, 2026
78bfd5a
refactor(cli): table-drive sandbox dispatch
cv May 3, 2026
96ce61f
refactor(cli): normalize gateway token command id
cv May 3, 2026
266ebc9
refactor(cli): render public oclif help
cv May 3, 2026
dc98994
refactor(cli): add shared oclif command base
cv May 3, 2026
d0d2a70
refactor(cli): use oclif flag relationships
cv May 3, 2026
8396c06
refactor(cli): pass lifecycle typed options
cv May 3, 2026
1532b19
refactor(cli): parse durations with oclif flags
cv May 3, 2026
7c0445a
refactor(cli): project public help through oclif
cv May 3, 2026
3978c32
test(cli): cover oclif metadata routing helpers
cv May 3, 2026
591eabb
test(cli): cover global oclif command adapters
cv May 3, 2026
8a6ecbc
test(cli): cover sandbox oclif command adapters
cv May 3, 2026
e5aa22f
refactor(cli): split share oclif commands
cv May 3, 2026
e179336
test(cli): narrow sandbox logs coverage ignores
cv May 3, 2026
a629813
test(cli): cover remaining global oclif adapters
cv May 3, 2026
c5954e7
test(cli): cover credentials oclif adapter
cv May 3, 2026
3380de9
test(cli): cover runtime utility helpers
cv May 3, 2026
f60a190
test(cli): improve global adapter coverage
cv May 3, 2026
78a16d1
merge: sync simple global adapter coverage
cv May 3, 2026
a58dc90
merge: sync credentials adapter coverage
cv May 3, 2026
034454f
merge: include credentials adapter coverage
cv May 3, 2026
28bc497
merge: sync combined global adapter coverage
cv May 3, 2026
e9ed6ba
test(cli): cover global action facade
cv May 3, 2026
00b288c
merge: sync global action facade coverage
cv May 3, 2026
7bd5f32
test(cli): stabilize coverage dist sourcemaps
cv May 4, 2026
d4b55d9
test(cli): rebaseline clean-dist function coverage
cv May 4, 2026
b872474
refactor(cli): extract maintenance image helpers
cv May 4, 2026
4965762
refactor(cli): extract upgrade sandbox helpers
cv May 4, 2026
9acd660
refactor(cli): extract sandbox destroy helpers
cv May 4, 2026
cc0eaa1
refactor(cli): extract sandbox logs helpers
cv May 4, 2026
847e115
refactor(cli): extract policy channel helpers
cv May 4, 2026
b6c0f5a
test(cli): allow clean coverage variance
cv May 4, 2026
5c98bd1
merge: sync coverage stabilization
cv May 4, 2026
4754f3e
merge: sync maintenance helper extraction
cv May 4, 2026
96f0758
merge: sync upgrade helper extraction
cv May 4, 2026
b6bd105
merge: sync destroy helper extraction
cv May 4, 2026
98fb75d
merge: sync logs helper extraction
cv May 4, 2026
5920d5b
test(cli): keep destroy helpers outside coverage ratchet
cv May 4, 2026
64d1646
merge: sync destroy helper coverage boundary
cv May 4, 2026
717c482
test(cli): keep logs helpers outside coverage ratchet
cv May 4, 2026
688074f
merge: sync logs helper coverage boundary
cv May 4, 2026
1314270
test(cli): keep policy channel helpers outside coverage ratchet
cv May 4, 2026
79d81c2
refactor(cli): group oclif core modules
cv May 4, 2026
5453a89
refactor(cli): split oclif commands by command tree
cv May 4, 2026
bbc297f
test(cli): reset credential command helper cache
cv May 4, 2026
25224c1
test(cli): avoid generic warning assertion in debug output
cv May 4, 2026
1ed9256
refactor(cli): group pure domain helpers
cv May 4, 2026
51799e4
refactor(cli): group openshell adapters
cv May 4, 2026
0b1e877
test(cli): enforce initial layer import boundaries
cv May 4, 2026
5bc2242
test(cli): format layer boundary checker
cv May 4, 2026
073225e
ci(prek): tolerate unmatched biome format chunks
cv May 4, 2026
f1e6725
merge(main): reconcile layer import boundary checks
cv May 6, 2026
3b6e5d2
Merge branch 'main' into test/layer-import-boundaries
cv May 6, 2026
b0b5f3a
test(cli): use path comparison for boundary script entry
cv May 6, 2026
f9e6ff6
test(cli): tighten layer boundary detection
cv May 6, 2026
b85451a
Merge branch 'main' into test/layer-import-boundaries
cv May 6, 2026
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ repos:
hooks:
- id: biome-format
name: Biome format
entry: npx biome format --write
entry: npx biome format --write --no-errors-on-unmatched
language: system
files: ^(biome\.json|package(-lock)?\.json|nemoclaw/package(-lock)?\.json|commitlint\.config\.js|bin/.*\.js|scripts/.*\.(js|ts)|test/.*\.js|nemoclaw/src/.*\.ts)$
pass_filenames: true
Expand Down
308 changes: 308 additions & 0 deletions scripts/check-layer-import-boundaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import ts from "typescript";

type Violation = {
file: string;
line: number;
column: number;
rule: string;
detail: string;
};

type ImportRef = {
specifier: string;
line: number;
column: number;
};

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"]);

function toRepoPath(absPath: string): string {
return path.relative(REPO_ROOT, absPath).split(path.sep).join("/");
}

function isProductionTsFile(absPath: string): boolean {
return absPath.endsWith(".ts") && !absPath.endsWith(".test.ts") && !absPath.endsWith(".spec.ts");
}

function* walk(dir: string): Generator<string> {
if (!existsSync(dir)) return;
for (const entry of readdirSync(dir)) {
if (SKIP_DIRS.has(entry)) continue;
const absPath = path.join(dir, entry);
const stats = statSync(absPath);
if (stats.isDirectory()) {
yield* walk(absPath);
} else if (stats.isFile() && isProductionTsFile(absPath)) {
yield absPath;
}
}
}

function sourceFileFor(absPath: string): ts.SourceFile {
return ts.createSourceFile(
absPath,
readFileSync(absPath, "utf8"),
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
}

function position(sourceFile: ts.SourceFile, node: ts.Node): { line: number; column: number } {
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
return { line: pos.line + 1, column: pos.character + 1 };
}

function collectImportRefs(absPath: string): ImportRef[] {
const sourceFile = sourceFileFor(absPath);
const refs: ImportRef[] = [];

function add(specifier: string, node: ts.Node): void {
const pos = position(sourceFile, node);
refs.push({ specifier, ...pos });
}

function visit(node: ts.Node): void {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
add(node.moduleSpecifier.text, node.moduleSpecifier);
} else if (
ts.isExportDeclaration(node) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
add(node.moduleSpecifier.text, node.moduleSpecifier);
} else if (
ts.isCallExpression(node) &&
((ts.isIdentifier(node.expression) && node.expression.text === "require") ||
node.expression.kind === ts.SyntaxKind.ImportKeyword) &&
node.arguments.length > 0 &&
ts.isStringLiteralLike(node.arguments[0])
) {
add(node.arguments[0].text, node.arguments[0]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment on lines +73 to +90
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and see its overall structure
fd scripts/check-layer-import-boundaries.ts

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Let's also get the line count to understand file size
wc -l scripts/check-layer-import-boundaries.ts

Repository: NVIDIA/NemoClaw

Length of output: 104


🏁 Script executed:

# Now let's examine the collectImportRefs function around lines 73-90
sed -n '60,100p' scripts/check-layer-import-boundaries.ts

Repository: NVIDIA/NemoClaw

Length of output: 1499


🏁 Script executed:

# Let's also check if ImportEqualsDeclaration is mentioned anywhere in the file
rg "ImportEqualsDeclaration" scripts/check-layer-import-boundaries.ts

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Check if there's any configuration (tsconfig.json, biome.json) that might restrict import forms
fd -e json -e js | grep -E "(biome|tsconfig)" | head -20

Repository: NVIDIA/NemoClaw

Length of output: 162


🏁 Script executed:

# Check the biome.json configuration
cat biome.json

Repository: NVIDIA/NemoClaw

Length of output: 3139


🏁 Script executed:

# Check tsconfig files to see if there are any restrictions
cat tsconfig.cli.json

Repository: NVIDIA/NemoClaw

Length of output: 628


🏁 Script executed:

# Search the codebase for any use of "import ... = require"
rg "import\s+\w+\s*=\s*require" --type ts --type js

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Check if there are any tests for the check-layer-import-boundaries script
fd check-layer-import-boundaries

Repository: NVIDIA/NemoClaw

Length of output: 100


🏁 Script executed:

# Let's also check if this import form is used in the src directory at all
rg "import\s+\w+\s*=\s*require" src/ --type ts --type js

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Let's check the SPDX header compliance for this file
head -5 scripts/check-layer-import-boundaries.ts

Repository: NVIDIA/NemoClaw

Length of output: 304


🏁 Script executed:

# Let's verify the suggested diff is syntactically correct by checking TypeScript AST types
# Create a simple test to check if ImportEqualsDeclaration and ExternalModuleReference exist
python3 << 'EOF'
# The suggestion uses:
# ts.isImportEqualsDeclaration(node)
# ts.isExternalModuleReference(node.moduleReference)
# node.moduleReference.expression
# ts.isStringLiteralLike(node.moduleReference.expression)

# These are legitimate TypeScript compiler API methods/properties
# Let's verify by searching for any existing usage patterns in TypeScript

print("Verifying TypeScript AST API for ImportEqualsDeclaration...")
print("The following are valid TypeScript compiler API:")
print("- ts.isImportEqualsDeclaration(node): Checks if node is 'import x = require(...)'")
print("- ts.isExternalModuleReference(): Checks if moduleReference is external")
print("- ts.isStringLiteralLike(): Matches string literals and template literals")
print("\nSuggested fix appears syntactically correct for TypeScript Compiler API.")
EOF

Repository: NVIDIA/NemoClaw

Length of output: 470


🏁 Script executed:

# Let's check if there are any tests or comments in the file that mention ImportEqualsDeclaration
cat -n scripts/check-layer-import-boundaries.ts | grep -A 5 -B 5 -i "importequals\|import.*require"

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Check what import forms are actually used in src/lib (the directory being checked)
rg "^import\s" src/lib --type ts | head -20

Repository: NVIDIA/NemoClaw

Length of output: 1611


🏁 Script executed:

# Also check for any require patterns in src/lib
rg "require\(" src/lib --type ts | head -20

Repository: NVIDIA/NemoClaw

Length of output: 1538


Handle import foo = require("...") in the collector.

collectImportRefs() currently handles ES6 imports, exports, require() calls, and dynamic import(), but misses TypeScript's import x = require("...") form (ImportEqualsDeclaration). While this form is not currently used in the codebase, it remains a valid TypeScript import syntax that would bypass all boundary checks if introduced. The suggested fix is both correct and straightforward.

Suggested change
   function visit(node: ts.Node): void {
     if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
       add(node.moduleSpecifier.text, node.moduleSpecifier);
     } else if (
       ts.isExportDeclaration(node) &&
       node.moduleSpecifier &&
       ts.isStringLiteral(node.moduleSpecifier)
     ) {
       add(node.moduleSpecifier.text, node.moduleSpecifier);
+    } else if (
+      ts.isImportEqualsDeclaration(node) &&
+      ts.isExternalModuleReference(node.moduleReference) &&
+      node.moduleReference.expression &&
+      ts.isStringLiteralLike(node.moduleReference.expression)
+    ) {
+      add(node.moduleReference.expression.text, node.moduleReference.expression);
     } else if (
       ts.isCallExpression(node) &&
       ((ts.isIdentifier(node.expression) && node.expression.text === "require") ||
         node.expression.kind === ts.SyntaxKind.ImportKeyword) &&
       node.arguments.length > 0 &&
🤖 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 `@scripts/check-layer-import-boundaries.ts` around lines 73 - 90, The visitor
in collectImportRefs (function visit) misses TypeScript's
ImportEqualsDeclaration form; update visit to detect
ts.isImportEqualsDeclaration(node) and when node.moduleReference is an
ExternalModuleReference with a string literal expression extract that module
string and call add(moduleText, the string literal node) so import x =
require("...") references are collected like other import/require forms.

ts.forEachChild(node, visit);
}

visit(sourceFile);
return refs;
}

function resolveInternalImport(fromAbsPath: string, specifier: string): string | null {
if (!specifier.startsWith(".")) return null;
const base = path.resolve(path.dirname(fromAbsPath), specifier);
const candidates = [base, `${base}.ts`, `${base}.tsx`, path.join(base, "index.ts")];
const found = candidates.find((candidate) => existsSync(candidate));
return found ? toRepoPath(found) : toRepoPath(`${base}.ts`);
}

function isDomainFile(repoPath: string): boolean {
return repoPath.startsWith("src/lib/domain/");
}

function isAdapterFile(repoPath: string): boolean {
return repoPath.startsWith("src/lib/adapters/");
}

function isCommandFile(repoPath: string): boolean {
return repoPath.startsWith("src/lib/commands/");
}

function isActionFile(repoPath: string): boolean {
if (repoPath.startsWith("src/lib/actions/")) return true;
return /(^|\/)[^/]+-actions?\.ts$/.test(repoPath);
}

function isGeneratedOrRegistryCommandFile(repoPath: string): boolean {
return repoPath === "src/lib/commands/index.ts" || repoPath.endsWith("/common.ts");
}

function importTargetsForbiddenLayer(
fromAbsPath: string,
ref: ImportRef,
forbiddenPrefixes: readonly string[],
forbiddenActionFiles = false,
): string | null {
const target = resolveInternalImport(fromAbsPath, ref.specifier);
if (!target) return null;
if (forbiddenPrefixes.some((prefix) => target.startsWith(prefix))) return target;
if (forbiddenActionFiles && isActionFile(target)) return target;
return null;
}

function addViolation(
violations: Violation[],
file: string,
line: number,
column: number,
rule: string,
detail: string,
): void {
violations.push({ file, line, column, rule, detail });
}

function checkDomainFile(absPath: string, repoPath: string, violations: Violation[]): void {
const imports = collectImportRefs(absPath);
for (const ref of imports) {
if (ref.specifier === "@oclif/core") {
addViolation(
violations,
repoPath,
ref.line,
ref.column,
"domain-purity",
"domain must not import @oclif/core",
);
}
if (ref.specifier === "node:child_process" || ref.specifier === "child_process") {
addViolation(
violations,
repoPath,
ref.line,
ref.column,
"domain-purity",
"domain must not spawn child processes",
);
}
const target = importTargetsForbiddenLayer(
absPath,
ref,
["src/lib/adapters/", "src/lib/commands/", "src/lib/cli/"],
true,
);
if (target) {
addViolation(
violations,
repoPath,
ref.line,
ref.column,
"domain-purity",
`domain must not import ${target}`,
);
}
}

const sourceFile = sourceFileFor(absPath);
function visit(node: ts.Node): void {
if (
ts.isPropertyAccessExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === "process" &&
node.name.text === "exit"
) {
const pos = position(sourceFile, node);
addViolation(
violations,
repoPath,
pos.line,
pos.column,
"domain-purity",
"domain must not call process.exit",
);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
}

function checkActionFile(absPath: string, repoPath: string, violations: Violation[]): void {
for (const ref of collectImportRefs(absPath)) {
if (ref.specifier === "@oclif/core") {
addViolation(
violations,
repoPath,
ref.line,
ref.column,
"actions-no-oclif",
"actions must not import @oclif/core",
);
}
}
}

function checkAdapterFile(absPath: string, repoPath: string, violations: Violation[]): void {
for (const ref of collectImportRefs(absPath)) {
const target = importTargetsForbiddenLayer(absPath, ref, ["src/lib/commands/"], true);
if (target) {
addViolation(
violations,
repoPath,
ref.line,
ref.column,
"adapters-no-workflows",
`adapters must not import command/action layer module ${target}`,
);
}
}
}

function checkCommandFile(absPath: string, repoPath: string, violations: Violation[]): void {
if (isGeneratedOrRegistryCommandFile(repoPath)) return;
const sourceFile = sourceFileFor(absPath);
let commandClassCount = 0;

function isCommandBase(expression: ts.ExpressionWithTypeArguments): boolean {
const text = expression.expression.getText(sourceFile);
return text === "Command" || text === "NemoClawCommand";
}

function visit(node: ts.Node): void {
if (
ts.isClassDeclaration(node) &&
node.heritageClauses?.some((clause) => clause.types.some(isCommandBase))
Comment on lines +257 to +259
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n scripts/check-layer-import-boundaries.ts | sed -n '250,270p'

Repository: NVIDIA/NemoClaw

Length of output: 767


🏁 Script executed:

cat -n scripts/check-layer-import-boundaries.ts | sed -n '240,280p'

Repository: NVIDIA/NemoClaw

Length of output: 1521


🏁 Script executed:

# Let's verify the TypeScript API for heritage clauses
# Check if there are examples of ts.SyntaxKind.ExtendsKeyword usage in the codebase
rg "SyntaxKind\." scripts/ -A 2 -B 2 | head -50

Repository: NVIDIA/NemoClaw

Length of output: 3099


🏁 Script executed:

# Search for heritage clause usage in TypeScript API
rg "heritageClauses" scripts/ -B 3 -A 3

Repository: NVIDIA/NemoClaw

Length of output: 555


🌐 Web query:

TypeScript heritage clause token extends keyword kind property

💡 Result:

Answer: In the TypeScript compiler API, the “extends”/“implements” keyword in a heritage clause is represented by the HeritageClause node’s token property, whose type is a SyntaxKind value (specifically SyntaxKind.ExtendsKeyword or SyntaxKind.ImplementsKeyword). token is not the kind property you’re asking about. Grounding from the sources: - A HeritageClause interface in the compiler API exposes a token property whose value is a SyntaxKind.ExtendsKeyword | SyntaxKind.ImplementsKeyword [1]. - The TypeScript compiler’s SyntaxKind includes ExtendsKeyword as the keyword kind for extends [2]. - A separate API surface (tsserver typings / services) for HeritageClauseSyntax similarly exposes a kind (node kind) and an extendsOrImplementsKeyword token, but that’s distinct from the heritage clause “token kind” you’re asking about in the compiler API [3]. Practical example (compiler API): - For class/interface declarations, iterate node.heritageClauses and check hc.token === ts.SyntaxKind.ExtendsKeyword to find extends clauses; then inspect hc.types for the base types [1]. If you tell me which library API you’re using (raw typescript compiler API, ts-morph, or TypeScript’s language-service node types), I can provide the exact property names/types for that surface.

Citations:


🏁 Script executed:

# Look for tests related to checkCommandFile or one-command-per-file
find . -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "one-command-per-file\|checkCommandFile" 2>/dev/null

Repository: NVIDIA/NemoClaw

Length of output: 43


🏁 Script executed:

# Search for test files related to check-layer-import-boundaries
fd -e test.ts -e spec.ts | xargs grep -l "check-layer-import-boundaries\|layer.*import" 2>/dev/null | head -5

Repository: NVIDIA/NemoClaw

Length of output: 96


🏁 Script executed:

cat -n test/layer-import-boundaries.test.ts | head -100

Repository: NVIDIA/NemoClaw

Length of output: 1075


🏁 Script executed:

# Search for actual command files that might inherit from Command or NemoClawCommand
find . -path ./node_modules -prune -o -name "*.ts" -type f -print | xargs grep -l "extends Command\|extends NemoClawCommand\|implements Command" 2>/dev/null | head -5

Repository: NVIDIA/NemoClaw

Length of output: 247


🏁 Script executed:

# Check one of the command files to see the pattern
cat -n ./src/lib/commands/credentials.ts | head -30

Repository: NVIDIA/NemoClaw

Length of output: 1136


🏁 Script executed:

# Search for implements Command (case-insensitive)
rg "implements\s+.*Command" --type ts | head -20

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Let's look for any helper classes or interfaces that might implement or extend Command
rg "class.*implements.*|interface.*extends.*" --type ts | grep -i command | head -20

Repository: NVIDIA/NemoClaw

Length of output: 172


🏁 Script executed:

# Let's verify the TypeScript API once more by checking if there's a ts.HeritageClause type definition being used
rg "HeritageClause" --type ts | head -10

Repository: NVIDIA/NemoClaw

Length of output: 41


Only count extends clauses as command classes.

The current predicate treats implements Command the same as extends Command, so a helper class can satisfy the exact-one rule without actually being a registered oclif command. Filter on clause.token === ts.SyntaxKind.ExtendsKeyword before checking the base type.

Suggested change
     if (
       ts.isClassDeclaration(node) &&
-      node.heritageClauses?.some((clause) => clause.types.some(isCommandBase))
+      node.heritageClauses?.some(
+        (clause) =>
+          clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.some(isCommandBase),
+      )
     ) {
       commandClassCount += 1;
     }
📝 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
if (
ts.isClassDeclaration(node) &&
node.heritageClauses?.some((clause) => clause.types.some(isCommandBase))
if (
ts.isClassDeclaration(node) &&
node.heritageClauses?.some(
(clause) =>
clause.token === ts.SyntaxKind.ExtendsKeyword && clause.types.some(isCommandBase),
)
) {
commandClassCount += 1;
}
🤖 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 `@scripts/check-layer-import-boundaries.ts` around lines 257 - 259, The
predicate that identifies command classes currently treats any heritage clause
(including implements) as matching; update the check in the class detection
logic (the ts.isClassDeclaration branch that iterates node.heritageClauses and
uses isCommandBase) to only consider extends clauses by adding a filter that
clause.token === ts.SyntaxKind.ExtendsKeyword before calling
clause.types.some(isCommandBase), so only classes that actually extend the
Command base are counted as commands.

) {
commandClassCount += 1;
}
ts.forEachChild(node, visit);
}

visit(sourceFile);
if (commandClassCount !== 1) {
addViolation(
violations,
repoPath,
1,
1,
"one-command-per-file",
`command files must define exactly one registered oclif command class; found ${commandClassCount}`,
);
}
}

export function findLayerImportBoundaryViolations(root = SRC_LIB_ROOT): Violation[] {
const violations: Violation[] = [];
for (const absPath of walk(root)) {
const repoPath = toRepoPath(absPath);
if (isDomainFile(repoPath)) checkDomainFile(absPath, repoPath, violations);
if (isActionFile(repoPath)) checkActionFile(absPath, repoPath, violations);
if (isAdapterFile(repoPath)) checkAdapterFile(absPath, repoPath, violations);
if (isCommandFile(repoPath)) checkCommandFile(absPath, repoPath, violations);
}
return violations;
}

function main(): void {
const violations = findLayerImportBoundaryViolations();
if (violations.length > 0) {
const formatted = violations
.map(
(violation) =>
`${violation.file}:${String(violation.line)}:${String(violation.column)} ${violation.rule}: ${violation.detail}`,
)
.join("\n");
console.error(`Layer import boundary violations:\n${formatted}`);
process.exit(1);
}
console.log("Layer import boundaries passed.");
}

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

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

import { describe, expect, it } from "vitest";

const REPO_ROOT = path.join(import.meta.dirname, "..");
const TSX = path.join(REPO_ROOT, "node_modules", ".bin", "tsx");
const BOUNDARY_SCRIPT = path.join(REPO_ROOT, "scripts", "check-layer-import-boundaries.ts");

describe("CLI layer import boundaries", () => {
it("keeps domain, adapter, action, and command layers separated", () => {
const result = spawnSync(TSX, [BOUNDARY_SCRIPT], {
cwd: REPO_ROOT,
encoding: "utf-8",
});

expect(`${result.stdout}${result.stderr}`).toContain("Layer import boundaries passed.");
expect(result.status).toBe(0);
});
});
Loading