Skip to content

tests and bun#5

Merged
AustinKelsay merged 7 commits intomasterfrom
staging
Oct 20, 2025
Merged

tests and bun#5
AustinKelsay merged 7 commits intomasterfrom
staging

Conversation

@AustinKelsay
Copy link
Copy Markdown
Member

@AustinKelsay AustinKelsay commented Oct 16, 2025

Summary by CodeRabbit

  • New Features

    • Bun runtime scripts; non-interactive/autopilot test flows (auto-advance/auto-exit); automated password decryption; environment-aware terminal/input behavior; improved echo sending with relay overrides and debug logging; ephemeral test relay.
  • Documentation

    • README split for Node (default) and Bun (optional); added RELEASING.md and release workflow; minor echo doc update.
  • Tests

    • New end-to-end CLI suites, relay/echo unit tests, and test helpers (CLI runner, tmp utilities).
  • Chores

    • Publish config added; dependency bumped; test script now builds first; CI/release workflows added.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 16, 2025

Walkthrough

Splits docs to document Node (default) vs Bun (optional); adds Bun scripts and publishConfig; adds non-interactive/autopilot flags, stdin/Ink polyfills, and auto-exit hooks; refactors echo relay normalization into computeEchoRelays; adds ephemeral Nostr relay, many CLI E2E tests and helpers, and CI/release workflows and release docs.

Changes

Cohort / File(s) Summary
Docs & Release
README.md, RELEASING.md, llm/context/igloo-core-readme.md, .github/workflows/release.yml
Split runtime docs (Node default, Bun optional); add RELEASING.md; note igloo-core echo compatibility; add release workflow verifying tag vs package.json and publishing dist/**.
Package manifest
package.json
Add Bun-targeted scripts and start:bun; reorder test to typecheck→build→tsx --test; bump @frostr/igloo-core to ^0.2.4; add publishConfig.access: "public".
CLI entry
src/cli.tsx
Add global unhandledRejection handler; create PassThrough fake stdin and inkOptions when IGLOO_DISABLE_RAW_MODE set; add stdin ref/unref polyfills; extend showHelpScreen(version, opts?) and pass inkOptions to render.
Component automation / non-interactive
src/components/keyset/KeysetList.tsx, src/components/keyset/KeysetLoad.tsx, src/components/keyset/KeysetSigner.tsx, src/components/share/ShareAdd.tsx
Detect IGLOO_* flags (DISABLE_RAW_MODE, TEST_AUTOPILOT, AUTOPASSWORD, SKIP_ECHO, CLI_NO_INPUT); add autopilot/auto-decrypt flows, input gating, dual-persist share save (outputDir + default), auto-exit scheduling for tests, and test-friendly stdout hooks.
Echo listener & sending
src/components/keyset/useShareEchoListener.ts, src/keyset/echo.ts
Consolidate imports; support IGLOO_TEST_RELAY override and IGLOO_DEBUG_ECHO logging; normalize relay URLs to ws/wss; respect IGLOO_SKIP_ECHO; simplify awaitShareEcho flow and pass computed relay union + logging options.
Relay resolution utility
src/keyset/echoRelays.ts
New module: computeEchoRelays and normalizeRelayUrl to normalize, merge, dedupe, and order explicit/env/group/base relays; exports ComputeEchoRelaysOverrides.
Test relay implementation
src/test-relay/NostrRelay.ts
New ephemeral WebSocket NostrRelay with in-memory cache, per-connection subscriptions, filter matching, REQ/EVENT/CLOSE handling, EOSE streaming, cache purge, and startEphemeralRelay.
Test helpers
tests/helpers/runCli.ts, tests/helpers/tmp.ts
Add runCli to spawn CLI capturing stdout/stderr with timeout and success-pattern early kill; add makeTmp and writePasswordFile helpers for tests.
End-to-end & unit tests
tests/cli.basic.test.ts, tests/cli.keys.convert.test.ts, tests/cli.relays.command.test.ts, tests/cli.share.flow.test.ts, tests/echoRelays.test.ts
New test suites covering CLI screens/version/help, key conversions, relays config mutation, full share flow (import/list/decrypt), and echoRelays unit tests for normalization/dedupe behavior.
CI
.github/workflows/ci.yml
New CI workflow: Node 18, install, typecheck, build, test with IGLOO_DISABLE_RAW_MODE=1.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Test as Test Runner / runCli
    participant CLI as CLI (dist/cli.js)
    participant Env as Environment (IGLOO_*)
    participant App as App Components (ShareAdd / KeysetLoad)
    participant Relay as Ephemeral Nostr Relay

    Test->>CLI: spawn with env (IGLOO_*)
    CLI->>Env: read IGLOO_* flags
    CLI->>App: render with inkOptions (fake stdin when disabled)
    App->>Env: check autopilot / autodecrypt flags
    alt Autopilot / Auto-decrypt enabled
        App->>App: perform auto actions (decrypt, save)
        alt echo enabled
            App->>Relay: send echo event (relayUnion + logging)
            Relay-->>App: ack / echo response
        end
        App->>CLI: render final output, schedule process.exit
    else Interactive
        App->>User: await input (useInput active)
    end
    CLI-->>Test: stdout/stderr captured
Loading
sequenceDiagram
    autonumber
    participant Client as Nostr Client
    participant Relay as NostrRelay
    participant Cache as Event Cache
    participant Subs as Subscriptions

    Client->>Relay: open WebSocket
    Client->>Relay: REQ (filters)
    Relay->>Subs: register subscription
    Relay->>Cache: query matching events
    Cache-->>Relay: return matches
    Relay-->>Client: stream events + EOSE

    Client->>Relay: EVENT (publish)
    Relay->>Cache: store event
    Relay->>Subs: fan-out to matching subscribers
    Relay-->>Client: OK
    Note over Relay,Cache: periodic cache purge removes old events
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble keys in evening light,
Bun and Node both snug and bright,
Autopilot hums as echoes play,
Tiny relays sing tests away,
🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "tests and bun" refers to real, substantial aspects of the changeset. The PR clearly adds extensive test infrastructure (multiple test files, test helpers, and CI workflows) and introduces Bun support (Bun-specific scripts in package.json, documentation updates, and environment handling throughout components). However, the title doesn't capture the full scope of the changeset, which also includes significant additions like a WebSocket-based Nostr relay implementation, echo relay resolution logic, component updates for test automation, and release workflows. The title is partially related to the changeset as it addresses real parts of the change, though it is somewhat broad and doesn't fully represent the comprehensive nature of the PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch staging

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71ab4fe and d015861.

📒 Files selected for processing (1)
  • package.json (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/components/keyset/KeysetSigner.tsx (1)

624-643: Consider extracting environment flag parsing to a shared utility.

The IIFE pattern for parsing environment flags is verbose and duplicated across multiple files (see similar code in KeysetList.tsx lines 22-26). This creates maintenance burden if flag parsing logic needs to change.

Consider extracting to a shared utility:

In a new file src/utils/env.ts:

export function isNonInteractive(): boolean {
  const rawMode = (process.env.IGLOO_DISABLE_RAW_MODE ?? '').toLowerCase();
  const noInput = (process.env.IGLOO_CLI_NO_INPUT ?? '').toLowerCase();
  return rawMode === '1' || rawMode === 'true' || noInput === '1' || noInput === 'true';
}

Then simplify both files:

-  const inputDisabled = (() => {
-    const a = (process.env.IGLOO_DISABLE_RAW_MODE ?? '').toLowerCase();
-    const b = (process.env.IGLOO_CLI_NO_INPUT ?? '').toLowerCase();
-    return a === '1' || a === 'true' || b === '1' || b === 'true';
-  })();
+  const inputDisabled = isNonInteractive();
src/components/keyset/KeysetList.tsx (2)

20-26: Code duplication with KeysetSigner.tsx.

This IIFE pattern for computing nonInteractive is identical to the inputDisabled logic in KeysetSigner.tsx (lines 624-628). See the refactoring suggestion in KeysetSigner.tsx for extracting this to a shared utility.


47-53: Clean up effect dependencies and error handling.

  1. The effect dependencies include state.shares.length and state.error which aren't used in the effect body. This causes unnecessary re-runs when shares or errors change.
  2. The try/catch around process.exit(0) is unnecessary since process.exit() doesn't throw exceptions.

Apply this diff:

  useEffect(() => {
    if (!nonInteractive || state.loading) return;
    const t = setTimeout(() => {
-      try { process.exit(0); } catch {}
+      process.exit(0);
    }, 10);
    return () => clearTimeout(t);
-  }, [nonInteractive, state.loading, state.shares.length, state.error]);
+  }, [nonInteractive, state.loading]);
src/test-relay/NostrRelay.ts (1)

118-134: Stop should clear the purge interval

The cache purge timer created on Line 118 isn’t cleared. Even after stop() runs, that interval keeps firing, holding the relay instance in memory across tests and wasting CPU. Store the handle and clear it during shutdown.

 export class NostrRelay extends EventEmitter {
   private readonly subs: Map<string, Subscription> = new Map();
   private readonly cache: NostrEvent[] = [];
   private readonly config: Required<NostrRelayConfig>;
   private wss?: WebSocketServer;
   private _url?: string;
+  private purgeTimer: NodeJS.Timeout | null = null;
@@
-        setInterval(() => {
-          this.cache.length = 0;
-        }, this.config.purgeIntervalSec * 1000).unref?.();
+        this.purgeTimer = setInterval(() => {
+          this.cache.length = 0;
+        }, this.config.purgeIntervalSec * 1000);
+        this.purgeTimer.unref?.();
@@
     this.subs.clear();
     this.cache.length = 0;
+    if (this.purgeTimer) {
+      clearInterval(this.purgeTimer);
+      this.purgeTimer = null;
+    }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2e3d5b and 49e7e17.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • README.md (2 hunks)
  • package.json (1 hunks)
  • src/cli.tsx (5 hunks)
  • src/components/keyset/KeysetList.tsx (2 hunks)
  • src/components/keyset/KeysetLoad.tsx (3 hunks)
  • src/components/keyset/KeysetSigner.tsx (2 hunks)
  • src/components/keyset/useShareEchoListener.ts (1 hunks)
  • src/components/share/ShareAdd.tsx (5 hunks)
  • src/keyset/echo.ts (1 hunks)
  • src/test-relay/NostrRelay.ts (1 hunks)
  • tests/cli.basic.test.ts (1 hunks)
  • tests/cli.keys.convert.test.ts (1 hunks)
  • tests/cli.relays.command.test.ts (1 hunks)
  • tests/cli.share.flow.test.ts (1 hunks)
  • tests/helpers/runCli.ts (1 hunks)
  • tests/helpers/tmp.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All import specifiers must include .js extensions (TypeScript ESM requirement) even though source files are .ts/.tsx

Files:

  • src/test-relay/NostrRelay.ts
  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • src/components/keyset/KeysetList.tsx
  • src/components/keyset/KeysetSigner.tsx
  • src/cli.tsx
  • src/keyset/echo.ts
  • src/components/share/ShareAdd.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript with ESM, 2-space indentation, single quotes, and trailing commas
Order imports from shallow-to-deep
Use camelCase for utility function and variable names
Use SCREAMING_SNAKE_CASE for constants

Files:

  • src/test-relay/NostrRelay.ts
  • tests/helpers/tmp.ts
  • src/components/keyset/KeysetLoad.tsx
  • tests/cli.relays.command.test.ts
  • src/components/keyset/useShareEchoListener.ts
  • tests/cli.keys.convert.test.ts
  • src/components/keyset/KeysetList.tsx
  • tests/cli.basic.test.ts
  • tests/helpers/runCli.ts
  • src/components/keyset/KeysetSigner.tsx
  • src/cli.tsx
  • tests/cli.share.flow.test.ts
  • src/keyset/echo.ts
  • src/components/share/ShareAdd.tsx
src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use Ink components (, , etc.) for terminal UI and do not use HTML elements

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/KeysetList.tsx
  • src/components/keyset/KeysetSigner.tsx
  • src/cli.tsx
  • src/components/share/ShareAdd.tsx
src/components/**

📄 CodeRabbit inference engine (CLAUDE.md)

All UI components should live under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • src/components/keyset/KeysetList.tsx
  • src/components/keyset/KeysetSigner.tsx
  • src/components/share/ShareAdd.tsx
src/components/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

When adding a new command, implement it as a component under src/components/

Store reusable UI components under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/KeysetList.tsx
  • src/components/keyset/KeysetSigner.tsx
  • src/components/share/ShareAdd.tsx
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React components using PascalCase

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • src/components/keyset/KeysetList.tsx
  • src/components/keyset/KeysetSigner.tsx
  • src/components/share/ShareAdd.tsx
**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.ts: Allow colocated test files named feature.test.ts beside implementations
Test files should exercise both happy paths and failure modes

Files:

  • tests/cli.relays.command.test.ts
  • tests/cli.keys.convert.test.ts
  • tests/cli.basic.test.ts
  • tests/cli.share.flow.test.ts
src/cli.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

src/cli.tsx: Argument parsing is handled manually in cli.tsx via parseArgv() and must support --flag value, --flag=value, and -f value forms
When adding command flags, extend parseArgv() in cli.tsx for any custom parsing logic

src/cli.tsx: Keep the CLI entrypoint in src/cli.tsx and use it to bootstrap Ink rendering
Keep CLI flags lower-case (e.g., --verbose)

Files:

  • src/cli.tsx
src/keyset/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/keyset/**/*.ts: Keep key exchange and credential helpers under src/keyset/
Document non-obvious flows with concise comments, especially around key lifecycle management

Files:

  • src/keyset/echo.ts
🧠 Learnings (2)
📚 Learning: 2025-10-03T23:45:30.161Z
Learnt from: CR
PR: FROSTR-ORG/igloo-cli#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-03T23:45:30.161Z
Learning: Applies to tsup.config.ts : Build output should be dist/cli.js and include a shebang (#!/usr/bin/env node)

Applied to files:

  • README.md
📚 Learning: 2025-10-08T16:24:14.475Z
Learnt from: CR
PR: FROSTR-ORG/igloo-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-08T16:24:14.475Z
Learning: Applies to src/cli.tsx : Keep the CLI entrypoint in src/cli.tsx and use it to bootstrap Ink rendering

Applied to files:

  • src/cli.tsx
🧬 Code graph analysis (7)
src/components/keyset/KeysetLoad.tsx (2)
src/components/keyset/useShareEchoListener.ts (1)
  • useShareEchoListener (55-255)
src/keyset/crypto.ts (1)
  • decryptShareCredential (232-284)
tests/cli.relays.command.test.ts (2)
tests/helpers/tmp.ts (1)
  • makeTmp (5-8)
tests/helpers/runCli.ts (1)
  • runCli (13-69)
tests/cli.keys.convert.test.ts (2)
tests/helpers/tmp.ts (1)
  • makeTmp (5-8)
tests/helpers/runCli.ts (1)
  • runCli (13-69)
tests/cli.basic.test.ts (2)
tests/helpers/tmp.ts (1)
  • makeTmp (5-8)
tests/helpers/runCli.ts (1)
  • runCli (13-69)
src/cli.tsx (1)
src/components/Help.tsx (1)
  • Help (8-50)
tests/cli.share.flow.test.ts (3)
tests/helpers/tmp.ts (2)
  • makeTmp (5-8)
  • writePasswordFile (10-14)
src/test-relay/NostrRelay.ts (2)
  • startEphemeralRelay (207-211)
  • url (95-97)
tests/helpers/runCli.ts (1)
  • runCli (13-69)
src/components/share/ShareAdd.tsx (5)
src/keyset/crypto.ts (7)
  • randomSaltHex (130-132)
  • deriveSecret (79-90)
  • SHARE_FILE_PBKDF2_ITERATIONS (8-8)
  • SHARE_FILE_PASSWORD_ENCODING (11-11)
  • SHARE_FILE_SALT_PBKDF2_EXPANDED_BYTES (13-13)
  • encryptPayload (92-111)
  • SHARE_FILE_VERSION (7-7)
src/keyset/types.ts (1)
  • ShareFileRecord (16-28)
src/keyset/policy.ts (1)
  • createDefaultPolicy (14-20)
src/keyset/storage.ts (1)
  • saveShareRecord (48-63)
src/keyset/echo.ts (1)
  • sendShareEcho (15-39)
🪛 ast-grep (0.39.6)
tests/cli.keys.convert.test.ts

[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(nsec: ${expectedNsec})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 31-31: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(Public hex: ${hexPublicKey})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 32-32: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(npub: ${npub})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)


[warning] 52-52: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(npub: ${npub})
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

tests/cli.share.flow.test.ts

[warning] 71-71: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${name} share 1)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 Biome (2.1.2)
tests/cli.basic.test.ts

[error] 7-7: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 7-7: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)

🪛 LanguageTool
README.md

[grammar] ~19-~19: There might be a mistake here.
Context: ...js 18+ (primary target, default scripts) - Bun 1.0+ (optional; faster dev/build/tes...

(QB_NEW_EN)


[grammar] ~20-~20: There might be a mistake here.
Context: ...n 1.0+ (optional; faster dev/build/test) - npm 10+ (bundled with Node 20+) ## Firs...

(QB_NEW_EN)


[grammar] ~164-~164: There might be a mistake here.
Context: ...pecheck then run tests via tsx --test. - Bun (optional, faster) - `bun run dev:...

(QB_NEW_EN)

🔇 Additional comments (7)
src/components/keyset/useShareEchoListener.ts (1)

80-98: LGTM! Test relay override is cleanly implemented.

The IGLOO_TEST_RELAY environment variable override provides a clean way to force relay selection in tests without modifying the underlying relay resolution logic. The short-circuit pattern is appropriate and the implementation is straightforward.

tests/helpers/tmp.ts (1)

1-15: LGTM! Clean and focused test utilities.

The temporary directory and password file helpers are well-implemented with appropriate defaults. The trailing newline in the password file is good practice for file-based password inputs.

package.json (1)

7-16: Verify intended scope of test:bun script.

The test:bun script only runs tests/relays.test.ts while the Node test script runs all tests via the glob pattern tests/**/*.test.ts. This inconsistency suggests either:

  1. Bun test coverage is intentionally limited to relay tests (work in progress), or
  2. This is an oversight and should match the Node test script.

Please clarify whether test:bun should run all tests like the Node version:

-    "test:bun": "bunx tsc --noEmit && bunx tsx --test tests/relays.test.ts",
+    "test:bun": "bunx tsc --noEmit && bunx tsx --test tests/**/*.test.ts",
src/keyset/echo.ts (1)

20-32: LGTM! Test-mode environment flags are well-integrated.

The IGLOO_SKIP_ECHO flag provides a clean way to disable echo in test scenarios, and the IGLOO_TEST_RELAY override ensures deterministic relay behavior. The implementation is consistent with similar patterns in useShareEchoListener.ts.

README.md (2)

19-42: LGTM! Clear documentation of dual runtime support.

The documentation effectively communicates that Node.js is the primary runtime while positioning Bun as an optional, faster alternative. The parallel "First run" sections make it easy for users to choose their preferred runtime.


160-171: LGTM! Development workflow is well-documented.

The side-by-side presentation of Node and Bun scripts makes it easy to see the parallel workflows and choose the appropriate runtime for development tasks.

tests/cli.relays.command.test.ts (1)

1-45: LGTM! Well-structured relay configuration tests.

The test suite provides good coverage of relay management operations with proper isolation via temporary directories. The tests verify both exit codes and output content, and the mutation test exercises the full configuration lifecycle.

  - Delegate send to sendEcho; listen via awaitShareEcho
  - Remove legacy hacks, notify helper, and rejection polyfill
  - Normalize relay handling; update tests and docs
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (7)
tests/cli.share.flow.test.ts (1)

1-9: Reorder imports per shallow-to-deep convention.

The external package import on line 8 should come before the local imports (lines 5-7), as node_modules dependencies are shallower than project-local files.

Apply this diff:

 import {test, beforeEach, afterEach} from 'node:test';
 import assert from 'node:assert/strict';
 import fs from 'node:fs/promises';
 import path from 'node:path';
+import {generateKeysetWithSecret} from '@frostr/igloo-core';
 import {runCli} from './helpers/runCli.js';
 import {makeTmp, writePasswordFile} from './helpers/tmp.js';
 import {startEphemeralRelay} from '../src/test-relay/NostrRelay.js';
-import {generateKeysetWithSecret} from '@frostr/igloo-core';
 import {buildShareId} from '../src/keyset/naming.js';

Based on coding guidelines

src/components/keyset/KeysetLoad.tsx (1)

65-68: Guard process.env access for cross-runtime safety

Use a typeof process check to avoid ReferenceError outside Node (e.g., bundlers/Bun settings).

-  const skipEcho = (() => {
-    const a = (process.env.IGLOO_SKIP_ECHO ?? '').toLowerCase();
-    return a === '1' || a === 'true';
-  })();
+  const skipEcho = (() => {
+    const raw = typeof process !== 'undefined' ? process.env.IGLOO_SKIP_ECHO ?? '' : '';
+    const a = raw.toLowerCase();
+    return a === '1' || a === 'true';
+  })();
src/components/keyset/useShareEchoListener.ts (3)

69-71: Prefer cross‑env timer type

NodeJS.Timeout can clash in mixed DOM/Node typings. Use ReturnType<typeof setTimeout> for Bun/Node compatibility.

-  const retryTimeoutRef = useRef<NodeJS.Timeout | null>(null);
-  const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
-  const fallbackTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+  const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const warningTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const fallbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

98-115: Remove dead fallback fields left from previous logic

fallbackTimeoutRef and fallbackTriggeredRef are no longer used beyond clearing; simplify state/cleanup.

-  const fallbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
-  const fallbackTriggeredRef = useRef(false);
...
-    if (fallbackTimeoutRef.current) {
-      clearTimeout(fallbackTimeoutRef.current);
-      fallbackTimeoutRef.current = null;
-    }
+  // (removed fallback timeout state)

Note: Also remove the variable declarations and any remaining references in clearPending(). Adjust imports/types accordingly.

Also applies to: 189-197


74-96: Deduplicate normalize helper

normalize is defined twice in the memo. Extract once above the hook for clarity and reuse.

const normalizeRelayUrl = (u: string) =>
  /^wss?:\/\//i.test(u) ? u.replace(/^http/i, 'ws') : `wss://${u}`;

// then use: map(normalizeRelayUrl)
src/components/share/ShareAdd.tsx (2)

376-391: Non‑interactive auto‑advance: LGTM; consider DRY env parsing

The logic is correct and aligns with the non-interactive requirement. Optionally centralize truthy env parsing to avoid repetition.

Helper (place near top-level):

const envTrue = (v?: string) => {
  const s = (v ?? '').toLowerCase();
  return s === '1' || s === 'true';
};

Then:

const nonInteractive = envTrue(process.env.IGLOO_DISABLE_RAW_MODE) || envTrue(process.env.IGLOO_TEST_AUTOPILOT);

476-498: Background echo with skip flag: LGTM; optional small cleanup

The non-blocking echo with mounted checks is solid. Optionally reuse a shared envTrue helper for IGLOO_SKIP_ECHO to keep parsing consistent across the file.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49e7e17 and e30ce80.

📒 Files selected for processing (10)
  • .github/workflows/release.yml (1 hunks)
  • RELEASING.md (1 hunks)
  • llm/context/igloo-core-readme.md (2 hunks)
  • package.json (3 hunks)
  • src/components/keyset/KeysetLoad.tsx (3 hunks)
  • src/components/keyset/useShareEchoListener.ts (3 hunks)
  • src/components/share/ShareAdd.tsx (5 hunks)
  • src/keyset/echo.ts (2 hunks)
  • tests/cli.share.flow.test.ts (1 hunks)
  • tests/helpers/runCli.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • llm/context/igloo-core-readme.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/keyset/echo.ts
  • tests/helpers/runCli.ts
  • package.json
🧰 Additional context used
📓 Path-based instructions (7)
src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use Ink components (, , etc.) for terminal UI and do not use HTML elements

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All import specifiers must include .js extensions (TypeScript ESM requirement) even though source files are .ts/.tsx

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
  • src/components/keyset/useShareEchoListener.ts
src/components/**

📄 CodeRabbit inference engine (CLAUDE.md)

All UI components should live under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
  • src/components/keyset/useShareEchoListener.ts
src/components/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

When adding a new command, implement it as a component under src/components/

Store reusable UI components under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript with ESM, 2-space indentation, single quotes, and trailing commas
Order imports from shallow-to-deep
Use camelCase for utility function and variable names
Use SCREAMING_SNAKE_CASE for constants

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
  • src/components/keyset/useShareEchoListener.ts
  • tests/cli.share.flow.test.ts
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React components using PascalCase

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
  • src/components/keyset/useShareEchoListener.ts
**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.ts: Allow colocated test files named feature.test.ts beside implementations
Test files should exercise both happy paths and failure modes

Files:

  • tests/cli.share.flow.test.ts
🧬 Code graph analysis (3)
src/components/keyset/KeysetLoad.tsx (2)
src/components/keyset/useShareEchoListener.ts (1)
  • useShareEchoListener (49-224)
src/keyset/crypto.ts (1)
  • decryptShareCredential (232-284)
src/components/share/ShareAdd.tsx (5)
src/keyset/crypto.ts (7)
  • randomSaltHex (130-132)
  • deriveSecret (79-90)
  • SHARE_FILE_PBKDF2_ITERATIONS (8-8)
  • SHARE_FILE_PASSWORD_ENCODING (11-11)
  • SHARE_FILE_SALT_PBKDF2_EXPANDED_BYTES (13-13)
  • encryptPayload (92-111)
  • SHARE_FILE_VERSION (7-7)
src/keyset/types.ts (1)
  • ShareFileRecord (16-28)
src/keyset/policy.ts (1)
  • createDefaultPolicy (14-20)
src/keyset/storage.ts (1)
  • saveShareRecord (48-63)
src/keyset/echo.ts (1)
  • sendShareEcho (18-39)
tests/cli.share.flow.test.ts (3)
tests/helpers/tmp.ts (2)
  • makeTmp (5-8)
  • writePasswordFile (10-14)
src/test-relay/NostrRelay.ts (2)
  • startEphemeralRelay (207-211)
  • url (95-97)
tests/helpers/runCli.ts (1)
  • runCli (21-82)
🪛 ast-grep (0.39.6)
tests/cli.share.flow.test.ts

[warning] 71-71: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(${name} share 1)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (16)
tests/cli.share.flow.test.ts (3)

16-32: LGTM! Excellent test isolation.

The setup and teardown hooks properly isolate each test with temporary directories, dedicated password files, and environment cleanup. The defensive relay cleanup check on line 28 prevents errors when tests skip relay initialization.


34-80: LGTM! Comprehensive end-to-end test coverage.

The test effectively validates the complete share workflow with deterministic test data. The use of IGLOO_TEST_AUTOPILOT and IGLOO_TEST_RELAY enables reliable CI execution. The assertions on lines 60, 66, 71-72, and 80 properly verify each step of the flow.

Note: The static analysis warning on line 71 about constructing RegExp from a variable is a false positive—name is a test constant, not user input, so there's no ReDoS risk.


83-83: Nice fix! The assertion now validates relay activity.

The change from >= 0 to > 0 correctly verifies that the relay observed at least one event, preventing false passes if echo functionality breaks.

.github/workflows/release.yml (4)

28-36: Tag verification logic is sound.

The workflow correctly extracts the git tag, parses package.json version, and ensures they match the v<version> format before proceeding. This prevents version mismatches between tags and releases.


55-62: Release creation step looks good.

The workflow uses softprops/action-gh-release@v2 with generate_release_notes: true to automatically generate release notes and upload dist/** assets. The GitHub token is correctly passed via secrets.


46-53: All npm scripts are properly defined in package.json.

Verification confirms that typecheck, test, and build scripts all exist in package.json. The workflow can execute these commands without errors.


22-44: Remove cache: 'npm' or commit package-lock.json to fix workflow failure.

The setup-node action with cache: 'npm' searches for package-lock.json in the repository root for caching, but the repository has no package-lock.json. This causes the cache property to fail during workflow execution.

The conditional install logic (lines 38-44) cannot compensate because the workflow fails at the setup-node step before reaching the install step.

Options:

  • Remove cache: 'npm' at line 26 if intentionally avoiding a lockfile
  • Commit package-lock.json if the project should use one
⛔ Skipped due to learnings
Learnt from: CR
PR: FROSTR-ORG/igloo-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-08T16:24:14.475Z
Learning: Install dependencies under Node.js 18 using nvm use 18
RELEASING.md (3)

1-31: Documentation accurately reflects the workflow and release process.

The RELEASING.md file provides clear, step-by-step instructions that align well with the GitHub Actions workflow in .github/workflows/release.yml. The documentation covers GPG setup, tag creation, and the automated build/test/release pipeline triggered by pushing the tag.


33-53: Local npm publishing instructions are clear and complete.

The documentation correctly describes the local publishing workflow, including optional review and signing steps. The reference to publishConfig.access = public indicates the npm package is intended to be public, which is consistent with the repository structure.


55-59: Signing and provenance notes are helpful and realistic.

The documentation appropriately cautions against importing private keys into CI and explains the trade-off of local vs. CI publishing for provenance attestations. This guidance helps maintainers make informed decisions about their release security posture.

src/components/keyset/KeysetLoad.tsx (1)

70-72: Echo skip wiring looks correct

Passing undefined to the hook when skipping echo cleanly disables listening without branching elsewhere.

src/components/keyset/useShareEchoListener.ts (4)

75-80: Good: IGLOO_TEST_RELAY override with URL normalization

The override + normalization logic is solid and test-friendly. Nice.


86-96: Union normalized group relays with defaults

Merging normalized group-derived relays with DEFAULT_ECHO_RELAYS and falling back via resolveRelaysWithFallbackSync is robust.


152-165: Result handling after awaitShareEcho looks correct

Short-circuit on controller.cancelled, set success, or keep listening with a clear message. Backoff below maintains responsiveness.


150-157: Confirm awaitShareEcho options shape

Ensure { relays, timeout: timeoutMs, eventConfig: { enableLogging: false } } matches the current @frostr/igloo-core API in this branch. If the shape changed, adjust accordingly.

src/components/share/ShareAdd.tsx (1)

461-465: Dual persistence path: LGTM

Persisting to both the requested outputDir and the default share directory improves discoverability for other commands. The try/catch keeps UX smooth if the secondary write fails.

  automated password handling, echo relay computation, and autopilot flows.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
src/cli.tsx (1)

130-146: Consider stronger typing for inkOptions and fakeIn.

The logic correctly constructs a fake stdin and disables raw mode for non-interactive environments. However, the extensive use of any reduces type safety.

Apply this diff to add more specific types:

-let inkOptions: any | undefined;
+let inkOptions: {isRawModeSupported: boolean; stdin: NodeJS.ReadStream & {isTTY: boolean}} | undefined;
 if (process.env.IGLOO_DISABLE_RAW_MODE === '1' || process.env.IGLOO_DISABLE_RAW_MODE === 'true') {
-  const fakeIn: any = new PassThrough();
+  const fakeIn = new PassThrough() as any as NodeJS.ReadStream & {isTTY: boolean};
   fakeIn.isTTY = true;

Note: The as any as cast is necessary because PassThrough doesn't natively implement NodeJS.ReadStream, but this is more explicit about the intent.

src/test-relay/NostrRelay.ts (1)

11-15: Consider stronger typing for Subscription fields.

The client and filters fields are typed as any, which reduces type safety. Consider typing client as WebSocket from the ws package and filters as NostrFilter[] (defining a NostrFilter type with the expected filter properties).

+import {WebSocket} from 'ws';
+
+type NostrFilter = {
+  ids?: string[];
+  authors?: string[];
+  kinds?: number[];
+  since?: number;
+  until?: number;
+  limit?: number;
+  [key: `#${string}`]: string[] | undefined;
+};
+
 type Subscription = {
-  client: any;
+  client: WebSocket;
   subId: string;
-  filters: any[];
+  filters: NostrFilter[];
 };
src/components/share/ShareAdd.tsx (1)

265-271: Avoid redundant double-write when no outputDir is provided

The secondary save to the default share directory runs even when outputDir is undefined, causing a needless overwrite of the same file. Only perform the second save when an explicit outputDir was used.

Apply in both places:

@@
-            const filepath = await saveShareRecord(record, {directory: outputDir});
-            // Always persist into the default appdata share directory as well,
-            // so other commands (e.g. `share list`) can discover the record.
-            try {
-              await saveShareRecord(record);
-            } catch {}
+            const filepath = await saveShareRecord(record, {directory: outputDir});
+            // Mirror into the default directory only when an explicit outputDir was used.
+            if (outputDir) {
+              try {
+                await saveShareRecord(record);
+              } catch {}
+            }
@@
-      const filepath = await saveShareRecord(record, {directory: outputDir});
-      try {
-        // Also persist to the default share directory for discovery.
-        await saveShareRecord(record);
-      } catch {}
+      const filepath = await saveShareRecord(record, {directory: outputDir});
+      if (outputDir) {
+        try {
+          // Also persist to the default share directory for discovery.
+          await saveShareRecord(record);
+        } catch {}
+      }

Also applies to: 513-517

src/components/keyset/KeysetLoad.tsx (1)

115-129: Align auto-exit env parsing with other components and make delay configurable

Accept '1' or 'true' for IGLOO_DISABLE_RAW_MODE/IGLOO_TEST_AUTOPILOT and use IGLOO_TEST_AUTOPILOT_EXIT_MS like ShareAdd for consistency.

-  useEffect(() => {
-    if (!result) return;
-    const shouldAutoExit =
-      process.env.IGLOO_DISABLE_RAW_MODE === '1' || process.env.IGLOO_TEST_AUTOPILOT === '1';
-    if (!shouldAutoExit) return;
-
-    if (typeof window !== 'undefined') return; // ensure Node/CLI context
-    if (typeof process === 'undefined' || typeof process.exit !== 'function') return;
-
-    const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
-      try {
-        process.exit(0);
-      } catch {}
-    }, 20);
-    return () => clearTimeout(timer);
-  }, [result]);
+  useEffect(() => {
+    if (!result) return;
+    const a = (process.env.IGLOO_DISABLE_RAW_MODE ?? '').toLowerCase();
+    const b = (process.env.IGLOO_TEST_AUTOPILOT ?? '').toLowerCase();
+    const shouldAutoExit = a === '1' || a === 'true' || b === '1' || b === 'true';
+    if (!shouldAutoExit) return;
+
+    if (typeof window !== 'undefined') return; // ensure Node/CLI context
+    if (typeof process === 'undefined' || typeof process.exit !== 'function') return;
+
+    const raw = process.env.IGLOO_TEST_AUTOPILOT_EXIT_MS;
+    const parsed = raw ? Number(raw) : NaN;
+    const delay = Number.isFinite(parsed) && parsed >= 0 ? parsed : 200;
+
+    const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
+      try { process.exit(0); } catch {}
+    }, delay);
+    return () => clearTimeout(timer);
+  }, [result]);
src/components/keyset/useShareEchoListener.ts (2)

2-3: Reuse computeEchoRelays; drop local normalization/DEFAULT handling

Avoid duplicating URL normalization and defaults here; import and use computeEchoRelays.

-import {awaitShareEcho, decodeGroup, DEFAULT_ECHO_RELAYS} from '@frostr/igloo-core';
-import {resolveRelaysWithFallbackSync} from '../../keyset/relays.js';
+import {awaitShareEcho} from '@frostr/igloo-core';
+import {computeEchoRelays} from '../../keyset/echoRelays.js';
@@
-  const relays = useMemo(() => {
-    const envOverride = typeof process !== 'undefined' ? process.env.IGLOO_TEST_RELAY : undefined;
-    if (envOverride && envOverride.length > 0) {
-      const normalize = (u: string) => (/^wss?:\/\//i.test(u) ? u.replace(/^http/i, 'ws') : `wss://${u}`);
-      return [normalize(envOverride)];
-    }
-    if (!groupCredential) {
-      return undefined;
-    }
-    try {
-      const decoded = decodeGroup(groupCredential);
-      const fromGroup = extractRelays(decoded);
-      const normalize = (u: string) => (/^wss?:\/\//i.test(u) ? u.replace(/^http/i, 'ws') : `wss://${u}`);
-      const groupSet = new Set((fromGroup ?? []).map(normalize));
-      const defaults = DEFAULT_ECHO_RELAYS.map(normalize);
-      const union = [...new Set([...defaults, ...groupSet])];
-      if (union.length > 0) return union;
-      return resolveRelaysWithFallbackSync(undefined, DEFAULT_ECHO_RELAYS);
-    } catch {
-      // ignore decode failures; we'll fall back to default relays
-    }
-    return resolveRelaysWithFallbackSync(undefined, DEFAULT_ECHO_RELAYS);
-  }, [groupCredential]);
+  const relays = useMemo(() => {
+    const env = typeof process !== 'undefined' ? (process.env.IGLOO_TEST_RELAY ?? '').trim() : '';
+    const envRelay = env.length > 0 ? env : undefined;
+    return computeEchoRelays(groupCredential ?? undefined, undefined, envRelay);
+  }, [groupCredential]);
@@
-        const result = await awaitShareEcho(
-          groupCredential,
-          shareCredential,
-          { relays, timeout: timeoutMs, eventConfig: { enableLogging: debugEnabled, customLogger: debugLogger } }
-        );
+        const result = await awaitShareEcho(groupCredential, shareCredential, {
+          relays,
+          timeout: timeoutMs,
+          eventConfig: {enableLogging: debugEnabled, customLogger: debugLogger},
+        });

Also applies to: 74-96, 161-166


69-74: Remove unused fallback refs and cleanup code

fallbackTimeoutRef and fallbackTriggeredRef are never set; they add noise. Remove their declarations and clear calls.

-  const fallbackTimeoutRef = useRef<NodeJS.Timeout | null>(null);
   const activeShareRef = useRef<string | null>(null);
-  const fallbackTriggeredRef = useRef(false);
@@
-    if (fallbackTimeoutRef.current) {
-      clearTimeout(fallbackTimeoutRef.current);
-      fallbackTimeoutRef.current = null;
-    }
@@
-    fallbackTriggeredRef.current = false;
@@
-        if (fallbackTimeoutRef.current) {
-          clearTimeout(fallbackTimeoutRef.current);
-          fallbackTimeoutRef.current = null;
-        }

Also applies to: 111-115, 130-131, 198-206

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e30ce80 and 7c93041.

📒 Files selected for processing (8)
  • src/cli.tsx (5 hunks)
  • src/components/keyset/KeysetLoad.tsx (4 hunks)
  • src/components/keyset/useShareEchoListener.ts (3 hunks)
  • src/components/share/ShareAdd.tsx (6 hunks)
  • src/keyset/echo.ts (2 hunks)
  • src/keyset/echoRelays.ts (1 hunks)
  • src/test-relay/NostrRelay.ts (1 hunks)
  • tests/echoRelays.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/keyset/echo.ts
🧰 Additional context used
📓 Path-based instructions (9)
src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use Ink components (, , etc.) for terminal UI and do not use HTML elements

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
  • src/cli.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All import specifiers must include .js extensions (TypeScript ESM requirement) even though source files are .ts/.tsx

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • src/components/share/ShareAdd.tsx
  • src/cli.tsx
  • src/keyset/echoRelays.ts
  • src/test-relay/NostrRelay.ts
src/components/**

📄 CodeRabbit inference engine (CLAUDE.md)

All UI components should live under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • src/components/share/ShareAdd.tsx
src/components/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

When adding a new command, implement it as a component under src/components/

Store reusable UI components under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/share/ShareAdd.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript with ESM, 2-space indentation, single quotes, and trailing commas
Order imports from shallow-to-deep
Use camelCase for utility function and variable names
Use SCREAMING_SNAKE_CASE for constants

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • tests/echoRelays.test.ts
  • src/components/share/ShareAdd.tsx
  • src/cli.tsx
  • src/keyset/echoRelays.ts
  • src/test-relay/NostrRelay.ts
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React components using PascalCase

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/components/keyset/useShareEchoListener.ts
  • src/components/share/ShareAdd.tsx
**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.ts: Allow colocated test files named feature.test.ts beside implementations
Test files should exercise both happy paths and failure modes

Files:

  • tests/echoRelays.test.ts
src/cli.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

src/cli.tsx: Argument parsing is handled manually in cli.tsx via parseArgv() and must support --flag value, --flag=value, and -f value forms
When adding command flags, extend parseArgv() in cli.tsx for any custom parsing logic

src/cli.tsx: Keep the CLI entrypoint in src/cli.tsx and use it to bootstrap Ink rendering
Keep CLI flags lower-case (e.g., --verbose)

Files:

  • src/cli.tsx
src/keyset/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/keyset/**/*.ts: Keep key exchange and credential helpers under src/keyset/
Document non-obvious flows with concise comments, especially around key lifecycle management

Files:

  • src/keyset/echoRelays.ts
🧠 Learnings (1)
📚 Learning: 2025-10-08T16:24:14.475Z
Learnt from: CR
PR: FROSTR-ORG/igloo-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-08T16:24:14.475Z
Learning: Applies to src/cli.tsx : Keep the CLI entrypoint in src/cli.tsx and use it to bootstrap Ink rendering

Applied to files:

  • src/cli.tsx
🧬 Code graph analysis (6)
src/components/keyset/KeysetLoad.tsx (2)
src/keyset/crypto.ts (1)
  • decryptShareCredential (232-284)
src/components/keyset/useShareEchoListener.ts (1)
  • useShareEchoListener (49-233)
src/components/keyset/useShareEchoListener.ts (1)
src/keyset/echoRelays.ts (1)
  • normalize (67-67)
tests/echoRelays.test.ts (1)
src/keyset/echoRelays.ts (1)
  • computeEchoRelays (43-65)
src/components/share/ShareAdd.tsx (5)
src/keyset/crypto.ts (7)
  • randomSaltHex (130-132)
  • deriveSecret (79-90)
  • SHARE_FILE_PBKDF2_ITERATIONS (8-8)
  • SHARE_FILE_PASSWORD_ENCODING (11-11)
  • SHARE_FILE_SALT_PBKDF2_EXPANDED_BYTES (13-13)
  • encryptPayload (92-111)
  • SHARE_FILE_VERSION (7-7)
src/keyset/types.ts (1)
  • ShareFileRecord (16-28)
src/keyset/policy.ts (1)
  • createDefaultPolicy (14-20)
src/keyset/storage.ts (1)
  • saveShareRecord (48-63)
src/keyset/echo.ts (1)
  • sendShareEcho (19-54)
src/cli.tsx (1)
src/components/Help.tsx (1)
  • Help (8-50)
src/keyset/echoRelays.ts (1)
src/keyset/relays.ts (1)
  • resolveRelaysWithFallbackSync (99-115)
🔇 Additional comments (9)
src/cli.tsx (3)

4-4: LGTM!

The PassThrough import is correctly used to construct a fake stdin stream for non-interactive environments.


116-117: LGTM!

The signature extension to accept optional inkOptions and propagate them to the render call is correct and enables environment-aware Ink behavior.


161-162: LGTM!

The inkOptions are consistently propagated to all render calls (Help, helpable commands, and main App), ensuring uniform environment-aware Ink behavior.

Also applies to: 165-165, 174-175

src/test-relay/NostrRelay.ts (3)

33-41: LGTM!

The function correctly validates tag structure with proper length checks before accessing array elements. The non-null assertion on line 38 is safe due to the length check on line 37.


151-167: LGTM! Filter limit handling is correct.

The REQ handler properly respects per-filter limits as specified in NIP-01. The implementation correctly iterates through each filter independently and enforces the limit for matching events within that filter before sending EOSE.


43-76: LGTM! Filter matching implementation is solid.

The function correctly implements Nostr filter matching per NIP-01, including prefix matching for ids/authors, exact matching for kinds, time windows, and tag-based filters. The defensive type checks are appropriate.

src/components/share/ShareAdd.tsx (1)

568-585: Nice: exit moved to effect with cleanup

Side-effect correctly shifted out of render and made configurable; guards for Node/CLI context look good.

tests/echoRelays.test.ts (1)

1-66: Confirm test runner compatibility (Node vs Bun)

Tests import 'node:test'. If your default runner is Bun, this may not execute. Ensure CI runs Node ≥18 for this suite, or switch to 'bun:test' when running under Bun.

src/keyset/echoRelays.ts (1)

9-22: LGTM: clear normalization and stable relay union

Normalization, fallback derivation, and deduplication behavior look correct and align with tests. Public API is tidy.

Also applies to: 43-65

  - Auto-decrypt once with error guard; auto-exit via effect; stable hooks
  - stdin ref/unref polyfill runs before render
  - unhandledRejection only ignores benign relay case; others rethrow/log
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/components/keyset/KeysetLoad.tsx (2)

110-125: Auto-exit moved to effect correctly; consider defensive process checks for consistency.

The auto-exit logic correctly triggers from a useEffect rather than during render, with proper timer cleanup.

However, lines 112-113 access process.env before verifying that process is defined (line 117). While this code runs only in Node environments where process is always available, line 80 uses a defensive typeof process !== 'undefined' check. Consider applying the same pattern here for consistency, especially given the polyfill mentions in the PR context.

Apply this diff for consistency:

   useEffect(() => {
     if (!result) return;
+    if (typeof process === 'undefined') return;
     const shouldAutoExit =
       process.env.IGLOO_DISABLE_RAW_MODE === '1' || process.env.IGLOO_TEST_AUTOPILOT === '1';
     if (!shouldAutoExit) return;
 
     if (typeof window !== 'undefined') return; // ensure Node/CLI context
-    if (typeof process === 'undefined' || typeof process.exit !== 'function') return;
+    if (typeof process.exit !== 'function') return;

129-136: Consider defensive process check for consistency.

The skipEcho flag correctly controls echo listening behavior. However, line 130 directly accesses process.env without checking typeof process !== 'undefined', while line 80 uses this defensive pattern. For consistency across the component, consider applying the same guard here.

Apply this diff:

   const skipEcho = (() => {
+    if (typeof process === 'undefined') return false;
     const a = (process.env.IGLOO_SKIP_ECHO ?? '').toLowerCase();
     return a === '1' || a === 'true';
   })();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c93041 and 1f40c30.

📒 Files selected for processing (2)
  • src/cli.tsx (5 hunks)
  • src/components/keyset/KeysetLoad.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use Ink components (, , etc.) for terminal UI and do not use HTML elements

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/cli.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All import specifiers must include .js extensions (TypeScript ESM requirement) even though source files are .ts/.tsx

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/cli.tsx
src/components/**

📄 CodeRabbit inference engine (CLAUDE.md)

All UI components should live under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
src/components/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

When adding a new command, implement it as a component under src/components/

Store reusable UI components under src/components/

Files:

  • src/components/keyset/KeysetLoad.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript with ESM, 2-space indentation, single quotes, and trailing commas
Order imports from shallow-to-deep
Use camelCase for utility function and variable names
Use SCREAMING_SNAKE_CASE for constants

Files:

  • src/components/keyset/KeysetLoad.tsx
  • src/cli.tsx
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Name React components using PascalCase

Files:

  • src/components/keyset/KeysetLoad.tsx
src/cli.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

src/cli.tsx: Argument parsing is handled manually in cli.tsx via parseArgv() and must support --flag value, --flag=value, and -f value forms
When adding command flags, extend parseArgv() in cli.tsx for any custom parsing logic

src/cli.tsx: Keep the CLI entrypoint in src/cli.tsx and use it to bootstrap Ink rendering
Keep CLI flags lower-case (e.g., --verbose)

Files:

  • src/cli.tsx
🧠 Learnings (1)
📚 Learning: 2025-10-08T16:24:14.475Z
Learnt from: CR
PR: FROSTR-ORG/igloo-cli#0
File: AGENTS.md:0-0
Timestamp: 2025-10-08T16:24:14.475Z
Learning: Applies to src/cli.tsx : Keep the CLI entrypoint in src/cli.tsx and use it to bootstrap Ink rendering

Applied to files:

  • src/cli.tsx
🧬 Code graph analysis (1)
src/components/keyset/KeysetLoad.tsx (2)
src/keyset/crypto.ts (1)
  • decryptShareCredential (232-284)
src/components/keyset/useShareEchoListener.ts (1)
  • useShareEchoListener (49-233)
🔇 Additional comments (8)
src/cli.tsx (4)

9-42: Well-designed unhandled rejection handler.

The implementation correctly filters the benign Nostr shutdown message while preserving fail-fast behavior for genuine errors. The use of if/else to check process.on availability (rather than an outer try/catch) ensures registration errors are visible, addressing the previous review concern.


158-170: Correct positioning of stdin polyfills.

The polyfills now execute before any render() calls, preventing runtime errors in Bun where ref/unref may be missing. This addresses the previous critical review concern about initialization order.


126-129: Clean propagation of inkOptions for environment-aware rendering.

The changes correctly thread inkOptions through to all render() invocations, enabling consistent non-interactive behavior when IGLOO_DISABLE_RAW_MODE is set.

Also applies to: 185-189, 198-200


140-156: No issues found—the code is correct as written.

The isTTY=true and isRawModeSupported=false combination is intentional and works as designed. The fake stdin with isTTY=true allows Ink to render a terminal UI in non-interactive environments (CI/tests), while isRawModeSupported=false prevents Ink from attempting to enable raw mode, which would fail. Components like Prompt.tsx properly check the isRawModeSupported flag (via useStdin()) and render non-interactive fallbacks when it's false. The no-op ref/unref/setRawMode methods ensure Ink won't error if it attempts to call them. Ink v6.3.1 respects this configuration correctly.

src/components/keyset/KeysetLoad.tsx (4)

25-26: LGTM! Automation state added as suggested.

The autoDecrypting and autoError state variables correctly support the non-interactive decryption flow moved out of render.


66-68: LGTM! Error cleanup on share change.

Clearing autoError when the selected share changes prevents stale error messages from previous attempts.


74-105: LGTM! Auto-decrypt moved to effect with proper safeguards.

The non-interactive decryption flow correctly:

  • Runs outside render in a useEffect
  • Guards against retry loops with the autoError check at line 85
  • Uses a cancellation flag to prevent state updates after unmount
  • Keeps the interactive prompt visible on failure

This addresses the past review concern about side-effects during render.


214-219: LGTM! Automation status rendered inline.

The UI correctly displays automation progress and errors while keeping the interactive prompt visible, allowing users to proceed manually if auto-decryption fails.

  - Fire‑and‑forget echo in autopilot with short pre‑announce grace
  - Fix test relay to decode ws RawData (Buffer/ArrayBuffer/etc.)
  - Deflake CLI flow test: skip echo, clear IGLOO_SKIP_ECHO in teardown
  - Add GitHub Actions CI (build+test) for staging/master
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
src/test-relay/NostrRelay.ts (3)

118-120: Memory leak: purge interval is never cleared.

The past review comment on these lines correctly identifies that the setInterval creates a recurring timer that is never stored or cleared when stop() is called. This issue remains unresolved and should be addressed as suggested in the previous review.


127-134: Incomplete cleanup in stop().

The stop() method does not clear the purge interval timer created in start(). This is the counterpart to the memory leak issue flagged in the previous review. Ensure the interval timer is stored during start() and cleared here during stop().


190-207: Unbounded cache growth between purge intervals.

The past review comment correctly identifies that the cache grows without bounds until the periodic purge fires (default 30 seconds). In high-frequency test scenarios, this could lead to excessive memory consumption. The suggested solutions (max cache size limit or event deduplication by ID) remain valid.

src/components/share/ShareAdd.tsx (1)

574-591: Good fix: move process.exit scheduling into an effect with cleanup.

Removes render‑time side‑effects, accepts '1'|'true', and clears the timer. Looks correct.

🧹 Nitpick comments (5)
tests/cli.share.flow.test.ts (3)

12-12: Use the password-file path (and remove the unused variable).

You create passFile but never use it. Exercise the password-file flow and drop the direct --password to cover more code.

- let passFile: string;
+ let passFile: string;
@@
- passFile = await writePasswordFile(appdataDir, 'strongpassword123');
+ passFile = await writePasswordFile(appdataDir, 'strongpassword123');
@@
-    '--name', name,
-    '--password', 'strongpassword123',
+    '--name', name,
+    '--password-file', passFile,

Also applies to: 17-17, 45-46


64-64: Avoid dynamic RegExp; use a direct substring check.

Safer and simpler; also resolves the static analyzer warning.

-  assert.match(resList.stdout, new RegExp(`${name} share 1`));
+  assert.ok(resList.stdout.includes(`${name} share 1`), 'Expected list to include the saved share entry');

Based on static analysis.


49-51: Speed up the test by disabling the echo grace wait.

Echo is skipped; set the grace to 0 so autopilot doesn’t sleep 500ms before printing “Share saved.”.

-    env: { IGLOO_TEST_AUTOPILOT: '1', IGLOO_SKIP_ECHO: '1' },
+    env: { IGLOO_TEST_AUTOPILOT: '1', IGLOO_SKIP_ECHO: '1', IGLOO_TEST_AUTOPILOT_ECHO_GRACE_MS: '0' },
src/components/share/ShareAdd.tsx (2)

293-301: Skip the grace sleep when echo is disabled.

Currently waits even if IGLOO_SKIP_ECHO disables echo; gate the delay on shouldEcho to cut CI time.

-            // Give the fire-and-forget echo a brief head start so test relays
-            // can observe at least one EVENT before the harness terminates.
-            try {
-              const raw = process.env.IGLOO_TEST_AUTOPILOT_ECHO_GRACE_MS;
-              const parsed = raw ? Number(raw) : NaN;
-              const grace = Number.isFinite(parsed) && parsed >= 0 ? parsed : 500;
-              await new Promise(res => setTimeout(res, grace));
-            } catch {}
+            // Give echo a brief head start only when echo is enabled.
+            if (shouldEcho) {
+              try {
+                const raw = process.env.IGLOO_TEST_AUTOPILOT_ECHO_GRACE_MS;
+                const parsed = raw ? Number(raw) : NaN;
+                const grace = Number.isFinite(parsed) && parsed >= 0 ? parsed : 500;
+                await new Promise(res => setTimeout(res, grace));
+              } catch {}
+            }

438-441: Optional: DRY up env flag parsing.

You parse on/off flags in several places; a tiny helper reduces duplication and mistakes.

Add near the top-level:

const envIsOn = (name: string) => {
  const v = (process.env[name] ?? '').toLowerCase();
  return v === '1' || v === 'true';
};

Then e.g.:

  • const autopilot = envIsOn('IGLOO_TEST_AUTOPILOT');
  • const shouldEcho = !envIsOn('IGLOO_SKIP_ECHO');
  • const nonInteractive = envIsOn('IGLOO_DISABLE_RAW_MODE') || envIsOn('IGLOO_TEST_AUTOPILOT');


Also applies to: 535-538, 276-278, 168-170, 577-579

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1f40c30233588a5e96eeae6a49ac827207efde5a and e039fb25bab3cf7ead16d37f1ff3cad3ce521ec7.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `.github/workflows/ci.yml` (1 hunks)
* `src/components/share/ShareAdd.tsx` (6 hunks)
* `src/test-relay/NostrRelay.ts` (1 hunks)
* `tests/cli.share.flow.test.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* .github/workflows/ci.yml

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (7)</summary>

<details>
<summary>src/**/*.tsx</summary>


**📄 CodeRabbit inference engine (CLAUDE.md)**

> Use Ink components (<Box>, <Text>, etc.) for terminal UI and do not use HTML elements

Files:
- `src/components/share/ShareAdd.tsx`

</details>
<details>
<summary>src/**/*.{ts,tsx}</summary>


**📄 CodeRabbit inference engine (CLAUDE.md)**

> All import specifiers must include .js extensions (TypeScript ESM requirement) even though source files are .ts/.tsx

Files:
- `src/components/share/ShareAdd.tsx`
- `src/test-relay/NostrRelay.ts`

</details>
<details>
<summary>src/components/**</summary>


**📄 CodeRabbit inference engine (CLAUDE.md)**

> All UI components should live under src/components/

Files:
- `src/components/share/ShareAdd.tsx`

</details>
<details>
<summary>src/components/**/*.tsx</summary>


**📄 CodeRabbit inference engine (CLAUDE.md)**

> When adding a new command, implement it as a component under src/components/
> 
> Store reusable UI components under src/components/

Files:
- `src/components/share/ShareAdd.tsx`

</details>
<details>
<summary>**/*.{ts,tsx}</summary>


**📄 CodeRabbit inference engine (AGENTS.md)**

> `**/*.{ts,tsx}`: Use TypeScript with ESM, 2-space indentation, single quotes, and trailing commas
> Order imports from shallow-to-deep
> Use camelCase for utility function and variable names
> Use SCREAMING_SNAKE_CASE for constants

Files:
- `src/components/share/ShareAdd.tsx`
- `src/test-relay/NostrRelay.ts`
- `tests/cli.share.flow.test.ts`

</details>
<details>
<summary>src/components/**/*.{ts,tsx}</summary>


**📄 CodeRabbit inference engine (AGENTS.md)**

> Name React components using PascalCase

Files:
- `src/components/share/ShareAdd.tsx`

</details>
<details>
<summary>**/*.test.ts</summary>


**📄 CodeRabbit inference engine (AGENTS.md)**

> `**/*.test.ts`: Allow colocated test files named feature.test.ts beside implementations
> Test files should exercise both happy paths and failure modes

Files:
- `tests/cli.share.flow.test.ts`

</details>

</details><details>
<summary>🧬 Code graph analysis (2)</summary>

<details>
<summary>src/components/share/ShareAdd.tsx (5)</summary><blockquote>

<details>
<summary>src/keyset/crypto.ts (7)</summary>

* `randomSaltHex` (130-132)
* `deriveSecret` (79-90)
* `SHARE_FILE_PBKDF2_ITERATIONS` (8-8)
* `SHARE_FILE_PASSWORD_ENCODING` (11-11)
* `SHARE_FILE_SALT_PBKDF2_EXPANDED_BYTES` (13-13)
* `encryptPayload` (92-111)
* `SHARE_FILE_VERSION` (7-7)

</details>
<details>
<summary>src/keyset/types.ts (1)</summary>

* `ShareFileRecord` (16-28)

</details>
<details>
<summary>src/keyset/policy.ts (1)</summary>

* `createDefaultPolicy` (14-20)

</details>
<details>
<summary>src/keyset/storage.ts (1)</summary>

* `saveShareRecord` (48-63)

</details>
<details>
<summary>src/keyset/echo.ts (1)</summary>

* `sendShareEcho` (19-54)

</details>

</blockquote></details>
<details>
<summary>tests/cli.share.flow.test.ts (2)</summary><blockquote>

<details>
<summary>tests/helpers/tmp.ts (2)</summary>

* `makeTmp` (5-8)
* `writePasswordFile` (10-14)

</details>
<details>
<summary>tests/helpers/runCli.ts (1)</summary>

* `runCli` (21-82)

</details>

</blockquote></details>

</details><details>
<summary>🪛 ast-grep (0.39.6)</summary>

<details>
<summary>tests/cli.share.flow.test.ts</summary>

[warning] 63-63: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(`${name} share 1`)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
    - https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
    - https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>src/test-relay/NostrRelay.ts (9)</summary><blockquote>

`1-2`: **LGTM!**

Imports are correctly ordered shallow-to-deep and follow ESM conventions.

---

`4-26`: **LGTM!**

Type definitions appropriately model the relay configuration, subscriptions, and Nostr events. The use of `any` for client WebSocket types is acceptable for test infrastructure.

---

`28-76`: **LGTM!**

The helper functions correctly implement Nostr prefix matching, tag filtering, and comprehensive filter matching logic. The non-null assertion on line 38 is safe due to the preceding length check.

---

`78-101`: **LGTM!**

The class structure is clean with proper encapsulation. The readonly getter for `events` appropriately prevents external modifications to the cache.

---

`166-182`: **LGTM!**

The REQ handling correctly implements subscription logic with proper filter matching and limit enforcement. Cached events are streamed before EOSE, following the Nostr protocol.

---

`184-188`: **LGTM!**

The CLOSE handler properly removes the subscription using the correct composite key.

---

`212-218`: **LGTM!**

The close handler properly cleans up all subscriptions for the disconnected client by matching the session ID prefix.

---

`141-164`: **LGTM!**

The message parsing logic comprehensively handles multiple WebSocket data formats with appropriate error handling. The NOTICE response on parse failure follows the Nostr protocol.

---

`222-226`: **LGTM!**

The helper function provides a convenient way to create and start an ephemeral relay for testing, returning both the relay instance and connection URL.

</blockquote></details>
<details>
<summary>src/components/share/ShareAdd.tsx (1)</summary><blockquote>

`166-323`: **Autopilot flow looks solid and addresses prior concerns.**

One‑shot guard, group/share validation, consistent env parsing ('1'|'true'), single now for timestamps, and dual persistence are all in place. LGTM.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@AustinKelsay AustinKelsay merged commit 802e6b8 into master Oct 20, 2025
3 checks passed
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.

1 participant