diff --git a/lib/commands/fund.mjs b/lib/commands/fund.mjs index bfba013..fa41ad2 100644 --- a/lib/commands/fund.mjs +++ b/lib/commands/fund.mjs @@ -93,11 +93,19 @@ asks you to confirm before depositing. (--onramp is the exception: it only mints a browser link; the purchase happens in Circle's widget.) \`selat freeze\` is a local kill switch: this command refuses while frozen.`; +/** Exact `-h` / `--help` argv tokens only. Empty argv is the deposit flow, not usage. */ +function wantsFundHelp(args) { + if (!Array.isArray(args)) return false; + return args.some((a) => a === "-h" || a === "--help"); +} + export async function fund(args, { interactive = stdinIsInteractive(), run = sh, stdout = process.stdout, stderr = process.stderr } = {}) { // Help is inert: no skill lookup, no prompts, no deposit flow. Previously // `fund --help` fell through and started prompting toward a real deposit — // the defect class #101 fixed for setup-policy, one command over. - if (args.includes("-h") || args.includes("--help")) { + // Bare `selat fund` (no tokens) must still enter the deposit flow — a + // `!args.length` (or substring) match here would swallow it and print Usage. + if (wantsFundHelp(args)) { console.log(FUND_HELP); return 0; } diff --git a/lib/selat-pay.mjs b/lib/selat-pay.mjs index 221098b..00c785a 100644 --- a/lib/selat-pay.mjs +++ b/lib/selat-pay.mjs @@ -170,11 +170,22 @@ export async function selatPayVersion() { } function findPackageRoot(binPath) { - // Walk up from the binary looking for a package.json. Bounded depth so a - // weird symlink can't spin forever. + // Walk up from the binary looking for THIS package's package.json. + // Returning the first package.json we see is wrong: a bare SELAT_PAY_BIN + // under an in-tree TMPDIR (or any nested temp path) would claim selat-cli's + // own package as selat-pay and report its version. Bounded so a weird + // symlink can't spin forever. let dir = dirname(binPath); for (let i = 0; i < 6; i++) { - if (existsSync(join(dir, "package.json"))) return dir; + const pkgPath = join(dir, "package.json"); + if (existsSync(pkgPath)) { + try { + const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); + if (pkg?.name === "@selat-ai/selat-pay") return dir; + } catch { + // unreadable or invalid JSON — keep walking + } + } const parent = dirname(dir); if (parent === dir) break; dir = parent; diff --git a/test/selat-pay-resolve.test.mjs b/test/selat-pay-resolve.test.mjs index 1a2fe66..9092c40 100644 --- a/test/selat-pay-resolve.test.mjs +++ b/test/selat-pay-resolve.test.mjs @@ -88,7 +88,13 @@ test("ensureSelatPayHistoryDir honors XDG_STATE_HOME when no explicit path is se // ── An override that is a bare binary (no package.json, doesn't run) ───────── test("a bare override resolves with a null packageRoot and no version claim", async () => { - const dir = mkdtempSync(join(tmpdir(), "selat-pay-bare-")); + // Nest under an unrelated package.json so a naive "walk until any + // package.json" resolver cannot pass this when TMPDIR is /tmp. In-tree + // temps (and this decoy) used to resolve to that ancestor and claim its + // version as selat-pay. + const decoy = mkdtempSync(join(tmpdir(), "selat-pay-decoy-")); + writeFileSync(join(decoy, "package.json"), JSON.stringify({ name: "@selat-ai/selat-cli", version: "0.0.0" })); + const dir = mkdtempSync(join(decoy, "bare-")); const bin = join(dir, "selat-pay.mjs"); writeFileSync(bin, "process.exit(0);\n"); const restore = setEnv({ SELAT_PAY_BIN: bin });