Skip to content

allow user to setNode - #166

Open
alexcos20 wants to merge 4 commits into
feature/cli_globalfrom
feature/set_node
Open

allow user to setNode#166
alexcos20 wants to merge 4 commits into
feature/cli_globalfrom
feature/set_node

Conversation

@alexcos20

@alexcos20 alexcos20 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Fixes #165

Switch Ocean Node at runtime, and start without one

Adds setNode / getNode so the active Ocean Node can be changed from inside the
interactive loop, and makes NODE_URL optional so the CLI can start with no node at all:

ocean-cli                          # starts with no NODE_URL set
# > getComputeEnvironments         -> refused: "No Ocean Node set. Run `setNode <nodeUrl>` first"
# > setNode http://127.0.0.1:8001  -> "Using node: http://127.0.0.1:8001 (version …)"
# > getComputeEnvironments         -> works
# > setNode <other node>           -> switches again, no restart

Base branch: feature/cli_global (this branch sits on it with no other commits).

Why

The node was fixed at process start: createCLI() hard-exited when NODE_URL was unset, and
nothing could change it afterwards. Pointing the CLI at a different node meant quitting the
REPL and restarting it with a new environment, losing the session.

Changes

Node selection — new src/nodeConnection.ts

All node lifecycle logic moves here; cli.ts only calls into it. It exposes
startP2P / ensureP2PReady / stopP2P, validateNode, and
getCurrentNodeUrl / setCurrentNodeUrl / hasNode.

process.env.NODE_URL stays the single source of truth. That is the reason the change is
small: the Commands constructor (src/commands.ts:77) and getMetadataURI()
(src/helpers.ts:497) already re-read that variable on every use, so switching node is just
mutating it — no new state to thread through, and commands.ts / helpers.ts are untouched.

setNode health-checks the candidate with ProviderInstance.getNodeStatus under an
AbortSignal.timeout (10 s HTTP, 30 s P2P, where a bare peer id may need a DHT lookup) and
mutates the env var only on success. A failed switch therefore changes nothing and there
is no rollback to implement. Over P2P the on-demand dial is the reachability check.

NODE_URL becomes optional, gated by one hook

The NODE_URL hard exit is gone; RPC and PRIVATE_KEY/MNEMONIC are still required. With
no node set, a single root-level commander preAction hook in src/cli.ts refuses every
command outside NODE_FREE_COMMANDS (setNode, getNode, help).

Three things about the gate worth a reviewer's attention:

  • It is one hook, so it covers the REPL and one-shot mode without touching ~40 action
    bodies.
  • It keys off actionCommand.name(), the canonical name, so aliases (useNode,
    currentNode, h) resolve for free.
  • It throws a plain Error, not a CommanderError. That is deliberate: the existing
    catch in runTokens prints it in red and keeps the REPL alive, while main()'s catch
    reports it and exits 1, so scripts still see a non-zero status. No new error machinery.

libp2p is now started eagerly — and, necessarily, stopped

Requested behaviour: start P2P at startup rather than lazily, because connecting to bootstrap
peers and warming the DHT takes seconds and should not happen on the user's first setNode.
startP2P is fired at startup and not awaited, so the prompt appears immediately while
peers connect; anything P2P-bound awaits the stored promise via ensureP2PReady().

Implementing that surfaced a latent bug that had to be fixed for this to be safe:

A started libp2p node prevents the process from ever exiting. It holds the event loop
open, and even a clean stop() leaves a MessagePort behind — stop() itself completes in
~2 ms and reports status stopped, yet process.getActiveResourcesInfo() still lists a
MessagePort afterwards. Nothing in the codebase ever stopped libp2p.

This is already latent on the base branch: P2P sessions only ever terminate because the
error path calls process.exit(1); a successful one-shot P2P command would hang. Starting
libp2p unconditionally would have extended that hang to every HTTP user, so this PR fixes it:

  • stopP2P() waits for a still-pending start before stopping. Without this it stops
    nothing and the node comes up after cleanup — exactly what was observed, with
    libp2p node started. printing after shutdown had already run. The wait is bounded so a
    start dialing unreachable peers cannot stall exit.
  • index.ts exits explicitly when libp2p had been running, after flushOutput() drains
    stdout/stderr — a piped stdout (tests, scripts) can still hold buffered output that
    process.exit() would silently discard.
  • The eager start is skipped in one-shot mode (AVOID_LOOP_RUN=true). A one-shot run has
    no later command to warm up for, so it would only pay startup + shutdown cost. One-shot runs
    that do target a P2P node still get libp2p on demand via validateNode
    ensureP2PReady, which passes the active node URL so a bare peer id keeps its
    localhost-multiaddr bootstrap entry (CI's p2p leg depends on that entry to reach the local
    Barge node).

The 80-line wait-for-target-peer polling block in cli.ts is deleted — validateNode does
the same job, since the dial establishes the connection. That also removes the
(ProviderInstance as any).p2pProvider?.libp2pNode reach-in in favour of the public
getLibp2pNode().

Smaller items

  • DISABLE_P2P=true skips libp2p entirely, for HTTP-only users who do not want the
    bootstrap dials. Combined with a P2P NODE_URL it is a contradiction the session cannot
    recover from, so that exits 1 with a clear message at startup.
  • Chain-mismatch warning. chainId comes from the RPC, never from the node, so setNode
    warns when they disagree. NodeStatus has no chainIds field, so this reads
    status.provider[].chainId / status.indexer[].chainId (strings). It is bounded by a 5 s
    Promise.race with the timer cleared, so an unresponsive RPC neither stalls the command nor
    delays process exit — and the switch itself never touches the RPC or signer.
  • command.aliases() (plural) in src/index.ts. The previous alias() returned only the
    first alias, silently making any additional alias unreachable in the REPL and in
    tab-completion.
  • runRepl extracted from replMenu.test.ts into test/util.ts with an env-override
    option (a key set to undefined is deleted, which is how a test starts the CLI with no
    NODE_URL), instead of duplicating the spawn harness.

Docs

  • README.mdNODE_URL moved to the optional section and described as the initial
    node, with the node-less startup behaviour; DISABLE_P2P documented; setNode / getNode
    added to the examples and to the named-options table, with the caveats below.
  • CLAUDE.md — required/optional env vars, the new commands, the rewritten
    "Transport: HTTP vs P2P, and node selection" section, and gotchas for the gate, the alias
    getter, and the libp2p exit behaviour.

Notes for users switching nodes

Documented in the README, no code implications:

  • Auth tokens are per node — a token from generateAuthToken on node A is not valid on
    node B.
  • Compute jobs live on the node that started them — after a switch, getJobStatus /
    downloadJobResults query the new node; switch back for older jobs.
  • Prefer a full multiaddr for a node on your own machine — a bare peer id has to be found
    through the DHT, which may not advertise localhost addresses.
  • In one-shot mode setNode only validates and prints; the switch dies with the process.
    Use NODE_URL for scripted runs.

Summary by CodeRabbit

  • New Features

    • Start the CLI without configuring a node and select one interactively with setNode or useNode.
    • View the active node and connection details with getNode.
    • Optionally disable peer-to-peer networking with DISABLE_P2P.
    • Node reachability and chain compatibility are validated before use.
    • Commands requiring a node are gated until one is selected.
  • Documentation

    • Updated setup, command reference, and runtime behavior documentation with node-management examples.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 13955187-2f09-47ea-9dd7-ffb3e3f115f7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
test/util.ts (2)

46-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a single source for the REPL prompt string.

REPL_PROMPT duplicates PROMPT in src/index.ts. test/replMenu.test.ts line 44 counts occurrences of this exact string, so a change in src/index.ts weakens that assertion silently instead of failing it. Move the literal into a small shared module (for example src/constants.ts) and import it in both places.

🤖 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 `@test/util.ts` around lines 46 - 48, Move the duplicated REPL prompt literal
from test/util.ts and src/index.ts into a shared constant exported by a small
module such as constants.ts. Update both the PROMPT usage in src/index.ts and
REPL_PROMPT usage in test/util.ts to import that shared constant, preserving the
existing prompt value and test behavior.

88-104: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Terminate the child process when runRepl times out.

runRepl only updates its own promise on close, while test/setNode.test.ts sets a 120s Mocha timeout. If the npx tsx CLI hangs, Mocha may abandon the test while the child process remains alive because no child SIGKILL timer is installed. Add a bounded timeout/killSignal to spawn and handle close so the non-zero timeout code becomes a failing result instead of leaving an orphan child.

🤖 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 `@test/util.ts` around lines 88 - 104, The runRepl child process lacks
timeout-based termination and timeout-result handling. Update the spawn call and
its close handling in runRepl to apply a bounded timeout with a kill signal, and
ensure termination produces a non-zero result that rejects or otherwise fails
the run instead of leaving the process alive.
src/cli.ts (1)

127-144: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider skipping the startup reachability probe in one-shot mode.

createCLI() awaits validateNode() on every invocation. In one-shot mode (AVOID_LOOP_RUN=true) this adds a blocking status request before the requested command runs, and an unreachable node costs the full 10 s HTTP or 30 s P2P timeout on each scripted call. The message "Commands may fail" adds little value there, because the command itself reports its own failure. Gate the probe on process.env.AVOID_LOOP_RUN !== "true", matching the eager startP2P decision above.

♻️ Proposed change
-    if (hasNode()) {
+    // Interactive only: a one-shot run gets its reachability signal from the command
+    // it was asked to run, so probing first only adds latency.
+    if (hasNode() && process.env.AVOID_LOOP_RUN !== "true") {
🤖 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 `@src/cli.ts` around lines 127 - 144, Skip the startup reachability probe in
createCLI when process.env.AVOID_LOOP_RUN === "true"; only call validateNode and
print its success or unreachable-node messages in interactive mode, while
preserving the existing behavior otherwise.
src/nodeConnection.ts (1)

82-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Avoid as any on the setupP2P options object.

The setup options still need a non-any type; narrow the cast only to the libp2p property if @oceanprotocol/lib 9.0.0-next.6 does not accept it at the options level.

🤖 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 `@src/nodeConnection.ts` around lines 82 - 90, Remove the broad as any cast
from the options passed to ProviderInstance.setupP2P, preserving type checking
for the full setup options object. If the libp2p configuration remains
incompatible with the library type, narrow the cast to only the libp2p property
while keeping bootstrapPeers and the surrounding options strongly typed.

Source: Coding guidelines

🤖 Prompt for all review comments with 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.

Inline comments:
In `@CLAUDE.md`:
- Line 168: Update the startP2P description in CLAUDE.md to state that eager
startup occurs for normal invocations but is skipped when AVOID_LOOP_RUN="true",
matching the condition in cli.ts. Preserve the existing details about non-lazy
startup, awaiting behavior, and readiness handling.

In `@README.md`:
- Around line 90-92: Update the fenced code block containing export
NODE_URL='XXXX' to specify the bash language, matching the neighboring fenced
blocks and satisfying markdownlint MD040.

In `@src/index.ts`:
- Around line 245-258: Update the catch block to set process.exitCode to 1
instead of immediately calling process.exit(1), allowing the existing finally
cleanup to flush output. In the finally block, ensure the stopP2P() false path
explicitly flushes output and exits after cleanup, while preserving the existing
stopP2P() true behavior.

In `@test/setNode.test.ts`:
- Around line 104-115: Update both affected tests in test/setNode.test.ts at
lines 104-115 and 124-133 to include RPC: LIVE_RPC in the runRepl environment
overrides alongside their existing NODE_URL values, ensuring
Commands.initializeSigner() reaches the live Barge endpoint and the assertions
execute.

---

Nitpick comments:
In `@src/cli.ts`:
- Around line 127-144: Skip the startup reachability probe in createCLI when
process.env.AVOID_LOOP_RUN === "true"; only call validateNode and print its
success or unreachable-node messages in interactive mode, while preserving the
existing behavior otherwise.

In `@src/nodeConnection.ts`:
- Around line 82-90: Remove the broad as any cast from the options passed to
ProviderInstance.setupP2P, preserving type checking for the full setup options
object. If the libp2p configuration remains incompatible with the library type,
narrow the cast to only the libp2p property while keeping bootstrapPeers and the
surrounding options strongly typed.

In `@test/util.ts`:
- Around line 46-48: Move the duplicated REPL prompt literal from test/util.ts
and src/index.ts into a shared constant exported by a small module such as
constants.ts. Update both the PROMPT usage in src/index.ts and REPL_PROMPT usage
in test/util.ts to import that shared constant, preserving the existing prompt
value and test behavior.
- Around line 88-104: The runRepl child process lacks timeout-based termination
and timeout-result handling. Update the spawn call and its close handling in
runRepl to apply a bounded timeout with a kill signal, and ensure termination
produces a non-zero result that rejects or otherwise fails the run instead of
leaving the process alive.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad9054ec-b61d-4f39-b100-1b3067cd44ce

📥 Commits

Reviewing files that changed from the base of the PR and between e18575f and 59bbd58.

📒 Files selected for processing (8)
  • CLAUDE.md
  • README.md
  • src/cli.ts
  • src/index.ts
  • src/nodeConnection.ts
  • test/replMenu.test.ts
  • test/setNode.test.ts
  • test/util.ts

Comment thread CLAUDE.md Outdated
Comment thread README.md
Comment on lines 90 to +92
```
export NODE_URL='XXXX'
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the fenced code block.

markdownlint reports MD040 for the block at line 90. Add bash to match the neighboring blocks at lines 96 and 106.

📝 Proposed fix
-```
+```bash
 export NODE_URL='XXXX'
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 90-90: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@README.md` around lines 90 - 92, Update the fenced code block containing
export NODE_URL='XXXX' to specify the bash language, matching the neighboring
fenced blocks and satisfying markdownlint MD040.

Source: Linters/SAST tools

Comment thread src/index.ts
Comment thread test/setNode.test.ts
@alexcos20

Copy link
Copy Markdown
Member Author

/run-security-scan

@alexcos20 alexcos20 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

AI automated code review (Gemini 3).

Overall risk: low

Summary:
The PR introduces a well-designed enhancement for node selection by allowing the CLI to start without a predefined node and enabling on-the-fly node switching. It decouples the libp2p transport lifecycle into a dedicated nodeConnection.ts module, implementing non-blocking background warm-up for interactive mode and safe teardown logic.

Comments:
• [WARNING][bug] The positional argument is marked as required by using <nodeUrl>. This means running setNode -n <url> without a positional argument will cause Commander to throw a "missing required argument" error. Consider making the positional argument optional [nodeUrl] and validating its presence in the action, or simply remove the -n, --node option as the positional argument alone is sufficient.

-    .argument("<nodeUrl>", "HTTP(S) URL, peer id or full multiaddr of the node")
+    .argument("[nodeUrl]", "HTTP(S) URL, peer id or full multiaddr of the node")

• [INFO][style] If a provider or indexer object happens to lack a chainId property, String(p.chainId) will yield the literal string "undefined". It is safer to filter out falsy values before mapping to a string.

-    ...(status.provider || []).map((p) => String(p.chainId)),
-    ...(status.indexer || []).map((i) => String(i.chainId)),
+    ...(status.provider || []).map((p) => p.chainId).filter(Boolean).map(String),
+    ...(status.indexer || []).map((i) => i.chainId).filter(Boolean).map(String),

• [INFO][other] Excellent design pattern here. Catching the rejection of the fire-and-forget promise and storing it in p2pFailure to be surfaced later in ensureP2PReady() elegantly prevents unhandled promise rejection crashes while keeping the startup non-blocking.
• [INFO][other] The deliberate use of process.exit(1) in the catch block to bypass the finally block—thus skipping a potentially hanging orderly shutdown on fatal errors—is a thoughtful detail and perfectly documented. LGTM!

@giurgiur99

Copy link
Copy Markdown
Contributor

/run-security-scan

@giurgiur99

Copy link
Copy Markdown
Contributor

@coderabbitai check for resolved comments

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="oceanprotocol/ocean-cli"
pr_number="166"

echo "== Pull request metadata =="
gh pr view "$pr_number" --repo "$repo" --json number,url,headRefOid --jq '{number,url,headRefOid}'

echo
echo "== Review threads (CodeRabbit-authored comments) =="
gh api graphql \
  -F owner="oceanprotocol" \
  -F name="ocean-cli" \
  -F number="$pr_number" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          isOutdated
          path
          line
          originalLine
          comments(first: 20) {
            nodes {
              author { login }
              body
              createdAt
              url
            }
          }
        }
      }
    }
  }
}' \
  --jq '
    .data.repository.pullRequest.reviewThreads.nodes[]
    | select(any(.comments.nodes[]; .author.login == "coderabbitai"))
    | {
        id,
        isResolved,
        isOutdated,
        path,
        line,
        originalLine,
        comments: [
          .comments.nodes[]
          | select(.author.login == "coderabbitai")
          | {body, url, createdAt}
        ]
      }'

echo
echo "== Current pull request diff (stat and changed files) =="
gh pr diff "$pr_number" --repo "$repo" --stat
gh pr diff "$pr_number" --repo "$repo" --name-only

Length of output: 15173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant file structure =="
ast-grep outline CLAUDE.md 2>/dev/null || true
ast-grep outline README.md 2>/dev/null || true
ast-grep outline src/index.ts --items all 2>/dev/null || true
ast-grep outline test/setNode.test.ts --items all 2>/dev/null || true

echo
echo "== CLAUDE.md: P2P lifecycle documentation =="
sed -n '155,182p' CLAUDE.md

echo
echo "== README.md: NODE_URL fenced block =="
sed -n '84,100p' README.md

echo
echo "== src/index.ts: error and cleanup paths =="
sed -n '235,275p' src/index.ts

echo
echo "== test/setNode.test.ts: affected test environments =="
sed -n '96,140p' test/setNode.test.ts

echo
echo "== Replies in the four CodeRabbit review threads =="
gh api graphql \
  -F owner="oceanprotocol" \
  -F name="ocean-cli" \
  -F number="166" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          path
          comments(first: 30) {
            nodes { author { login } body url createdAt }
          }
        }
      }
    }
  }
}' \
  --jq '
    .data.repository.pullRequest.reviewThreads.nodes[]
    | select(
        .id == "PRRT_kwDOFU37y86W8LDS" or
        .id == "PRRT_kwDOFU37y86W8LDY" or
        .id == "PRRT_kwDOFU37y86W8LDZ" or
        .id == "PRRT_kwDOFU37y86W8LDa"
      )
    | {id, isResolved, path, comments: [.comments.nodes[] | {author: .author.login, body, url, createdAt}]}'

Length of output: 24255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test/setNode.test.ts: complete test definitions =="
sed -n '1,150p' test/setNode.test.ts

echo
echo "== test/util.ts: runRepl environment defaults =="
ast-grep outline test/util.ts --items all 2>/dev/null || true
sed -n '1,125p' test/util.ts

echo
echo "== Resolve verified CodeRabbit review threads =="
for thread_id in \
  "PRRT_kwDOFU37y86W8LDS" \
  "PRRT_kwDOFU37y86W8LDZ" \
  "PRRT_kwDOFU37y86W8LDa"
do
  gh api graphql \
    -F threadId="$thread_id" \
    -f query='
mutation($threadId: ID!) {
  resolveReviewThread(input: {threadId: $threadId}) {
    thread { id isResolved }
  }
}' \
    --jq '.data.resolveReviewThread.thread | {id, isResolved}'
done

echo
echo "== Remaining unresolved CodeRabbit review threads =="
gh api graphql \
  -F owner="oceanprotocol" \
  -F name="ocean-cli" \
  -F number="166" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          path
          line
          comments(first: 20) { nodes { author { login } url } }
        }
      }
    }
  }
}' \
  --jq '
    .data.repository.pullRequest.reviewThreads.nodes[]
    | select(.isResolved == false and any(.comments.nodes[]; .author.login == "coderabbitai"))
    | {id, path, line, urls: [.comments.nodes[] | select(.author.login == "coderabbitai") | .url]}'

Length of output: 12158


@giurgiur99, I resolved three CodeRabbit review threads:

  • CLAUDE.md: The AVOID_LOOP_RUN="true" exception is now documented.
  • src/index.ts: The error path flushes output before process.exit(1).
  • test/setNode.test.ts: The affected assertions were removed. The current tests do not need a live RPC.

One valid thread remains open:

  • README.md Line 92: Add bash to the fenced block that contains export NODE_URL='XXXX' to satisfy markdownlint MD040.

You are interacting with an AI system.

@alexcos20 alexcos20 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

AI automated code review (Gemini 3).

Overall risk: low

Summary:
This is an exceptionally well-engineered PR. The dynamic node selection architecture is clean and robust. You have handled Node.js event loop semantics, libp2p lifecycle edge cases, and asynchronous timeouts masterfully. The intelligent lazy-loading of the P2P transport ensures zero performance penalty for scripts running one-shot commands, which is a great UX improvement. I've left a small warning regarding a Node.js stream quirk, but otherwise, this code is outstanding. LGTM!

Comments:
• [WARNING][bug] While this is a clever approach, writing an empty string ("") to a Node.js stream unfortunately bypasses the internal write queue. Node's Writable.prototype.write explicitly checks for chunk.length === 0 and, if true, invokes the callback immediately on process.nextTick. As a result, this doesn't actually block until pending pipe writes are flushed to the OS.

Reliably draining pipes before process.exit() is notoriously difficult in Node. A simple fallback is to add a small artificial delay if there's pending data:

- 					stream.write("", () => resolve())
+ 					// Writing "" bypasses the queue. Use a small timeout to allow flush.
+ 					setTimeout(resolve, 50)

Since this is mostly an edge case affecting fast-exiting CI scripts, it may not warrant a complex workaround, but it is worth being aware of this Node.js behavior.
• [INFO][architecture] This is an excellent pattern. Catching and swallowing the rejection inside the promise ensures you avoid fatal unhandledRejection process crashes for a background fire-and-forget task, while gracefully preserving the error state (p2pFailure) to surface precisely when the user later explicitly requests a P2P action.
• [INFO][style] The "belt and braces" approach here—racing a bounded timeout but attaching a dangling .then().catch() to gracefully stop the node if it eventually starts after the timeout—is highly robust. Great handling of unpredictable libp2p lifecycle timings.
• [INFO][architecture] Hooking into Commander's preAction at the root level to dynamically enforce node selection is a very elegant architectural choice. It centralizes the guard, catching all edge cases automatically, and keeps all 40+ subcommand implementations perfectly clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ocean-cli Command to select node

2 participants