@carlelieser/nexus-cli 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -281,7 +281,8 @@ const out = {
281
281
  `);
282
282
  }
283
283
  };
284
- const ACCOUNT_URL = "https://www.nexusmods.com/users/myaccount";
284
+ const MAIN_HOST = "www.nexusmods.com";
285
+ const ACCOUNT_URL = `https://${MAIN_HOST}/users/myaccount`;
285
286
  const SIGN_IN_HOST$1 = "users.nexusmods.com";
286
287
  const NEXUS_DOMAIN = ".nexusmods.com";
287
288
  const CHALLENGE_TIMEOUT_MS = 25e3;
@@ -377,6 +378,9 @@ class CamoufoxSession {
377
378
  return this.page.content();
378
379
  }
379
380
  async postJson(url, body, headers = {}) {
381
+ if (new URL(this.page.url()).host !== MAIN_HOST) {
382
+ await this.navigate(`https://${MAIN_HOST}/`).catch(() => void 0);
383
+ }
380
384
  for (let attempt = 0; ; attempt++) {
381
385
  await this.page.waitForLoadState("domcontentloaded", { timeout: 1e4 }).catch(() => void 0);
382
386
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/core/errors.ts","../../src/core/types.ts","../../src/app/backoff.ts","../../src/app/retry.ts","../../src/app/downloadCollection.ts","../../src/app/downloadMod.ts","../../src/app/restoreSession.ts","../../src/config/paths.ts","../../src/adapters/nexus/parseNexusUrl.ts","../../src/cli/output.ts","../../src/adapters/browser/CamoufoxBrowser.ts","../../src/adapters/cookies/BrowserCookieSource.ts","../../src/adapters/cookies/FileCookieSource.ts","../../src/adapters/download/BrowserDownloader.ts","../../src/adapters/nexus/NexusWebAdapter.ts","../../src/adapters/session/FileSessionStore.ts","../../src/cli/wiring.ts","../../src/cli/commands/download.ts","../../src/app/importSession.ts","../../src/cli/commands/import.ts","../../src/cli/commands/logout.ts","../../src/cli/index.ts"],"sourcesContent":["/**\n * Typed, recoverable errors. The CLI renders these as concise one-line\n * messages (no stack trace unless --verbose).\n */\n\nexport abstract class NexusError extends Error {\n abstract readonly kind: string;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options as ErrorOptions);\n this.name = new.target.name;\n }\n}\n\n/** Session is missing, invalid, or expired; the user must re-login. */\nexport class AuthError extends NexusError {\n readonly kind = 'auth';\n}\n\n/** A page could not be parsed into the expected domain shape. */\nexport class ScrapeError extends NexusError {\n readonly kind = 'scrape';\n}\n\n/** A file download failed after exhausting retries. */\nexport class DownloadError extends NexusError {\n readonly kind = 'download';\n}\n\n/** A transient network/transport failure. */\nexport class NetworkError extends NexusError {\n readonly kind = 'network';\n}\n\n/** The site is signalling throttling (HTTP 429 / Cloudflare challenge). */\nexport class ThrottleError extends NexusError {\n readonly kind = 'throttle';\n}\n\n/** The run was cancelled by the user (Ctrl+C). Maps to exit code 130. */\nexport class CancelError extends NexusError {\n readonly kind = 'cancel';\n}\n\n/**\n * Whether an error represents user cancellation — either our own\n * {@link CancelError} or the `AbortError` a DOMException-style abort throws.\n */\nexport function isCancel(e: unknown): boolean {\n return e instanceof CancelError || (e instanceof Error && e.name === 'AbortError');\n}\n\nexport function isNexusError(e: unknown): e is NexusError {\n return e instanceof NexusError;\n}\n\nexport function messageOf(e: unknown): string {\n if (e instanceof Error) return e.message;\n return String(e);\n}\n","/**\n * Pure domain types. No I/O, no dependency on adapters.\n */\n\n/** A Nexus game domain slug, e.g. `skyrimspecialedition`. */\nexport type GameDomain = string;\n\n/** A browser cookie, in the shape Playwright round-trips. */\nexport interface Cookie {\n name: string;\n value: string;\n domain: string;\n path: string;\n expires?: number;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Strict' | 'Lax' | 'None';\n}\n\n/**\n * A persisted, reusable authenticated session — cookies imported from the\n * user's real browser (which has already cleared Cloudflare and logged in).\n */\nexport interface Session {\n /** Nexus username, when resolvable from the imported data. */\n username: string;\n /** Imported Nexus cookies, replayed into a headless context for downloads. */\n cookies: Cookie[];\n /** ISO-8601 timestamp of when the cookies were imported. */\n capturedAt: string;\n}\n\n/** A single downloadable file resolved from a mod's files page. */\nexport interface DownloadTarget {\n /** The (possibly relative) URL that initiates the download. */\n url: string;\n /** Nexus file id, used for fallback naming. */\n fileId: number;\n /** Human-readable file name from the page, if known. */\n fileName?: string;\n /** Nexus file category. Only `main` files are downloaded by default. */\n category: FileCategory;\n}\n\nexport type FileCategory = 'main' | 'optional' | 'miscellaneous' | 'old' | 'unknown';\n\n/** A mod, identified within a game domain. */\nexport interface Mod {\n game: GameDomain;\n modId: number;\n /** Mod display name, when resolved. */\n name?: string;\n}\n\n/**\n * A member of a collection — one specific file the collection curates. A\n * collection pins exact files (by `fileId`), not just mods, and flags some as\n * optional.\n */\nexport interface CollectionMember {\n game: GameDomain;\n modId: number;\n /** The specific file the collection pins for this mod. */\n fileId: number;\n /** Whether the collection marks this file optional. */\n optional: boolean;\n /** File / mod display name, when available. */\n name?: string;\n /** File size in bytes, when the API reports it (for a global ETA). */\n sizeBytes?: number;\n}\n\n/** A collection, identified by slug or numeric id within a game domain. */\nexport interface Collection {\n game: GameDomain;\n /** The slug or id as supplied by the user. */\n ref: string;\n members: CollectionMember[];\n}\n\n/** Outcome of attempting to download one mod. */\nexport interface ModResult {\n modId: number;\n ok: boolean;\n /** Paths of files written for this mod (on success). */\n files: string[];\n /** Error message (on failure). */\n error?: string;\n /** Set when the failure was attributed to site throttling. */\n throttled?: boolean;\n}\n\n/** Aggregate result of a download run (single mod or collection). */\nexport interface DownloadReport {\n results: ModResult[];\n succeeded: number;\n failed: number;\n}\n\nexport function summarize(results: ModResult[]): DownloadReport {\n return {\n results,\n succeeded: results.filter((r) => r.ok).length,\n failed: results.filter((r) => !r.ok).length,\n };\n}\n","/**\n * Adaptive pacing for collection downloads. Pure, side-effect-free state\n * machine so it can be unit-tested without a browser.\n *\n * Starts fast (no delay, full concurrency). On a throttle signal it inserts\n * and progressively grows an inter-mod delay and reduces effective\n * concurrency. After a run of clean successes it relaxes back toward fast.\n */\n\nexport interface BackoffConfig {\n /** Configured ceiling for concurrency (the user's --concurrency). */\n maxConcurrency: number;\n /** Delay added on the first throttle signal (ms). */\n baseDelayMs: number;\n /** Hard cap on inter-mod delay (ms). */\n maxDelayMs: number;\n /** Clean successes required before relaxing one step. */\n relaxAfter: number;\n}\n\nexport const DEFAULT_BACKOFF: Omit<BackoffConfig, 'maxConcurrency'> = {\n baseDelayMs: 2_000,\n maxDelayMs: 60_000,\n relaxAfter: 5,\n};\n\nexport class BackoffPolicy {\n private delayMs = 0;\n private concurrency: number;\n private cleanStreak = 0;\n private readonly cfg: BackoffConfig;\n\n constructor(cfg: BackoffConfig) {\n this.cfg = cfg;\n this.concurrency = cfg.maxConcurrency;\n }\n\n /** Current inter-mod delay to wait before starting the next member. */\n get currentDelayMs(): number {\n return this.delayMs;\n }\n\n /** Current effective concurrency. */\n get currentConcurrency(): number {\n return this.concurrency;\n }\n\n /** Record a throttle signal (429 / Cloudflare / repeated timeout). */\n onThrottle(): void {\n this.cleanStreak = 0;\n this.delayMs =\n this.delayMs === 0 ? this.cfg.baseDelayMs : Math.min(this.delayMs * 2, this.cfg.maxDelayMs);\n this.concurrency = Math.max(1, this.concurrency - 1);\n }\n\n /** Record a clean success; may relax pacing after enough in a row. */\n onSuccess(): void {\n this.cleanStreak += 1;\n if (this.cleanStreak < this.cfg.relaxAfter) return;\n this.cleanStreak = 0;\n\n if (this.concurrency < this.cfg.maxConcurrency) {\n this.concurrency += 1;\n } else if (this.delayMs > 0) {\n this.delayMs = Math.floor(this.delayMs / 2);\n if (this.delayMs < this.cfg.baseDelayMs / 2) this.delayMs = 0;\n }\n }\n}\n","import { isCancel, ThrottleError } from '@core/errors.js';\n\nexport interface RetryOptions {\n attempts: number;\n baseDelayMs: number;\n /** Injected sleep, for deterministic tests. */\n sleep?: (ms: number) => Promise<void>;\n /** When aborted, stop retrying and re-throw immediately. */\n signal?: AbortSignal;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** Detect whether an error indicates site-side throttling. */\nexport function isThrottle(e: unknown): boolean {\n if (e instanceof ThrottleError) return true;\n const msg = (e instanceof Error ? e.message : String(e)).toLowerCase();\n return (\n msg.includes('429') ||\n msg.includes('too many requests') ||\n msg.includes('cloudflare') ||\n msg.includes('just a moment') ||\n msg.includes('timeout')\n );\n}\n\n/**\n * Run `fn` with exponential backoff. Re-throws the last error if all fail.\n * A cancellation (abort) is never retried — it short-circuits immediately.\n */\nexport async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {\n const sleep = opts.sleep ?? defaultSleep;\n let lastErr: unknown;\n for (let attempt = 1; attempt <= opts.attempts; attempt++) {\n opts.signal?.throwIfAborted();\n try {\n return await fn();\n } catch (e) {\n if (isCancel(e) || opts.signal?.aborted) throw e;\n lastErr = e;\n if (attempt === opts.attempts) break;\n await sleep(opts.baseDelayMs * 2 ** (attempt - 1));\n }\n }\n throw lastErr;\n}\n","import type { BrowserSession } from '@adapters/browser/Browser.js';\nimport type { Downloader, DownloadProgress } from '@adapters/download/Downloader.js';\nimport type { NexusSite } from '@adapters/nexus/NexusSite.js';\nimport { isCancel } from '@core/errors.js';\nimport {\n type CollectionMember,\n type DownloadReport,\n type DownloadTarget,\n type GameDomain,\n type ModResult,\n summarize,\n} from '@core/types.js';\nimport { BackoffPolicy, DEFAULT_BACKOFF } from './backoff.js';\nimport { isThrottle, withRetry } from './retry.js';\n\nexport interface DownloadCollectionDeps {\n site: NexusSite;\n downloader: Downloader;\n}\n\nexport interface DownloadCollectionParams {\n game: GameDomain;\n ref: string;\n outDir: string;\n concurrency: number;\n dryRun: boolean;\n /** Include files the collection marks optional (default: required only). */\n includeOptional: boolean;\n retryAttempts: number;\n retryBaseDelayMs: number;\n /** Injected sleep, for deterministic tests. Defaults to real timers. */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Cancels the run (Ctrl+C). Stops starting new members and aborts the\n * in-flight download; surfaces as a CancelError from this function.\n */\n signal?: AbortSignal;\n /** Called once with the full member list after it is resolved. */\n onResolved?: (members: CollectionMember[]) => void;\n /** Called before a member starts downloading (for live progress). */\n onStart?: (member: CollectionMember, index: number, total: number) => void;\n /** Per-file byte progress for the active member. */\n onFileProgress?: (p: DownloadProgress) => void;\n /** Progress callback, per completed member. */\n onProgress?: (r: ModResult) => void;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/**\n * Fetch a collection's pinned files via the Nexus GraphQL API and download each\n * one directly by its `fileId` (the collection curates exact files, so we do\n * not re-scrape per-mod files pages). Best-effort: one failure never aborts the\n * batch. Pacing adapts to throttling via {@link BackoffPolicy}, which lives in\n * the app layer (browser-agnostic).\n */\nexport async function downloadCollection(\n deps: DownloadCollectionDeps,\n session: BrowserSession,\n params: DownloadCollectionParams,\n): Promise<DownloadReport> {\n const sleep = params.sleep ?? defaultSleep;\n\n params.signal?.throwIfAborted();\n const req = deps.site.collectionMembersQuery(params.game, params.ref);\n const json = await session.postJson(req.url, req.body, req.headers);\n const all = deps.site.parseCollectionMembers(json);\n const members = params.includeOptional ? all : all.filter((m) => !m.optional);\n params.onResolved?.(members);\n\n const policy = new BackoffPolicy({\n maxConcurrency: params.concurrency,\n ...DEFAULT_BACKOFF,\n });\n\n const results: ModResult[] = [];\n for (let i = 0; i < members.length; i++) {\n // Cancellation stops *starting* new members; the in-flight download is\n // aborted from inside runOne, which re-throws to break out of the loop.\n params.signal?.throwIfAborted();\n\n const member = members[i]!;\n if (policy.currentDelayMs > 0) {\n await sleep(policy.currentDelayMs);\n }\n\n params.onStart?.(member, i + 1, members.length);\n const result = await runOne(deps, session, params, member);\n results.push(result);\n\n if (result.ok) policy.onSuccess();\n else if (result.throttled) policy.onThrottle();\n\n params.onProgress?.(result);\n }\n\n return summarize(results);\n}\n\nasync function runOne(\n deps: DownloadCollectionDeps,\n session: BrowserSession,\n params: DownloadCollectionParams,\n member: CollectionMember,\n): Promise<ModResult> {\n if (params.dryRun) {\n return {\n modId: member.modId,\n ok: true,\n files: [member.name ?? `mod ${member.modId} file ${member.fileId}`],\n };\n }\n\n const target: DownloadTarget = {\n url: deps.site.fileDownloadUrl(member.game, member.modId, member.fileId),\n fileId: member.fileId,\n category: member.optional ? 'optional' : 'main',\n ...(member.name ? { fileName: member.name } : {}),\n };\n\n try {\n const path = await withRetry(\n () =>\n deps.downloader.fetch(target, params.outDir, session, params.onFileProgress, params.signal),\n {\n attempts: params.retryAttempts,\n baseDelayMs: params.retryBaseDelayMs,\n ...(params.sleep ? { sleep: params.sleep } : {}),\n ...(params.signal ? { signal: params.signal } : {}),\n },\n );\n return { modId: member.modId, ok: true, files: [path] };\n } catch (e) {\n // A cancellation is not a per-member failure — let it abort the batch.\n if (isCancel(e)) throw e;\n const message = e instanceof Error ? e.message : String(e);\n return {\n modId: member.modId,\n ok: false,\n files: [],\n error: message,\n throttled: isThrottle(e),\n };\n }\n}\n","import type { BrowserSession } from '@adapters/browser/Browser.js';\nimport type { Downloader, DownloadProgress } from '@adapters/download/Downloader.js';\nimport type { NexusSite } from '@adapters/nexus/NexusSite.js';\nimport { AuthError, ScrapeError } from '@core/errors.js';\nimport type { GameDomain, ModResult } from '@core/types.js';\nimport { withRetry } from './retry.js';\n\nexport interface DownloadModDeps {\n site: NexusSite;\n downloader: Downloader;\n}\n\nexport interface DownloadModParams {\n game: GameDomain;\n modId: number;\n outDir: string;\n dryRun: boolean;\n retryAttempts: number;\n retryBaseDelayMs: number;\n /** Injected sleep for retry, for deterministic tests. */\n sleep?: (ms: number) => Promise<void>;\n /** Per-file byte progress callback. */\n onFileProgress?: (p: DownloadProgress) => void;\n /** Cancels the run (Ctrl+C) between and during file downloads. */\n signal?: AbortSignal;\n}\n\n/**\n * Resolve a mod's main file(s) and download them through `session`. Pure\n * orchestration: all site/browser/disk specifics are behind the injected\n * interfaces.\n */\nexport async function downloadMod(\n deps: DownloadModDeps,\n session: BrowserSession,\n params: DownloadModParams,\n): Promise<ModResult> {\n params.signal?.throwIfAborted();\n const url = deps.site.modFilesUrl(params.game, params.modId);\n const landed = await session.goto(url);\n\n if (deps.site.isAuthRedirect(landed)) {\n throw new AuthError('session expired or not authenticated');\n }\n\n const html = await session.html();\n const all = deps.site.parseDownloadTargets(html);\n const main = all.filter((t) => t.category === 'main');\n if (main.length === 0) {\n throw new ScrapeError(`mod ${params.modId} has no main files`);\n }\n\n if (params.dryRun) {\n return {\n modId: params.modId,\n ok: true,\n files: main.map((t) => t.fileName ?? `file-${t.fileId}`),\n };\n }\n\n const files: string[] = [];\n for (const target of main) {\n params.signal?.throwIfAborted();\n const path = await withRetry(\n () =>\n deps.downloader.fetch(target, params.outDir, session, params.onFileProgress, params.signal),\n {\n attempts: params.retryAttempts,\n baseDelayMs: params.retryBaseDelayMs,\n ...(params.sleep ? { sleep: params.sleep } : {}),\n ...(params.signal ? { signal: params.signal } : {}),\n },\n );\n files.push(path);\n }\n\n return { modId: params.modId, ok: true, files };\n}\n","import type { Browser, BrowserSession } from '@adapters/browser/Browser.js';\nimport type { SessionStore } from '@adapters/session/SessionStore.js';\nimport { AuthError } from '@core/errors.js';\n\nexport interface RestoreDeps {\n browser: Browser;\n store: SessionStore;\n}\n\n/**\n * Launch a browser and seed it with the imported session cookies. Throws\n * {@link AuthError} (→ exit code 2) when no session is stored. The caller owns\n * closing the returned session.\n */\nexport async function restoreSession(deps: RestoreDeps, headful: boolean): Promise<BrowserSession> {\n const saved = await deps.store.load();\n if (!saved) {\n throw new AuthError('no saved session — run `nexus import --from chrome` first');\n }\n const session = await deps.browser.launch({ headful });\n await session.setCookies(saved.cookies);\n\n // Visit the account page first: this validates the session AND warms the\n // context past Cloudflare's challenge before any deep mod-page navigation\n // (a cold jump straight to a files page is more likely to be challenged).\n if (!(await session.isLoggedIn())) {\n await session.close();\n throw new AuthError('session expired — run `nexus import --from chrome` again');\n }\n return session;\n}\n","import { homedir, platform } from 'node:os';\nimport { join } from 'node:path';\n\nimport type { GameDomain } from '@core/types.js';\n\nconst APP = 'nexus-cli';\n\n/**\n * OS-appropriate config directory.\n * - Linux: $XDG_CONFIG_HOME/nexus-cli or ~/.config/nexus-cli\n * - macOS: ~/Library/Application Support/nexus-cli\n * - Windows: %APPDATA%/nexus-cli\n */\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME;\n if (xdg) return join(xdg, APP);\n\n switch (platform()) {\n case 'win32':\n return join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), APP);\n case 'darwin':\n return join(homedir(), 'Library', 'Application Support', APP);\n default:\n return join(homedir(), '.config', APP);\n }\n}\n\n/** Path to the persisted session file. */\nexport function sessionFile(): string {\n return join(configDir(), 'session.json');\n}\n\n/** Default download directory for a game domain. */\nexport function defaultOutDir(game: GameDomain): string {\n return join(process.cwd(), 'downloads', game);\n}\n","import type { GameDomain } from '@core/types.js';\n\n/** A download target parsed from a Nexus URL: either a mod or a collection. */\nexport type NexusRef =\n { game: GameDomain; modId: number } | { game: GameDomain; collection: string };\n\n// Mod page: nexusmods.com/<game>/mods/<id>\n// Newer mod page: nexusmods.com/games/<game>/mods/<id>\nconst MOD = /nexusmods\\.com\\/(?:games\\/)?([^/]+)\\/mods\\/(\\d+)/i;\n// Collection page: nexusmods.com/games/<game>/collections/<slug>\nconst COLLECTION = /nexusmods\\.com\\/games\\/([^/]+)\\/collections\\/([^/?#]+)/i;\n\n/**\n * Parse a nexusmods.com mod or collection URL into the game domain and id/slug\n * the `download` command needs. Returns null for anything that isn't a\n * recognised mod or collection URL (the caller falls back to explicit flags).\n */\nexport function parseNexusUrl(input: string): NexusRef | null {\n const collection = COLLECTION.exec(input);\n if (collection) {\n return { game: collection[1]!, collection: collection[2]! };\n }\n const mod = MOD.exec(input);\n if (mod) {\n return { game: mod[1]!, modId: Number(mod[2]) };\n }\n return null;\n}\n","import { isNexusError } from '@core/errors.js';\n\n/** Minimal user-facing output. Errors render as one line unless verbose. */\nexport const out = {\n info(msg: string): void {\n process.stdout.write(`${msg}\\n`);\n },\n success(msg: string): void {\n process.stdout.write(`✓ ${msg}\\n`);\n },\n warn(msg: string): void {\n process.stderr.write(`! ${msg}\\n`);\n },\n error(e: unknown, verbose: boolean): void {\n if (verbose && e instanceof Error && e.stack) {\n process.stderr.write(`${e.stack}\\n`);\n return;\n }\n const prefix = isNexusError(e) ? `${e.kind} error` : 'error';\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`✗ ${prefix}: ${msg}\\n`);\n },\n};\n","import { mkdtemp, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { Camoufox } from 'camoufox-js';\nimport type { BrowserContext, Page, Response } from 'playwright-core';\n\nimport { NetworkError } from '@core/errors.js';\nimport type { Cookie } from '@core/types.js';\nimport type { Browser, BrowserSession, LaunchOptions, ResolvedDownload } from './Browser.js';\n\nconst ACCOUNT_URL = 'https://www.nexusmods.com/users/myaccount';\nconst SIGN_IN_HOST = 'users.nexusmods.com';\nconst NEXUS_DOMAIN = '.nexusmods.com';\n\n/** How long to wait for Camoufox to auto-solve a Cloudflare challenge. */\nconst CHALLENGE_TIMEOUT_MS = 25_000;\n\n/**\n * Fallback cap for the GenerateDownloadUrl resolver POST. A real success or\n * failure response normally arrives in seconds; this only bounds the case where\n * the click never fired the request at all.\n */\nconst RESOLVE_TIMEOUT_MS = 60_000;\n\n/** `page.goto`/`waitForNavigation` yield a Response, or null for same-document nav. */\ntype NavResponse = Response | null;\n\n/**\n * Whether a navigation response is a Cloudflare challenge. Cloudflare sets\n * `cf-mitigated: challenge` on the interstitial response and omits it on the\n * real page — a deterministic protocol signal, so we never scrape page markup.\n */\nfunction isChallenge(response: NavResponse): boolean {\n return response?.headers()['cf-mitigated'] === 'challenge';\n}\n\n/** Whether an error is Playwright's \"execution context destroyed\" mid-navigation. */\nfunction isContextDestroyed(e: unknown): boolean {\n return e instanceof Error && /execution context was destroyed/i.test(e.message);\n}\n\n/** Camoufox-backed implementation of the Browser interface. */\nexport class CamoufoxBrowser implements Browser {\n async launch(opts: LaunchOptions): Promise<BrowserSession> {\n // Auth comes from imported cookies, so a throwaway profile is fine. Passing\n // user_data_dir makes Camoufox return a fully-configured BrowserContext —\n // we must NOT call newContext() ourselves (Camoufox's patched Firefox\n // rejects Playwright's setDefaultViewport).\n const userDataDir = await mkdtemp(join(tmpdir(), 'nexus-camoufox-'));\n const context = await Camoufox({\n headless: !opts.headful,\n user_data_dir: userDataDir,\n humanize: true,\n // Pin locale to match the imported session's origin. geoip is left OFF:\n // when it resolved to a different region than the session cookies, the\n // fingerprint/cookie mismatch triggered a hard Cloudflare challenge.\n locale: 'en-US',\n });\n\n const page = context.pages()[0] ?? (await context.newPage());\n return new CamoufoxSession(context, page, userDataDir);\n }\n}\n\nclass CamoufoxSession implements BrowserSession {\n constructor(\n private readonly context: BrowserContext,\n private readonly page: Page,\n private readonly userDataDir: string,\n ) {}\n\n async goto(url: string): Promise<string> {\n await this.navigate(url);\n return this.page.url();\n }\n\n /**\n * Navigate to `url` and, if Cloudflare intercepts with a challenge, wait for\n * Camoufox to auto-solve it. Returns the response for the real page.\n *\n * A challenge is identified by the `cf-mitigated: challenge` response header —\n * a protocol signal, not page markup. When it's absent the response is the\n * real page and we return at once; when present, Camoufox's auto-solve fires\n * its own navigation to the real page, which we wait for.\n */\n private async navigate(url: string): Promise<NavResponse> {\n let response: NavResponse;\n try {\n // `commit` resolves as soon as the response headers arrive — we don't\n // block on a heavy page's trailing resources.\n response = await this.page.goto(url, { waitUntil: 'commit' });\n } catch (e) {\n throw new NetworkError(`failed to load ${url}`, { cause: e });\n }\n\n const deadline = Date.now() + CHALLENGE_TIMEOUT_MS;\n while (isChallenge(response) && Date.now() < deadline) {\n response = await this.page\n .waitForNavigation({ waitUntil: 'commit', timeout: deadline - Date.now() })\n .catch(() => response);\n }\n\n // Let the document finish (incl. any client-side redirect like\n // myaccount → settings) before returning, so a subsequent navigation does\n // not abort an in-flight one (NS_BINDING_ABORTED). Best-effort: a heavy\n // page that never fully idles must not block the caller.\n await this.page\n .waitForLoadState('domcontentloaded', { timeout: 10_000 })\n .catch(() => undefined);\n return response;\n }\n\n async setCookies(cookies: Cookie[]): Promise<void> {\n await this.context.addCookies(\n cookies.map((c) => ({\n name: c.name,\n value: c.value,\n domain: c.domain || NEXUS_DOMAIN,\n path: c.path || '/',\n ...(c.expires !== undefined ? { expires: c.expires } : {}),\n ...(c.httpOnly !== undefined ? { httpOnly: c.httpOnly } : {}),\n ...(c.secure !== undefined ? { secure: c.secure } : {}),\n ...(c.sameSite ? { sameSite: c.sameSite } : {}),\n })),\n );\n }\n\n async isLoggedIn(): Promise<boolean> {\n try {\n await this.navigate(ACCOUNT_URL);\n } catch {\n return false;\n }\n // The account URL redirects to /settings or /users/... when authenticated,\n // and to the sign-in host when not.\n if (new URL(this.page.url()).host === SIGN_IN_HOST) return false;\n const url = this.page.url();\n return url.includes('/users/') || url.includes('/settings');\n }\n\n async html(): Promise<string> {\n // `content()` throws if the page is mid-navigation (the `commit`-based goto\n // can return while a redirect is still settling). Wait for the DOM to be\n // ready and retry a couple of times before giving up.\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n await this.page.waitForLoadState('domcontentloaded', { timeout: 10_000 });\n return await this.page.content();\n } catch {\n await this.page.waitForTimeout(500);\n }\n }\n return this.page.content();\n }\n\n async postJson(\n url: string,\n body: unknown,\n headers: Record<string, string> = {},\n ): Promise<unknown> {\n // Run fetch INSIDE the page so cookies + origin (nexusmods.com) apply —\n // required for the GraphQL endpoint to accept the request.\n //\n // The page may still be settling a client-side redirect from the preceding\n // navigation (e.g. account-page warm-up), which destroys the execution\n // context mid-evaluate. Wait for the DOM to be ready and retry once.\n for (let attempt = 0; ; attempt++) {\n await this.page\n .waitForLoadState('domcontentloaded', { timeout: 10_000 })\n .catch(() => undefined);\n try {\n return await this.page.evaluate(\n async ({ url, body, headers }) => {\n const res = await fetch(url, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);\n return res.json() as Promise<unknown>;\n },\n { url, body, headers },\n );\n } catch (e) {\n if (attempt >= 2 || !isContextDestroyed(e)) throw e;\n }\n }\n }\n\n async resolveUsername(): Promise<string | null> {\n await this.navigate(ACCOUNT_URL).catch(() => undefined);\n return this.page\n .evaluate(() => {\n const el =\n document.querySelector<HTMLElement>('[data-username]') ??\n document.querySelector<HTMLElement>('#login-name, .username');\n const ds = el?.dataset?.username;\n if (ds) return ds;\n const text = el?.textContent?.trim();\n return text && text.length > 0 ? text : null;\n })\n .catch(() => null);\n }\n\n async resolveDownloadUrl(filePageUrl: string): Promise<ResolvedDownload> {\n try {\n // The manual-download page renders a <mod-file-download> web component\n // whose \"Slow download\" button (in an open shadow root) triggers a POST\n // to GenerateDownloadUrl that returns the signed CDN URL. We drive that\n // button (the trusted path that clears Cloudflare) and capture the\n // resolver's JSON response — then Node streams the URL itself.\n await this.navigate(filePageUrl);\n\n const slowButton = this.page.getByRole('button', { name: /slow download/i });\n await slowButton.waitFor({ state: 'visible', timeout: 30_000 });\n\n // Wait for the resolver POST regardless of status. Matching only 200s\n // meant a failed resolve never matched and we hung the full timeout; the\n // failure response is the signal, so inspect it instead of the page.\n const respPromise = this.page.waitForResponse((r) => /GenerateDownloadUrl/i.test(r.url()), {\n timeout: RESOLVE_TIMEOUT_MS,\n });\n // We don't want the native download — cancel it; we stream the URL.\n this.page.once('download', (d) => void d.cancel().catch(() => undefined));\n await slowButton.click();\n\n const resp = await respPromise;\n if (!resp.ok()) {\n // e.g. Nexus's \"download link could not be retrieved, try again later\".\n // Retryable — surfaced as NetworkError so withRetry backs off and retries.\n throw new NetworkError(`download resolver returned HTTP ${resp.status()}`);\n }\n const cdnUrl = ((await resp.json()) as { url?: string }).url;\n if (!cdnUrl) throw new NetworkError('resolver returned no download URL');\n\n const cookieHeader = (await this.context.cookies())\n .map((c) => `${c.name}=${c.value}`)\n .join('; ');\n const userAgent = await this.page.evaluate(() => navigator.userAgent);\n return { cdnUrl, cookieHeader, userAgent };\n } catch (e) {\n if (e instanceof NetworkError) throw e;\n throw new NetworkError(`failed to resolve download for ${filePageUrl}`, {\n cause: e,\n });\n }\n }\n\n async close(): Promise<void> {\n await this.context.close().catch(() => undefined);\n await rm(this.userDataDir, { recursive: true, force: true }).catch(() => undefined);\n }\n}\n","import type { CookieQueryStrategy, ExportedCookie } from '@mherod/get-cookie';\n\nimport { AuthError } from '@core/errors.js';\nimport type { Cookie } from '@core/types.js';\nimport type { CookieSource } from './CookieSource.js';\n\n/** Chromium variants get-cookie targets by name, sharing Chrome's cookie schema. */\nconst CHROMIUM_VARIANTS = [\n 'chromium',\n 'brave',\n 'edge',\n 'opera',\n 'opera-gx',\n 'vivaldi',\n 'arc',\n 'whale',\n] as const;\n\ntype ChromiumVariant = (typeof CHROMIUM_VARIANTS)[number];\n\nfunction isChromiumVariant(key: string): key is ChromiumVariant {\n return (CHROMIUM_VARIANTS as readonly string[]).includes(key);\n}\n\n/** Display names for the `--from` values we accept, for messages. */\nconst DISPLAY: Record<string, string> = {\n chrome: 'Chrome',\n chromium: 'Chromium',\n brave: 'Brave',\n edge: 'Edge',\n opera: 'Opera',\n 'opera-gx': 'Opera GX',\n vivaldi: 'Vivaldi',\n arc: 'Arc',\n whale: 'Whale',\n firefox: 'Firefox',\n safari: 'Safari',\n};\n\n/**\n * Reads cookies directly from an installed browser via `@mherod/get-cookie`,\n * which handles each platform's encryption (macOS Keychain, Windows DPAPI,\n * Linux libsecret). Supports Chrome and its Chromium cousins, Firefox, and\n * Safari — the latter may prompt once for macOS Full Disk Access.\n */\nexport class BrowserCookieSource implements CookieSource {\n readonly browser: string;\n private readonly key: string;\n\n constructor(from: string) {\n this.key = from.toLowerCase();\n if (!(this.key in DISPLAY)) {\n throw new AuthError(\n `unsupported browser '${from}' (supported: ${Object.keys(DISPLAY).join(', ')})`,\n );\n }\n this.browser = DISPLAY[this.key]!;\n }\n\n async read(domainSuffix: string): Promise<Cookie[]> {\n const strategy = await strategyFor(this.key);\n let found: ExportedCookie[];\n try {\n // '%' is get-cookie's match-all name pattern; we want every cookie on the\n // domain, not one named cookie.\n found = await strategy.queryCookies('%', domainSuffix);\n } catch (e) {\n throw new AuthError(`could not read ${this.browser} cookies`, { cause: e });\n }\n return found.map(toCookie);\n }\n}\n\n/**\n * Map a `--from` value to its get-cookie strategy. Imports get-cookie lazily —\n * and only after silencing its dotenv banner — so the noise never reaches the\n * user and the dep isn't loaded for commands that don't read a browser.\n */\nasync function strategyFor(key: string): Promise<CookieQueryStrategy> {\n process.env.DOTENV_CONFIG_QUIET = 'true';\n const gc = await import('@mherod/get-cookie');\n if (key === 'chrome') return new gc.ChromeCookieQueryStrategy();\n if (key === 'firefox') return new gc.FirefoxCookieQueryStrategy();\n if (key === 'safari') return new gc.SafariCookieQueryStrategy();\n if (isChromiumVariant(key)) return new gc.ChromiumCookieQueryStrategy(key);\n // Unreachable: the constructor already rejected unknown keys.\n throw new AuthError(`unsupported browser '${key}'`);\n}\n\n/** Convert a get-cookie `ExportedCookie` into our Playwright-shaped `Cookie`. */\nexport function toCookie(c: ExportedCookie): Cookie {\n const cookie: Cookie = {\n name: c.name,\n value: String(c.value),\n domain: c.domain,\n path: c.meta?.path ?? '/',\n };\n if (c.meta?.secure !== undefined) cookie.secure = c.meta.secure;\n if (c.meta?.httpOnly !== undefined) cookie.httpOnly = c.meta.httpOnly;\n const expires = expiryToUnix(c.expiry);\n if (expires !== undefined) cookie.expires = expires;\n return cookie;\n}\n\n/** get-cookie reports expiry as a Date, unix-ish number, or \"Infinity\"; we need unix seconds. */\nfunction expiryToUnix(expiry: ExportedCookie['expiry']): number | undefined {\n if (expiry === undefined || expiry === 'Infinity') return undefined;\n if (expiry instanceof Date) return Math.floor(expiry.getTime() / 1000);\n // A number: already seconds if it looks like seconds, else milliseconds.\n return expiry > 1e12 ? Math.floor(expiry / 1000) : expiry;\n}\n","import { readFile } from 'node:fs/promises';\n\nimport { AuthError } from '@core/errors.js';\nimport type { Cookie } from '@core/types.js';\nimport type { CookieSource } from './CookieSource.js';\n\n/**\n * Reads cookies from a file exported by a browser extension — an alternative to\n * reading the browser's store directly ({@link BrowserCookieSource}) for users\n * who would rather export a file than grant cookie-store access.\n *\n * Auto-detects the two shapes such extensions emit:\n * - a **JSON array** (Cookie-Editor / EditThisCookie style), or\n * - the **Netscape `cookies.txt`** tab-delimited format (curl/wget style).\n */\nexport class FileCookieSource implements CookieSource {\n readonly browser: string;\n\n constructor(private readonly path: string) {\n this.browser = `file ${path}`;\n }\n\n async read(domainSuffix: string): Promise<Cookie[]> {\n let raw: string;\n try {\n raw = await readFile(this.path, 'utf8');\n } catch (e) {\n throw new AuthError(`could not read cookie file ${this.path}`, { cause: e });\n }\n\n const cookies = looksLikeJson(raw) ? parseJson(raw, this.path) : parseNetscape(raw);\n return cookies.filter((c) => hostMatches(c.domain, domainSuffix));\n }\n}\n\n/** A leading `[` or `{` (after whitespace) marks a JSON export; otherwise Netscape. */\nfunction looksLikeJson(raw: string): boolean {\n return /^\\s*[[{]/.test(raw);\n}\n\ninterface JsonCookie {\n name?: string;\n value?: string;\n domain?: string;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: string;\n /** Extensions name expiry either `expirationDate` (Cookie-Editor) or `expires`. */\n expirationDate?: number;\n expires?: number;\n}\n\nfunction parseJson(raw: string, path: string): Cookie[] {\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch (e) {\n throw new AuthError(`cookie file ${path} is not valid JSON`, { cause: e });\n }\n // Accept a bare array or a `{ cookies: [...] }` wrapper.\n const arr = Array.isArray(data)\n ? data\n : Array.isArray((data as { cookies?: unknown }).cookies)\n ? (data as { cookies: unknown[] }).cookies\n : null;\n if (!arr) {\n throw new AuthError(`cookie file ${path} is not a JSON cookie array`);\n }\n\n const cookies: Cookie[] = [];\n for (const entry of arr) {\n const c = entry as JsonCookie;\n if (!c.name || c.value === undefined || !c.domain) continue;\n const expires = c.expirationDate ?? c.expires;\n cookies.push({\n name: c.name,\n value: c.value,\n domain: c.domain,\n path: c.path ?? '/',\n secure: Boolean(c.secure),\n httpOnly: Boolean(c.httpOnly),\n sameSite: sameSiteOf(c.sameSite),\n ...(typeof expires === 'number' && expires > 0 ? { expires: Math.floor(expires) } : {}),\n });\n }\n return cookies;\n}\n\n/**\n * Netscape `cookies.txt`: tab-delimited\n * `domain includeSubdomains path secure expires name value`.\n * Lines starting with `#` are comments, except the `#HttpOnly_` prefix some\n * tools prepend to the domain field.\n */\nfunction parseNetscape(raw: string): Cookie[] {\n const cookies: Cookie[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#HttpOnly_'))) continue;\n\n const f = trimmed.split('\\t');\n if (f.length < 7) continue;\n\n let domain = f[0]!;\n let httpOnly = false;\n if (domain.startsWith('#HttpOnly_')) {\n domain = domain.slice('#HttpOnly_'.length);\n httpOnly = true;\n }\n\n const expires = Number(f[4]);\n const cookie: Cookie = {\n name: f[5]!,\n value: f[6]!,\n domain,\n path: f[2] ?? '/',\n secure: f[3]!.toUpperCase() === 'TRUE',\n httpOnly,\n sameSite: 'Lax',\n };\n if (Number.isFinite(expires) && expires > 0) cookie.expires = expires;\n cookies.push(cookie);\n }\n return cookies;\n}\n\n/** Map an extension's `sameSite` string to Playwright's enum; default `Lax`. */\nfunction sameSiteOf(v: string | undefined): 'Strict' | 'Lax' | 'None' {\n switch (v?.toLowerCase()) {\n case 'strict':\n return 'Strict';\n case 'no_restriction':\n case 'none':\n return 'None';\n default:\n return 'Lax';\n }\n}\n\n/** Whether a cookie's host belongs to `suffix` (exact or a subdomain). */\nfunction hostMatches(domain: string, suffix: string): boolean {\n const host = domain.replace(/^\\./, '').toLowerCase();\n const s = suffix.toLowerCase();\n return host === s || host.endsWith(`.${s}`);\n}\n","import { access, mkdir, rename, rm } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { basename, join } from 'node:path';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport { DownloadError, NetworkError } from '@core/errors.js';\nimport type { DownloadTarget } from '@core/types.js';\nimport type { BrowserSession } from '../browser/Browser.js';\nimport type { Downloader, DownloadProgress } from './Downloader.js';\n\n/**\n * Resolves a file's signed CDN URL through the browser session, then streams it\n * to disk **from Node** (the in-page fetch is blocked by CORS on the CDN host;\n * Node has no CORS restriction). Writes to a `.part` file and renames on\n * success; skips files that already exist.\n */\nexport class BrowserDownloader implements Downloader {\n async fetch(\n target: DownloadTarget,\n outDir: string,\n session: BrowserSession,\n onProgress?: (p: DownloadProgress) => void,\n signal?: AbortSignal,\n ): Promise<string> {\n signal?.throwIfAborted();\n await mkdir(outDir, { recursive: true });\n\n const resolved = await session.resolveDownloadUrl(target.url);\n\n // The signed CDN URL carries the real filename in its path.\n const name = filenameFrom(resolved.cdnUrl) ?? fallbackName(target);\n const finalPath = join(outDir, name);\n if (await exists(finalPath)) return finalPath; // already complete — skip.\n\n const partPath = `${finalPath}.part`;\n\n // The signed CDN URL self-authenticates via its query params. Only Nexus's\n // own hosts expect the session cookie; third-party CDNs reject the\n // unexpected jar with a 400, so send cookies only to nexusmods.com hosts.\n const onNexusHost = isNexusHost(resolved.cdnUrl);\n const headers: Record<string, string> = {\n 'user-agent': resolved.userAgent,\n referer: 'https://www.nexusmods.com/',\n };\n if (onNexusHost) headers.cookie = resolved.cookieHeader;\n\n let res: Response;\n try {\n res = await fetch(resolved.cdnUrl, { headers, ...(signal ? { signal } : {}) });\n } catch (e) {\n if (signal?.aborted) throw e; // propagate the AbortError untouched\n throw new NetworkError(`failed to fetch file ${target.fileId}`, { cause: e });\n }\n if (!res.ok || !res.body) {\n const host = hostOf(resolved.cdnUrl);\n throw new DownloadError(\n `download for file ${target.fileId} returned HTTP ${res.status} (cdn: ${host})`,\n );\n }\n\n const totalBytes = Number(res.headers.get('content-length') ?? 0);\n let receivedBytes = 0;\n const source = Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]);\n source.on('data', (chunk: Buffer) => {\n receivedBytes += chunk.length;\n onProgress?.({ receivedBytes, totalBytes });\n });\n\n try {\n // `pipeline` with `signal` aborts both streams and rejects with an\n // AbortError when the user cancels mid-transfer.\n await pipeline(source, createWriteStream(partPath), signal ? { signal } : {});\n } catch (e) {\n // Either way, the .part is incomplete — there is no resume — so remove it.\n await rm(partPath, { force: true });\n if (signal?.aborted) throw e; // propagate the AbortError untouched\n throw new DownloadError(`failed writing file ${target.fileId}`, { cause: e });\n }\n\n await rename(partPath, finalPath);\n return finalPath;\n }\n}\n\n/** The URL's hostname, or '' if unparseable. */\nfunction hostOf(url: string): string {\n try {\n return new URL(url).hostname;\n } catch {\n return '';\n }\n}\n\n/** Whether a URL is served from a nexusmods.com host (so the session cookie applies). */\nfunction isNexusHost(url: string): boolean {\n const host = hostOf(url);\n return host === 'nexusmods.com' || host.endsWith('.nexusmods.com');\n}\n\n/** Derive the filename from a signed CDN URL's path (decoded). */\nfunction filenameFrom(cdnUrl: string): string | null {\n try {\n const path = new URL(cdnUrl).pathname;\n const base = decodeURIComponent(basename(path));\n return base.length > 0 ? sanitize(base) : null;\n } catch {\n return null;\n }\n}\n\nfunction fallbackName(target: DownloadTarget): string {\n if (target.fileName) {\n const base = sanitize(target.fileName);\n return /\\.[a-z0-9]{2,4}$/i.test(base) ? base : `${base}-${target.fileId}`;\n }\n return `file-${target.fileId}`;\n}\n\nfunction sanitize(name: string): string {\n return name\n .replace(/[/\\\\?%*:|\"<>]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n","import { ScrapeError } from '@core/errors.js';\nimport type { CollectionMember, DownloadTarget, FileCategory, GameDomain } from '@core/types.js';\nimport type { JsonRequest, NexusSite } from './NexusSite.js';\n\nconst BASE = 'https://www.nexusmods.com';\nconst GRAPHQL_URL = 'https://api-router.nexusmods.com/graphql';\nconst SIGN_IN_HOST = 'users.nexusmods.com';\n\nconst COLLECTION_QUERY = `\n query CollectionRevisionMods($slug: String!, $viewAdultContent: Boolean = true) {\n collectionRevision(slug: $slug, viewAdultContent: $viewAdultContent) {\n modFiles {\n fileId\n optional\n file {\n name\n sizeInBytes\n mod {\n modId\n name\n game { domainName }\n }\n }\n }\n }\n }`;\n\n/**\n * Scrapes Nexus pages from raw HTML. Deliberately dependency-free (regex over\n * the markup) so it is fast and trivially unit-testable against fixtures.\n *\n * Selectors are isolated here; when the site drifts, only this file and the\n * fixtures change.\n */\nexport class NexusWebAdapter implements NexusSite {\n modFilesUrl(game: GameDomain, modId: number): string {\n return `${BASE}/${game}/mods/${modId}?tab=files`;\n }\n\n collectionUrl(game: GameDomain, ref: string): string {\n return `${BASE}/games/${game}/collections/${ref}/mods`;\n }\n\n collectionMembersQuery(_game: GameDomain, ref: string): JsonRequest {\n return {\n url: GRAPHQL_URL,\n body: {\n operationName: 'CollectionRevisionMods',\n query: COLLECTION_QUERY,\n variables: { slug: ref, viewAdultContent: true },\n },\n headers: { 'x-graphql-operationname': 'CollectionRevisionMods' },\n };\n }\n\n parseCollectionMembers(json: unknown): CollectionMember[] {\n const modFiles = (json as GqlResponse)?.data?.collectionRevision?.modFiles;\n if (!Array.isArray(modFiles)) {\n throw new ScrapeError('unexpected collection response shape');\n }\n\n const members: CollectionMember[] = [];\n for (const entry of modFiles) {\n const mod = entry.file?.mod;\n const modId = mod?.modId;\n const game = mod?.game?.domainName;\n const fileId = entry.fileId;\n if (!modId || !game || !fileId) continue;\n const sizeBytes = Number(entry.file?.sizeInBytes);\n members.push({\n game,\n modId,\n fileId,\n optional: Boolean(entry.optional),\n ...(entry.file?.name ? { name: entry.file.name } : {}),\n ...(Number.isFinite(sizeBytes) && sizeBytes > 0 ? { sizeBytes } : {}),\n });\n }\n\n if (members.length === 0) {\n throw new ScrapeError('collection has no downloadable files');\n }\n return members;\n }\n\n fileDownloadUrl(game: GameDomain, modId: number, fileId: number): string {\n return `${BASE}/${game}/mods/${modId}?tab=files&file_id=${fileId}`;\n }\n\n parseDownloadTargets(html: string): DownloadTarget[] {\n const base = modBaseUrl(html);\n const targets: DownloadTarget[] = [];\n\n // Each file is a `<dt id=\"file-expander-header-<id>\" data-name=\"..\"\n // data-version=\"..\">` row inside its category section. The `data-id` is the\n // Nexus file id; the actual download href is constructed from it (the file\n // list's download links are otherwise hydrated client-side and absent from\n // static HTML). We segment by category header, then read these rows.\n for (const segment of segmentByCategory(html)) {\n // Match the whole expander-header tag, then read attributes from it\n // (order-independent — Nexus emits id/class/data-* in varying orders).\n const tagRe = /<dt[^>]*id=\"file-expander-header-(\\d+)\"[^>]*>/gi;\n let m: RegExpExecArray | null;\n while ((m = tagRe.exec(segment.body)) !== null) {\n const fileId = Number(m[1]);\n if (!Number.isFinite(fileId)) continue;\n const tag = m[0];\n const name = (/data-name=\"([^\"]*)\"/i.exec(tag)?.[1] ?? '').trim();\n const version = (/data-version=\"([^\"]*)\"/i.exec(tag)?.[1] ?? '').trim();\n const fileName = name ? (version ? `${name} ${version}` : name) : undefined;\n targets.push({\n url: downloadUrl(base, fileId),\n fileId,\n category: segment.category,\n ...(fileName ? { fileName } : {}),\n });\n }\n }\n\n return targets;\n }\n\n isAuthRedirect(landedUrl: string): boolean {\n // An expired/invalid session is bounced to the sign-in host. Compare the\n // host exactly (a substring match would also flag the main site's own\n // `users.nexusmods.com`-less paths inconsistently).\n try {\n return new URL(landedUrl).host === SIGN_IN_HOST;\n } catch {\n return false;\n }\n }\n}\n\ninterface GqlResponse {\n data?: {\n collectionRevision?: {\n modFiles?: {\n fileId?: number;\n optional?: boolean;\n file?: {\n name?: string;\n sizeInBytes?: string;\n mod?: {\n modId?: number;\n name?: string;\n game?: { domainName?: GameDomain };\n };\n };\n }[];\n };\n };\n}\n\ninterface Segment {\n category: FileCategory;\n body: string;\n}\n\n/**\n * Split the files page into per-category segments. The live Nexus page groups\n * files into containers identified by `id=\"file-container-<cat>-files\"` (e.g.\n * `main-files`, `optional-files`, `old-files`, `miscellaneous-files`). As a\n * fallback (and for fixtures), legacy `<h3>Main files</h3>`-style headers are\n * also recognised. Content before the first marker is treated as `unknown`.\n */\nfunction segmentByCategory(html: string): Segment[] {\n const markerRe =\n // 1) Live container ids: id=\"file-container-main-files\"\n // 2) Legacy text headers: <h3>Main files</h3> / \"Old versions\"\n /id=\"file-container-(main|optional|old|miscellaneous)-files\"|<(?:h[1-6]|div|dt)[^>]*>\\s*((?:main|optional|old|miscellaneous)[^<]*?files?|old versions?)\\s*<\\/(?:h[1-6]|div|dt)>/gi;\n\n const marks: { index: number; category: FileCategory }[] = [];\n let m: RegExpExecArray | null;\n while ((m = markerRe.exec(html)) !== null) {\n const category = m[1] ? categoryOf(m[1]) : categoryOf(m[2] ?? '');\n marks.push({ index: m.index, category });\n }\n\n if (marks.length === 0) {\n return [{ category: 'unknown', body: html }];\n }\n\n const segments: Segment[] = [];\n for (let i = 0; i < marks.length; i++) {\n const start = marks[i]!.index;\n const end = i + 1 < marks.length ? marks[i + 1]!.index : html.length;\n segments.push({ category: marks[i]!.category, body: html.slice(start, end) });\n }\n return segments;\n}\n\nfunction categoryOf(label: string): FileCategory {\n const h = label.toLowerCase();\n if (h.startsWith('main')) return 'main';\n if (h.startsWith('optional')) return 'optional';\n if (h.startsWith('old')) return 'old';\n if (h.startsWith('misc')) return 'miscellaneous';\n return 'unknown';\n}\n\n/** Extract the `https://.../<game>/mods/<id>` base from the page's og:url. */\nfunction modBaseUrl(html: string): string {\n const og = /property=\"og:url\"\\s+content=\"([^\"]+)\"/i.exec(html);\n if (og?.[1]) return og[1].replace(/[?#].*$/, '');\n const path = /\\/[a-z0-9]+\\/mods\\/\\d+/i.exec(html);\n return path ? `${BASE}${path[0]}` : BASE;\n}\n\n/**\n * Build the \"Manual download\" URL for a file id (no `nmm=1` — that one hands\n * off to a mod manager). This page presents the free slow-download button.\n */\nfunction downloadUrl(base: string, fileId: number): string {\n return `${base}?tab=files&file_id=${fileId}`;\n}\n","import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\nimport { sessionFile } from '@config/paths.js';\nimport type { Session } from '@core/types.js';\nimport type { SessionStore } from './SessionStore.js';\n\n/** Stores the session as a `600`-mode JSON file in the OS config dir. */\nexport class FileSessionStore implements SessionStore {\n constructor(private readonly path: string = sessionFile()) {}\n\n async save(s: Session): Promise<void> {\n await mkdir(dirname(this.path), { recursive: true });\n await writeFile(this.path, JSON.stringify(s, null, 2), { mode: 0o600 });\n }\n\n async load(): Promise<Session | null> {\n let raw: string;\n try {\n raw = await readFile(this.path, 'utf8');\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw e;\n }\n const parsed = JSON.parse(raw) as Session;\n if (!parsed.username || !parsed.cookies) return null;\n return parsed;\n }\n\n async clear(): Promise<void> {\n await rm(this.path, { force: true });\n }\n}\n","import { CamoufoxBrowser } from '@adapters/browser/CamoufoxBrowser.js';\nimport { BrowserCookieSource } from '@adapters/cookies/BrowserCookieSource.js';\nimport type { CookieSource } from '@adapters/cookies/CookieSource.js';\nimport { FileCookieSource } from '@adapters/cookies/FileCookieSource.js';\nimport { BrowserDownloader } from '@adapters/download/BrowserDownloader.js';\nimport { NexusWebAdapter } from '@adapters/nexus/NexusWebAdapter.js';\nimport { FileSessionStore } from '@adapters/session/FileSessionStore.js';\n\n/**\n * Composition root: the only place concrete adapters are constructed. Commands\n * receive these via their handlers; the app layer never imports them.\n */\nexport function buildDeps() {\n return {\n browser: new CamoufoxBrowser(),\n store: new FileSessionStore(),\n site: new NexusWebAdapter(),\n downloader: new BrowserDownloader(),\n };\n}\n\nexport type Deps = ReturnType<typeof buildDeps>;\n\n/** Domain suffix for Nexus cookies. */\nexport const NEXUS_COOKIE_DOMAIN = 'nexusmods.com';\n\n/** Resolve a cookie source by browser name. */\nexport function cookieSourceFor(browser: string): CookieSource {\n return new BrowserCookieSource(browser);\n}\n\n/** Resolve a cookie source from an exported cookie file (--file). */\nexport function fileCookieSource(path: string): CookieSource {\n return new FileCookieSource(path);\n}\n","import ora, { type Ora } from 'ora';\nimport type { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';\n\nimport { downloadCollection } from '@app/downloadCollection.js';\nimport { downloadMod } from '@app/downloadMod.js';\nimport { restoreSession } from '@app/restoreSession.js';\nimport { defaultOutDir } from '@config/paths.js';\nimport { basename } from 'node:path';\nimport { AuthError, CancelError, isCancel } from '@core/errors.js';\nimport { type DownloadReport, type ModResult, summarize } from '@core/types.js';\nimport { parseNexusUrl } from '@adapters/nexus/parseNexusUrl.js';\nimport { out } from '../output.js';\nimport { buildDeps } from '../wiring.js';\n\ninterface DownloadArgs {\n target?: string;\n game: string;\n mod?: number;\n collection?: string;\n out?: string;\n concurrency: number;\n 'dry-run': boolean;\n optional: boolean;\n headful: boolean;\n verbose: boolean;\n}\n\nconst RETRY_ATTEMPTS = 3;\nconst RETRY_BASE_DELAY_MS = 1_000;\n\nexport const downloadCommand: CommandModule = {\n command: 'download [target]',\n describe: 'Download a mod or a collection',\n builder: (y: Argv) =>\n y\n .positional('target', {\n type: 'string',\n describe: 'A nexusmods.com mod or collection URL (or use --game with --mod/--collection)',\n })\n .option('game', {\n type: 'string',\n describe: 'Nexus game domain (e.g. skyrimspecialedition)',\n })\n .option('mod', { type: 'number', describe: 'Numeric mod id' })\n .option('collection', { type: 'string', describe: 'Collection slug or id' })\n .option('out', { type: 'string', describe: 'Output directory' })\n .option('concurrency', { type: 'number', default: 2 })\n .option('dry-run', {\n type: 'boolean',\n default: false,\n describe: 'List what would be downloaded without fetching',\n })\n .option('optional', {\n type: 'boolean',\n default: false,\n describe: 'For collections, also download files marked optional',\n })\n .option('headful', {\n type: 'boolean',\n default: false,\n describe: 'Show the browser window (useful for debugging)',\n })\n .conflicts('mod', 'collection')\n .check((argv) => {\n // A URL positional supplies game + mod/collection; resolve it into the\n // same fields the flags use so the handler reads one shape.\n if (typeof argv.target === 'string') {\n const ref = parseNexusUrl(argv.target);\n if (!ref) {\n throw new Error(`not a recognised Nexus mod or collection URL: ${argv.target}`);\n }\n argv.game = ref.game;\n if ('modId' in ref) argv.mod = ref.modId;\n else argv.collection = ref.collection;\n }\n if (argv.game === undefined) {\n throw new Error('provide a Nexus URL, or --game with --mod/--collection');\n }\n if (argv.mod === undefined && argv.collection === undefined) {\n throw new Error('provide a Nexus URL, or --mod / --collection');\n }\n return true;\n }),\n handler: async (raw: ArgumentsCamelCase) => {\n const argv = raw as unknown as DownloadArgs;\n const outDir = argv.out ?? defaultOutDir(argv.game);\n const deps = buildDeps();\n\n // One spinner spans the whole run: the (slow) session restore + Cloudflare\n // warm-up, then per-file progress. ora auto-disables on non-TTY.\n //\n // discardStdin:false is REQUIRED: ora's default stdin discarder puts the TTY\n // in raw mode, which disables the terminal's SIGINT generation — Ctrl+C then\n // never reaches our handler and the run can't be cancelled.\n const spinner = ora({ text: 'Restoring session…', discardStdin: false }).start();\n\n // Ctrl+C: first press aborts gracefully (stop new work, abort the in-flight\n // file, close the browser, print a summary); a second press hard-exits in\n // case cleanup hangs. The handler is removed in `finally`.\n const controller = new AbortController();\n const onSigint = (): void => {\n if (controller.signal.aborted) {\n spinner.stop();\n out.warn('forced quit');\n process.exit(130);\n }\n controller.abort(new CancelError('cancelled by user'));\n spinner.text = 'Cancelling… (press Ctrl+C again to force quit)';\n };\n process.on('SIGINT', onSigint);\n\n let session;\n try {\n session = await restoreSession(deps, argv.headful);\n } catch (e) {\n spinner.stop();\n process.removeListener('SIGINT', onSigint);\n if (isCancel(e) || controller.signal.aborted) {\n out.warn('cancelled');\n process.exitCode = 130;\n return;\n }\n out.error(e, argv.verbose);\n process.exitCode = e instanceof AuthError ? 2 : 1;\n return;\n }\n\n // Tick once a second, independent of download events — ora only re-renders\n // text we reassign, so without this the run-elapsed clock and the per-file\n // elapsed/speed/ETA would freeze between byte/file callbacks.\n const runStart = Date.now();\n const progress = new FileProgress();\n const global = argv.collection !== undefined ? new GlobalProgress() : null;\n const ticker = setInterval(() => {\n if (argv['dry-run']) return;\n spinner.text = composeStatus(progress, global, Date.now() - runStart);\n }, 1000);\n if (typeof ticker.unref === 'function') ticker.unref();\n\n const signal = controller.signal;\n try {\n const report =\n argv.collection !== undefined\n ? await runCollection(\n deps,\n session,\n argv,\n outDir,\n spinner,\n progress,\n global!,\n runStart,\n signal,\n )\n : await runMod(deps, session, argv, outDir, spinner, progress, runStart, signal);\n\n clearInterval(ticker);\n finish(spinner, report, argv['dry-run'], Date.now() - runStart);\n process.exitCode = report.failed > 0 ? 1 : 0;\n } catch (e) {\n clearInterval(ticker);\n spinner.stop();\n if (isCancel(e) || signal.aborted) {\n out.warn('cancelled — partial downloads removed');\n process.exitCode = 130;\n } else {\n out.error(e, argv.verbose);\n process.exitCode = e instanceof AuthError ? 2 : 1;\n }\n } finally {\n process.removeListener('SIGINT', onSigint);\n await session.close();\n }\n },\n};\n\nasync function runMod(\n deps: ReturnType<typeof buildDeps>,\n session: Awaited<ReturnType<typeof restoreSession>>,\n argv: DownloadArgs,\n outDir: string,\n spinner: Ora,\n progress: FileProgress,\n runStart: number,\n signal: AbortSignal,\n): Promise<DownloadReport> {\n const verb = argv['dry-run'] ? 'Resolving' : 'Downloading';\n progress.start(`${verb} mod ${argv.mod}`);\n spinner.text = progress.render();\n const result: ModResult = await downloadMod(deps, session, {\n game: argv.game,\n modId: argv.mod!,\n outDir,\n dryRun: argv['dry-run'],\n retryAttempts: RETRY_ATTEMPTS,\n retryBaseDelayMs: RETRY_BASE_DELAY_MS,\n signal,\n onFileProgress: (p) => {\n progress.update(p.receivedBytes, p.totalBytes);\n spinner.text = composeStatus(progress, null, Date.now() - runStart);\n },\n });\n progress.done();\n return summarize([result]);\n}\n\nasync function runCollection(\n deps: ReturnType<typeof buildDeps>,\n session: Awaited<ReturnType<typeof restoreSession>>,\n argv: DownloadArgs,\n outDir: string,\n spinner: Ora,\n progress: FileProgress,\n global: GlobalProgress,\n runStart: number,\n signal: AbortSignal,\n): Promise<DownloadReport> {\n spinner.text = 'Resolving collection…';\n return downloadCollection(deps, session, {\n game: argv.game,\n ref: argv.collection!,\n outDir,\n concurrency: argv.concurrency,\n dryRun: argv['dry-run'],\n includeOptional: argv.optional,\n retryAttempts: RETRY_ATTEMPTS,\n retryBaseDelayMs: RETRY_BASE_DELAY_MS,\n signal,\n onResolved: (members) => {\n global.setTotal(members);\n global.begin();\n },\n onStart: (member, i, total) => {\n const name = member.name ?? `mod ${member.modId}`;\n const verb = argv['dry-run'] ? 'Resolving' : 'Downloading';\n progress.start(`[${i}/${total}] ${verb} ${name}`);\n global.startFile(member.sizeBytes ?? 0);\n spinner.text = composeStatus(progress, global, Date.now() - runStart);\n },\n onFileProgress: (p) => {\n progress.update(p.receivedBytes, p.totalBytes);\n global.setCurrent(p.receivedBytes);\n spinner.text = composeStatus(progress, global, Date.now() - runStart);\n },\n // Failures persist a line above the spinner.\n onProgress: (r) => {\n progress.done();\n global.completeFile(r.ok);\n if (!r.ok) {\n const text = `mod ${r.modId} failed: ${r.error}`;\n spinner.stopAndPersist({ symbol: '✗', text });\n spinner.start();\n }\n },\n });\n}\n\n/** Stop the spinner with a final summary line and (for dry-run) the listing. */\nfunction finish(spinner: Ora, report: DownloadReport, dryRun: boolean, elapsedMs: number): void {\n if (dryRun) {\n spinner.stop();\n for (const r of report.results) {\n out.info(`mod ${r.modId}: ${r.files.join(', ') || '(none)'}`);\n }\n out.info(`dry run — ${report.succeeded} resolvable, ${report.failed} not`);\n return;\n }\n\n const took = `in ${clock(elapsedMs / 1000)}`;\n\n // For a single mod, list the files written; for a collection, just the tally.\n if (report.results.length === 1 && report.results[0]?.ok) {\n const files = report.results[0].files.map((f) => basename(f));\n spinner.succeed(`Downloaded ${files.join(', ')} ${took}`);\n return;\n }\n\n const msg = `${report.succeeded} downloaded, ${report.failed} failed ${took}`;\n if (report.failed > 0) spinner.warn(msg);\n else spinner.succeed(msg);\n}\n\n/**\n * Tracks a whole collection's transfer for a global ETA: known total bytes\n * (summed from the API's reported file sizes) against bytes downloaded so far\n * (completed files + the in-flight file). ETA uses the overall average rate.\n */\nclass GlobalProgress {\n private startedAt = Date.now();\n private totalBytes = 0;\n private completedBytes = 0;\n private currentBytes = 0;\n /** The in-flight file's known size from the API (for skipped files). */\n private currentSize = 0;\n\n /** Set the known total from the resolved member list. */\n setTotal(members: { sizeBytes?: number }[]): void {\n this.totalBytes = members.reduce((sum, m) => sum + (m.sizeBytes ?? 0), 0);\n }\n\n /** Begin the timer once downloading actually starts. */\n begin(): void {\n this.startedAt = Date.now();\n }\n\n /** Note the file about to download and its known size. */\n startFile(sizeBytes: number): void {\n this.currentBytes = 0;\n this.currentSize = sizeBytes;\n }\n\n /** Update the in-flight file's streamed byte count. */\n setCurrent(bytes: number): void {\n this.currentBytes = bytes;\n }\n\n /**\n * Settle the in-flight file. On success, credit its bytes toward the total —\n * the streamed amount, or the known size when nothing streamed (a file that\n * already existed and was skipped). On failure, discard it: a file that\n * errored out must not count as downloaded.\n */\n completeFile(ok: boolean): void {\n if (ok) {\n this.completedBytes += Math.max(this.currentBytes, this.currentSize);\n }\n this.currentBytes = 0;\n this.currentSize = 0;\n }\n\n /**\n * Render the secondary line:\n * total 18% 240 MB/1.3 GB elapsed 02:15 ETA 48:30\n * `elapsedMs` is the whole-run elapsed (so both ETAs share one clock source).\n */\n render(runElapsedMs: number): string {\n if (this.totalBytes <= 0) return '';\n const done = this.completedBytes + this.currentBytes;\n const elapsedMs = Math.max(Date.now() - this.startedAt, 1);\n const rate = done / (elapsedMs / 1000);\n const pct = Math.floor((done / this.totalBytes) * 100);\n\n const cols = [\n 'total',\n `${pct}%`,\n `${size(done)}/${size(this.totalBytes)}`,\n `elapsed ${clock(runElapsedMs / 1000)}`,\n ];\n if (rate > 0 && done > 0) {\n cols.push(`ETA ${clock((this.totalBytes - done) / rate)}`);\n }\n return cols.join(' ');\n }\n}\n\n/**\n * Tracks one file's transfer and renders the primary status line:\n * [3/476] SkyUI 47% 1.2/2.6 MB 3.4 MB/s ETA 00:04\n * Speed/ETA use the overall average (received ÷ elapsed) — stable, unlike\n * instantaneous chunk-to-chunk rates.\n */\nclass FileProgress {\n private startedAt = 0;\n private received = 0;\n private total = 0;\n private label = '';\n private active = false;\n\n /** Reset for a new file. `label` is the position + name, e.g. \"[3/476] SkyUI\". */\n start(label: string): void {\n this.startedAt = Date.now();\n this.received = 0;\n this.total = 0;\n this.label = label;\n this.active = true;\n }\n\n /** Record the latest byte counts (called from the download callback). */\n update(received: number, total: number): void {\n this.received = received;\n this.total = total;\n this.active = true;\n }\n\n /** Render the file line against *now* so elapsed/speed/ETA advance live. */\n render(): string {\n if (!this.label) return '';\n if (!this.active || this.received === 0) return this.label; // resolving…\n\n const elapsedMs = Math.max(Date.now() - this.startedAt, 1);\n const rate = this.received / (elapsedMs / 1000);\n const cols: string[] = [this.label];\n\n if (this.total > 0) {\n cols.push(`${Math.floor((this.received / this.total) * 100)}%`);\n cols.push(`${size(this.received)}/${size(this.total)}`);\n } else {\n cols.push(size(this.received));\n }\n cols.push(`${size(rate)}/s`);\n if (this.total > 0 && rate > 0) {\n cols.push(`ETA ${clock((this.total - this.received) / rate)}`);\n }\n return cols.join(' ');\n }\n\n /** Mark the file finished so the ticker stops rendering its line. */\n done(): void {\n this.active = false;\n }\n}\n\n/**\n * Compose the spinner status: the file line, and (for collections) a second\n * \"total\" line below it. The spinner glyph prefixes the first line.\n */\nfunction composeStatus(\n progress: FileProgress,\n global: GlobalProgress | null,\n runElapsedMs: number,\n): string {\n const fileLine = progress.render();\n const totalLine = global?.render(runElapsedMs) ?? '';\n // Indent the second line to align under the first (past the spinner glyph).\n return totalLine ? `${fileLine}\\n ${totalLine}` : fileLine;\n}\n\n/** Auto-scale bytes to KB/MB/GB with one decimal, e.g. \"1.3 GB\". */\nfunction size(bytes: number): string {\n if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;\n if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;\n if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${Math.round(bytes)} B`;\n}\n\n/** Format seconds as zero-padded HH:MM:SS. */\nfunction clock(seconds: number): string {\n const s = Math.max(0, Math.floor(seconds));\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const sec = s % 60;\n return [h, m, sec].map((n) => String(n).padStart(2, '0')).join(':');\n}\n","import type { Browser } from '@adapters/browser/Browser.js';\nimport type { CookieSource } from '@adapters/cookies/CookieSource.js';\nimport type { SessionStore } from '@adapters/session/SessionStore.js';\nimport { AuthError } from '@core/errors.js';\nimport type { Session } from '@core/types.js';\n\nexport interface ImportDeps {\n source: CookieSource;\n browser: Browser;\n store: SessionStore;\n}\n\nexport interface ImportParams {\n /** Cookie host suffix to import (e.g. `nexusmods.com`). */\n domainSuffix: string;\n /** Validate the cookies authenticate before saving. */\n validate: boolean;\n}\n\n/**\n * Import Nexus cookies from the user's real browser, optionally verify they\n * authenticate, and persist them as the session. No interactive login — the\n * user already cleared Cloudflare + signed in via their normal browser.\n */\nexport async function importSession(deps: ImportDeps, params: ImportParams): Promise<Session> {\n const cookies = await deps.source.read(params.domainSuffix);\n if (cookies.length === 0) {\n throw new AuthError(\n `no ${params.domainSuffix} cookies found in ${deps.source.browser} — ` +\n 'log into Nexus there first',\n );\n }\n\n let username = 'nexus-user';\n if (params.validate) {\n const session = await deps.browser.launch({ headful: false });\n try {\n await session.setCookies(cookies);\n if (!(await session.isLoggedIn())) {\n throw new AuthError(`imported ${deps.source.browser} cookies are not logged in to Nexus`);\n }\n username = (await session.resolveUsername()) ?? username;\n } finally {\n await session.close();\n }\n }\n\n const saved: Session = {\n username,\n cookies,\n capturedAt: new Date().toISOString(),\n };\n await deps.store.save(saved);\n return saved;\n}\n","import type { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';\n\nimport { importSession } from '@app/importSession.js';\nimport { out } from '../output.js';\nimport { buildDeps, cookieSourceFor, fileCookieSource, NEXUS_COOKIE_DOMAIN } from '../wiring.js';\n\ninterface ImportArgs {\n from?: string;\n file?: string;\n validate: boolean;\n verbose: boolean;\n}\n\nexport const importCommand: CommandModule = {\n command: 'import',\n describe: 'Import Nexus cookies from your existing browser session',\n builder: (y: Argv) =>\n y\n // No yargs `default` here: a default value would always count as \"set\"\n // and trip `.conflicts('file', 'from')`, making `--file` unusable. The\n // chrome fallback is applied in the handler instead.\n .option('from', {\n type: 'string',\n describe:\n 'Browser to import cookies from: chrome (default), brave, edge, opera, vivaldi, arc, firefox, safari',\n })\n .option('file', {\n type: 'string',\n describe: 'Import from an exported cookie file (cookies.txt or JSON) instead of a browser',\n })\n .conflicts('file', 'from')\n .option('validate', {\n type: 'boolean',\n default: true,\n describe: 'Open a headless browser to confirm the cookies are logged in',\n }),\n handler: async (raw: ArgumentsCamelCase) => {\n const argv = raw as unknown as ImportArgs;\n const { browser, store } = buildDeps();\n try {\n const source = argv.file\n ? fileCookieSource(argv.file)\n : cookieSourceFor(argv.from ?? 'chrome');\n const session = await importSession(\n { source, browser, store },\n { domainSuffix: NEXUS_COOKIE_DOMAIN, validate: argv.validate },\n );\n out.success(\n `imported ${session.cookies.length} cookie(s) from ${source.browser}` +\n (session.username !== 'nexus-user' ? ` for ${session.username}` : ''),\n );\n process.exitCode = 0;\n } catch (e) {\n out.error(e, argv.verbose);\n process.exitCode = 1;\n }\n },\n};\n","import type { CommandModule } from 'yargs';\n\nimport { out } from '../output.js';\nimport { buildDeps } from '../wiring.js';\n\nexport const logoutCommand: CommandModule = {\n command: 'logout',\n describe: 'Clear the imported session',\n handler: async () => {\n const { store } = buildDeps();\n const existing = await store.load();\n await store.clear();\n if (existing) {\n out.success(`logged out (${existing.username})`);\n } else {\n out.info('no session to clear');\n }\n process.exitCode = 0;\n },\n};\n","import yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { downloadCommand } from './commands/download.js';\nimport { importCommand } from './commands/import.js';\nimport { logoutCommand } from './commands/logout.js';\n\nawait yargs(hideBin(process.argv))\n .scriptName('nexus')\n .usage('$0 <command> [options]')\n .option('verbose', {\n type: 'boolean',\n default: false,\n describe: 'Print full stack traces on error',\n global: true,\n })\n .command(importCommand)\n .command(logoutCommand)\n .command(downloadCommand)\n .demandCommand(1, 'a command is required')\n .strict()\n .help()\n .parseAsync();\n"],"names":["defaultSleep","SIGN_IN_HOST","url","body","headers"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/core/errors.ts","../../src/core/types.ts","../../src/app/backoff.ts","../../src/app/retry.ts","../../src/app/downloadCollection.ts","../../src/app/downloadMod.ts","../../src/app/restoreSession.ts","../../src/config/paths.ts","../../src/adapters/nexus/parseNexusUrl.ts","../../src/cli/output.ts","../../src/adapters/browser/CamoufoxBrowser.ts","../../src/adapters/cookies/BrowserCookieSource.ts","../../src/adapters/cookies/FileCookieSource.ts","../../src/adapters/download/BrowserDownloader.ts","../../src/adapters/nexus/NexusWebAdapter.ts","../../src/adapters/session/FileSessionStore.ts","../../src/cli/wiring.ts","../../src/cli/commands/download.ts","../../src/app/importSession.ts","../../src/cli/commands/import.ts","../../src/cli/commands/logout.ts","../../src/cli/index.ts"],"sourcesContent":["/**\n * Typed, recoverable errors. The CLI renders these as concise one-line\n * messages (no stack trace unless --verbose).\n */\n\nexport abstract class NexusError extends Error {\n abstract readonly kind: string;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options as ErrorOptions);\n this.name = new.target.name;\n }\n}\n\n/** Session is missing, invalid, or expired; the user must re-login. */\nexport class AuthError extends NexusError {\n readonly kind = 'auth';\n}\n\n/** A page could not be parsed into the expected domain shape. */\nexport class ScrapeError extends NexusError {\n readonly kind = 'scrape';\n}\n\n/** A file download failed after exhausting retries. */\nexport class DownloadError extends NexusError {\n readonly kind = 'download';\n}\n\n/** A transient network/transport failure. */\nexport class NetworkError extends NexusError {\n readonly kind = 'network';\n}\n\n/** The site is signalling throttling (HTTP 429 / Cloudflare challenge). */\nexport class ThrottleError extends NexusError {\n readonly kind = 'throttle';\n}\n\n/** The run was cancelled by the user (Ctrl+C). Maps to exit code 130. */\nexport class CancelError extends NexusError {\n readonly kind = 'cancel';\n}\n\n/**\n * Whether an error represents user cancellation — either our own\n * {@link CancelError} or the `AbortError` a DOMException-style abort throws.\n */\nexport function isCancel(e: unknown): boolean {\n return e instanceof CancelError || (e instanceof Error && e.name === 'AbortError');\n}\n\nexport function isNexusError(e: unknown): e is NexusError {\n return e instanceof NexusError;\n}\n\nexport function messageOf(e: unknown): string {\n if (e instanceof Error) return e.message;\n return String(e);\n}\n","/**\n * Pure domain types. No I/O, no dependency on adapters.\n */\n\n/** A Nexus game domain slug, e.g. `skyrimspecialedition`. */\nexport type GameDomain = string;\n\n/** A browser cookie, in the shape Playwright round-trips. */\nexport interface Cookie {\n name: string;\n value: string;\n domain: string;\n path: string;\n expires?: number;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Strict' | 'Lax' | 'None';\n}\n\n/**\n * A persisted, reusable authenticated session — cookies imported from the\n * user's real browser (which has already cleared Cloudflare and logged in).\n */\nexport interface Session {\n /** Nexus username, when resolvable from the imported data. */\n username: string;\n /** Imported Nexus cookies, replayed into a headless context for downloads. */\n cookies: Cookie[];\n /** ISO-8601 timestamp of when the cookies were imported. */\n capturedAt: string;\n}\n\n/** A single downloadable file resolved from a mod's files page. */\nexport interface DownloadTarget {\n /** The (possibly relative) URL that initiates the download. */\n url: string;\n /** Nexus file id, used for fallback naming. */\n fileId: number;\n /** Human-readable file name from the page, if known. */\n fileName?: string;\n /** Nexus file category. Only `main` files are downloaded by default. */\n category: FileCategory;\n}\n\nexport type FileCategory = 'main' | 'optional' | 'miscellaneous' | 'old' | 'unknown';\n\n/** A mod, identified within a game domain. */\nexport interface Mod {\n game: GameDomain;\n modId: number;\n /** Mod display name, when resolved. */\n name?: string;\n}\n\n/**\n * A member of a collection — one specific file the collection curates. A\n * collection pins exact files (by `fileId`), not just mods, and flags some as\n * optional.\n */\nexport interface CollectionMember {\n game: GameDomain;\n modId: number;\n /** The specific file the collection pins for this mod. */\n fileId: number;\n /** Whether the collection marks this file optional. */\n optional: boolean;\n /** File / mod display name, when available. */\n name?: string;\n /** File size in bytes, when the API reports it (for a global ETA). */\n sizeBytes?: number;\n}\n\n/** A collection, identified by slug or numeric id within a game domain. */\nexport interface Collection {\n game: GameDomain;\n /** The slug or id as supplied by the user. */\n ref: string;\n members: CollectionMember[];\n}\n\n/** Outcome of attempting to download one mod. */\nexport interface ModResult {\n modId: number;\n ok: boolean;\n /** Paths of files written for this mod (on success). */\n files: string[];\n /** Error message (on failure). */\n error?: string;\n /** Set when the failure was attributed to site throttling. */\n throttled?: boolean;\n}\n\n/** Aggregate result of a download run (single mod or collection). */\nexport interface DownloadReport {\n results: ModResult[];\n succeeded: number;\n failed: number;\n}\n\nexport function summarize(results: ModResult[]): DownloadReport {\n return {\n results,\n succeeded: results.filter((r) => r.ok).length,\n failed: results.filter((r) => !r.ok).length,\n };\n}\n","/**\n * Adaptive pacing for collection downloads. Pure, side-effect-free state\n * machine so it can be unit-tested without a browser.\n *\n * Starts fast (no delay, full concurrency). On a throttle signal it inserts\n * and progressively grows an inter-mod delay and reduces effective\n * concurrency. After a run of clean successes it relaxes back toward fast.\n */\n\nexport interface BackoffConfig {\n /** Configured ceiling for concurrency (the user's --concurrency). */\n maxConcurrency: number;\n /** Delay added on the first throttle signal (ms). */\n baseDelayMs: number;\n /** Hard cap on inter-mod delay (ms). */\n maxDelayMs: number;\n /** Clean successes required before relaxing one step. */\n relaxAfter: number;\n}\n\nexport const DEFAULT_BACKOFF: Omit<BackoffConfig, 'maxConcurrency'> = {\n baseDelayMs: 2_000,\n maxDelayMs: 60_000,\n relaxAfter: 5,\n};\n\nexport class BackoffPolicy {\n private delayMs = 0;\n private concurrency: number;\n private cleanStreak = 0;\n private readonly cfg: BackoffConfig;\n\n constructor(cfg: BackoffConfig) {\n this.cfg = cfg;\n this.concurrency = cfg.maxConcurrency;\n }\n\n /** Current inter-mod delay to wait before starting the next member. */\n get currentDelayMs(): number {\n return this.delayMs;\n }\n\n /** Current effective concurrency. */\n get currentConcurrency(): number {\n return this.concurrency;\n }\n\n /** Record a throttle signal (429 / Cloudflare / repeated timeout). */\n onThrottle(): void {\n this.cleanStreak = 0;\n this.delayMs =\n this.delayMs === 0 ? this.cfg.baseDelayMs : Math.min(this.delayMs * 2, this.cfg.maxDelayMs);\n this.concurrency = Math.max(1, this.concurrency - 1);\n }\n\n /** Record a clean success; may relax pacing after enough in a row. */\n onSuccess(): void {\n this.cleanStreak += 1;\n if (this.cleanStreak < this.cfg.relaxAfter) return;\n this.cleanStreak = 0;\n\n if (this.concurrency < this.cfg.maxConcurrency) {\n this.concurrency += 1;\n } else if (this.delayMs > 0) {\n this.delayMs = Math.floor(this.delayMs / 2);\n if (this.delayMs < this.cfg.baseDelayMs / 2) this.delayMs = 0;\n }\n }\n}\n","import { isCancel, ThrottleError } from '@core/errors.js';\n\nexport interface RetryOptions {\n attempts: number;\n baseDelayMs: number;\n /** Injected sleep, for deterministic tests. */\n sleep?: (ms: number) => Promise<void>;\n /** When aborted, stop retrying and re-throw immediately. */\n signal?: AbortSignal;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** Detect whether an error indicates site-side throttling. */\nexport function isThrottle(e: unknown): boolean {\n if (e instanceof ThrottleError) return true;\n const msg = (e instanceof Error ? e.message : String(e)).toLowerCase();\n return (\n msg.includes('429') ||\n msg.includes('too many requests') ||\n msg.includes('cloudflare') ||\n msg.includes('just a moment') ||\n msg.includes('timeout')\n );\n}\n\n/**\n * Run `fn` with exponential backoff. Re-throws the last error if all fail.\n * A cancellation (abort) is never retried — it short-circuits immediately.\n */\nexport async function withRetry<T>(fn: () => Promise<T>, opts: RetryOptions): Promise<T> {\n const sleep = opts.sleep ?? defaultSleep;\n let lastErr: unknown;\n for (let attempt = 1; attempt <= opts.attempts; attempt++) {\n opts.signal?.throwIfAborted();\n try {\n return await fn();\n } catch (e) {\n if (isCancel(e) || opts.signal?.aborted) throw e;\n lastErr = e;\n if (attempt === opts.attempts) break;\n await sleep(opts.baseDelayMs * 2 ** (attempt - 1));\n }\n }\n throw lastErr;\n}\n","import type { BrowserSession } from '@adapters/browser/Browser.js';\nimport type { Downloader, DownloadProgress } from '@adapters/download/Downloader.js';\nimport type { NexusSite } from '@adapters/nexus/NexusSite.js';\nimport { isCancel } from '@core/errors.js';\nimport {\n type CollectionMember,\n type DownloadReport,\n type DownloadTarget,\n type GameDomain,\n type ModResult,\n summarize,\n} from '@core/types.js';\nimport { BackoffPolicy, DEFAULT_BACKOFF } from './backoff.js';\nimport { isThrottle, withRetry } from './retry.js';\n\nexport interface DownloadCollectionDeps {\n site: NexusSite;\n downloader: Downloader;\n}\n\nexport interface DownloadCollectionParams {\n game: GameDomain;\n ref: string;\n outDir: string;\n concurrency: number;\n dryRun: boolean;\n /** Include files the collection marks optional (default: required only). */\n includeOptional: boolean;\n retryAttempts: number;\n retryBaseDelayMs: number;\n /** Injected sleep, for deterministic tests. Defaults to real timers. */\n sleep?: (ms: number) => Promise<void>;\n /**\n * Cancels the run (Ctrl+C). Stops starting new members and aborts the\n * in-flight download; surfaces as a CancelError from this function.\n */\n signal?: AbortSignal;\n /** Called once with the full member list after it is resolved. */\n onResolved?: (members: CollectionMember[]) => void;\n /** Called before a member starts downloading (for live progress). */\n onStart?: (member: CollectionMember, index: number, total: number) => void;\n /** Per-file byte progress for the active member. */\n onFileProgress?: (p: DownloadProgress) => void;\n /** Progress callback, per completed member. */\n onProgress?: (r: ModResult) => void;\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/**\n * Fetch a collection's pinned files via the Nexus GraphQL API and download each\n * one directly by its `fileId` (the collection curates exact files, so we do\n * not re-scrape per-mod files pages). Best-effort: one failure never aborts the\n * batch. Pacing adapts to throttling via {@link BackoffPolicy}, which lives in\n * the app layer (browser-agnostic).\n */\nexport async function downloadCollection(\n deps: DownloadCollectionDeps,\n session: BrowserSession,\n params: DownloadCollectionParams,\n): Promise<DownloadReport> {\n const sleep = params.sleep ?? defaultSleep;\n\n params.signal?.throwIfAborted();\n const req = deps.site.collectionMembersQuery(params.game, params.ref);\n const json = await session.postJson(req.url, req.body, req.headers);\n const all = deps.site.parseCollectionMembers(json);\n const members = params.includeOptional ? all : all.filter((m) => !m.optional);\n params.onResolved?.(members);\n\n const policy = new BackoffPolicy({\n maxConcurrency: params.concurrency,\n ...DEFAULT_BACKOFF,\n });\n\n const results: ModResult[] = [];\n for (let i = 0; i < members.length; i++) {\n // Cancellation stops *starting* new members; the in-flight download is\n // aborted from inside runOne, which re-throws to break out of the loop.\n params.signal?.throwIfAborted();\n\n const member = members[i]!;\n if (policy.currentDelayMs > 0) {\n await sleep(policy.currentDelayMs);\n }\n\n params.onStart?.(member, i + 1, members.length);\n const result = await runOne(deps, session, params, member);\n results.push(result);\n\n if (result.ok) policy.onSuccess();\n else if (result.throttled) policy.onThrottle();\n\n params.onProgress?.(result);\n }\n\n return summarize(results);\n}\n\nasync function runOne(\n deps: DownloadCollectionDeps,\n session: BrowserSession,\n params: DownloadCollectionParams,\n member: CollectionMember,\n): Promise<ModResult> {\n if (params.dryRun) {\n return {\n modId: member.modId,\n ok: true,\n files: [member.name ?? `mod ${member.modId} file ${member.fileId}`],\n };\n }\n\n const target: DownloadTarget = {\n url: deps.site.fileDownloadUrl(member.game, member.modId, member.fileId),\n fileId: member.fileId,\n category: member.optional ? 'optional' : 'main',\n ...(member.name ? { fileName: member.name } : {}),\n };\n\n try {\n const path = await withRetry(\n () =>\n deps.downloader.fetch(target, params.outDir, session, params.onFileProgress, params.signal),\n {\n attempts: params.retryAttempts,\n baseDelayMs: params.retryBaseDelayMs,\n ...(params.sleep ? { sleep: params.sleep } : {}),\n ...(params.signal ? { signal: params.signal } : {}),\n },\n );\n return { modId: member.modId, ok: true, files: [path] };\n } catch (e) {\n // A cancellation is not a per-member failure — let it abort the batch.\n if (isCancel(e)) throw e;\n const message = e instanceof Error ? e.message : String(e);\n return {\n modId: member.modId,\n ok: false,\n files: [],\n error: message,\n throttled: isThrottle(e),\n };\n }\n}\n","import type { BrowserSession } from '@adapters/browser/Browser.js';\nimport type { Downloader, DownloadProgress } from '@adapters/download/Downloader.js';\nimport type { NexusSite } from '@adapters/nexus/NexusSite.js';\nimport { AuthError, ScrapeError } from '@core/errors.js';\nimport type { GameDomain, ModResult } from '@core/types.js';\nimport { withRetry } from './retry.js';\n\nexport interface DownloadModDeps {\n site: NexusSite;\n downloader: Downloader;\n}\n\nexport interface DownloadModParams {\n game: GameDomain;\n modId: number;\n outDir: string;\n dryRun: boolean;\n retryAttempts: number;\n retryBaseDelayMs: number;\n /** Injected sleep for retry, for deterministic tests. */\n sleep?: (ms: number) => Promise<void>;\n /** Per-file byte progress callback. */\n onFileProgress?: (p: DownloadProgress) => void;\n /** Cancels the run (Ctrl+C) between and during file downloads. */\n signal?: AbortSignal;\n}\n\n/**\n * Resolve a mod's main file(s) and download them through `session`. Pure\n * orchestration: all site/browser/disk specifics are behind the injected\n * interfaces.\n */\nexport async function downloadMod(\n deps: DownloadModDeps,\n session: BrowserSession,\n params: DownloadModParams,\n): Promise<ModResult> {\n params.signal?.throwIfAborted();\n const url = deps.site.modFilesUrl(params.game, params.modId);\n const landed = await session.goto(url);\n\n if (deps.site.isAuthRedirect(landed)) {\n throw new AuthError('session expired or not authenticated');\n }\n\n const html = await session.html();\n const all = deps.site.parseDownloadTargets(html);\n const main = all.filter((t) => t.category === 'main');\n if (main.length === 0) {\n throw new ScrapeError(`mod ${params.modId} has no main files`);\n }\n\n if (params.dryRun) {\n return {\n modId: params.modId,\n ok: true,\n files: main.map((t) => t.fileName ?? `file-${t.fileId}`),\n };\n }\n\n const files: string[] = [];\n for (const target of main) {\n params.signal?.throwIfAborted();\n const path = await withRetry(\n () =>\n deps.downloader.fetch(target, params.outDir, session, params.onFileProgress, params.signal),\n {\n attempts: params.retryAttempts,\n baseDelayMs: params.retryBaseDelayMs,\n ...(params.sleep ? { sleep: params.sleep } : {}),\n ...(params.signal ? { signal: params.signal } : {}),\n },\n );\n files.push(path);\n }\n\n return { modId: params.modId, ok: true, files };\n}\n","import type { Browser, BrowserSession } from '@adapters/browser/Browser.js';\nimport type { SessionStore } from '@adapters/session/SessionStore.js';\nimport { AuthError } from '@core/errors.js';\n\nexport interface RestoreDeps {\n browser: Browser;\n store: SessionStore;\n}\n\n/**\n * Launch a browser and seed it with the imported session cookies. Throws\n * {@link AuthError} (→ exit code 2) when no session is stored. The caller owns\n * closing the returned session.\n */\nexport async function restoreSession(deps: RestoreDeps, headful: boolean): Promise<BrowserSession> {\n const saved = await deps.store.load();\n if (!saved) {\n throw new AuthError('no saved session — run `nexus import --from chrome` first');\n }\n const session = await deps.browser.launch({ headful });\n await session.setCookies(saved.cookies);\n\n // Visit the account page first: this validates the session AND warms the\n // context past Cloudflare's challenge before any deep mod-page navigation\n // (a cold jump straight to a files page is more likely to be challenged).\n if (!(await session.isLoggedIn())) {\n await session.close();\n throw new AuthError('session expired — run `nexus import --from chrome` again');\n }\n return session;\n}\n","import { homedir, platform } from 'node:os';\nimport { join } from 'node:path';\n\nimport type { GameDomain } from '@core/types.js';\n\nconst APP = 'nexus-cli';\n\n/**\n * OS-appropriate config directory.\n * - Linux: $XDG_CONFIG_HOME/nexus-cli or ~/.config/nexus-cli\n * - macOS: ~/Library/Application Support/nexus-cli\n * - Windows: %APPDATA%/nexus-cli\n */\nexport function configDir(): string {\n const xdg = process.env.XDG_CONFIG_HOME;\n if (xdg) return join(xdg, APP);\n\n switch (platform()) {\n case 'win32':\n return join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), APP);\n case 'darwin':\n return join(homedir(), 'Library', 'Application Support', APP);\n default:\n return join(homedir(), '.config', APP);\n }\n}\n\n/** Path to the persisted session file. */\nexport function sessionFile(): string {\n return join(configDir(), 'session.json');\n}\n\n/** Default download directory for a game domain. */\nexport function defaultOutDir(game: GameDomain): string {\n return join(process.cwd(), 'downloads', game);\n}\n","import type { GameDomain } from '@core/types.js';\n\n/** A download target parsed from a Nexus URL: either a mod or a collection. */\nexport type NexusRef =\n { game: GameDomain; modId: number } | { game: GameDomain; collection: string };\n\n// Mod page: nexusmods.com/<game>/mods/<id>\n// Newer mod page: nexusmods.com/games/<game>/mods/<id>\nconst MOD = /nexusmods\\.com\\/(?:games\\/)?([^/]+)\\/mods\\/(\\d+)/i;\n// Collection page: nexusmods.com/games/<game>/collections/<slug>\nconst COLLECTION = /nexusmods\\.com\\/games\\/([^/]+)\\/collections\\/([^/?#]+)/i;\n\n/**\n * Parse a nexusmods.com mod or collection URL into the game domain and id/slug\n * the `download` command needs. Returns null for anything that isn't a\n * recognised mod or collection URL (the caller falls back to explicit flags).\n */\nexport function parseNexusUrl(input: string): NexusRef | null {\n const collection = COLLECTION.exec(input);\n if (collection) {\n return { game: collection[1]!, collection: collection[2]! };\n }\n const mod = MOD.exec(input);\n if (mod) {\n return { game: mod[1]!, modId: Number(mod[2]) };\n }\n return null;\n}\n","import { isNexusError } from '@core/errors.js';\n\n/** Minimal user-facing output. Errors render as one line unless verbose. */\nexport const out = {\n info(msg: string): void {\n process.stdout.write(`${msg}\\n`);\n },\n success(msg: string): void {\n process.stdout.write(`✓ ${msg}\\n`);\n },\n warn(msg: string): void {\n process.stderr.write(`! ${msg}\\n`);\n },\n error(e: unknown, verbose: boolean): void {\n if (verbose && e instanceof Error && e.stack) {\n process.stderr.write(`${e.stack}\\n`);\n return;\n }\n const prefix = isNexusError(e) ? `${e.kind} error` : 'error';\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`✗ ${prefix}: ${msg}\\n`);\n },\n};\n","import { mkdtemp, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nimport { Camoufox } from 'camoufox-js';\nimport type { BrowserContext, Page, Response } from 'playwright-core';\n\nimport { NetworkError } from '@core/errors.js';\nimport type { Cookie } from '@core/types.js';\nimport type { Browser, BrowserSession, LaunchOptions, ResolvedDownload } from './Browser.js';\n\nconst MAIN_HOST = 'www.nexusmods.com';\nconst ACCOUNT_URL = `https://${MAIN_HOST}/users/myaccount`;\nconst SIGN_IN_HOST = 'users.nexusmods.com';\nconst NEXUS_DOMAIN = '.nexusmods.com';\n\n/** How long to wait for Camoufox to auto-solve a Cloudflare challenge. */\nconst CHALLENGE_TIMEOUT_MS = 25_000;\n\n/**\n * Fallback cap for the GenerateDownloadUrl resolver POST. A real success or\n * failure response normally arrives in seconds; this only bounds the case where\n * the click never fired the request at all.\n */\nconst RESOLVE_TIMEOUT_MS = 60_000;\n\n/** `page.goto`/`waitForNavigation` yield a Response, or null for same-document nav. */\ntype NavResponse = Response | null;\n\n/**\n * Whether a navigation response is a Cloudflare challenge. Cloudflare sets\n * `cf-mitigated: challenge` on the interstitial response and omits it on the\n * real page — a deterministic protocol signal, so we never scrape page markup.\n */\nfunction isChallenge(response: NavResponse): boolean {\n return response?.headers()['cf-mitigated'] === 'challenge';\n}\n\n/** Whether an error is Playwright's \"execution context destroyed\" mid-navigation. */\nfunction isContextDestroyed(e: unknown): boolean {\n return e instanceof Error && /execution context was destroyed/i.test(e.message);\n}\n\n/** Camoufox-backed implementation of the Browser interface. */\nexport class CamoufoxBrowser implements Browser {\n async launch(opts: LaunchOptions): Promise<BrowserSession> {\n // Auth comes from imported cookies, so a throwaway profile is fine. Passing\n // user_data_dir makes Camoufox return a fully-configured BrowserContext —\n // we must NOT call newContext() ourselves (Camoufox's patched Firefox\n // rejects Playwright's setDefaultViewport).\n const userDataDir = await mkdtemp(join(tmpdir(), 'nexus-camoufox-'));\n const context = await Camoufox({\n headless: !opts.headful,\n user_data_dir: userDataDir,\n humanize: true,\n // Pin locale to match the imported session's origin. geoip is left OFF:\n // when it resolved to a different region than the session cookies, the\n // fingerprint/cookie mismatch triggered a hard Cloudflare challenge.\n locale: 'en-US',\n });\n\n const page = context.pages()[0] ?? (await context.newPage());\n return new CamoufoxSession(context, page, userDataDir);\n }\n}\n\nclass CamoufoxSession implements BrowserSession {\n constructor(\n private readonly context: BrowserContext,\n private readonly page: Page,\n private readonly userDataDir: string,\n ) {}\n\n async goto(url: string): Promise<string> {\n await this.navigate(url);\n return this.page.url();\n }\n\n /**\n * Navigate to `url` and, if Cloudflare intercepts with a challenge, wait for\n * Camoufox to auto-solve it. Returns the response for the real page.\n *\n * A challenge is identified by the `cf-mitigated: challenge` response header —\n * a protocol signal, not page markup. When it's absent the response is the\n * real page and we return at once; when present, Camoufox's auto-solve fires\n * its own navigation to the real page, which we wait for.\n */\n private async navigate(url: string): Promise<NavResponse> {\n let response: NavResponse;\n try {\n // `commit` resolves as soon as the response headers arrive — we don't\n // block on a heavy page's trailing resources.\n response = await this.page.goto(url, { waitUntil: 'commit' });\n } catch (e) {\n throw new NetworkError(`failed to load ${url}`, { cause: e });\n }\n\n const deadline = Date.now() + CHALLENGE_TIMEOUT_MS;\n while (isChallenge(response) && Date.now() < deadline) {\n response = await this.page\n .waitForNavigation({ waitUntil: 'commit', timeout: deadline - Date.now() })\n .catch(() => response);\n }\n\n // Let the document finish (incl. any client-side redirect like\n // myaccount → settings) before returning, so a subsequent navigation does\n // not abort an in-flight one (NS_BINDING_ABORTED). Best-effort: a heavy\n // page that never fully idles must not block the caller.\n await this.page\n .waitForLoadState('domcontentloaded', { timeout: 10_000 })\n .catch(() => undefined);\n return response;\n }\n\n async setCookies(cookies: Cookie[]): Promise<void> {\n await this.context.addCookies(\n cookies.map((c) => ({\n name: c.name,\n value: c.value,\n domain: c.domain || NEXUS_DOMAIN,\n path: c.path || '/',\n ...(c.expires !== undefined ? { expires: c.expires } : {}),\n ...(c.httpOnly !== undefined ? { httpOnly: c.httpOnly } : {}),\n ...(c.secure !== undefined ? { secure: c.secure } : {}),\n ...(c.sameSite ? { sameSite: c.sameSite } : {}),\n })),\n );\n }\n\n async isLoggedIn(): Promise<boolean> {\n try {\n await this.navigate(ACCOUNT_URL);\n } catch {\n return false;\n }\n // The account URL redirects to /settings or /users/... when authenticated,\n // and to the sign-in host when not.\n if (new URL(this.page.url()).host === SIGN_IN_HOST) return false;\n const url = this.page.url();\n return url.includes('/users/') || url.includes('/settings');\n }\n\n async html(): Promise<string> {\n // `content()` throws if the page is mid-navigation (the `commit`-based goto\n // can return while a redirect is still settling). Wait for the DOM to be\n // ready and retry a couple of times before giving up.\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n await this.page.waitForLoadState('domcontentloaded', { timeout: 10_000 });\n return await this.page.content();\n } catch {\n await this.page.waitForTimeout(500);\n }\n }\n return this.page.content();\n }\n\n async postJson(\n url: string,\n body: unknown,\n headers: Record<string, string> = {},\n ): Promise<unknown> {\n // Run fetch INSIDE the page so cookies + origin (nexusmods.com) apply —\n // required for the GraphQL endpoint to accept the request.\n //\n // The fetch is cross-origin (api-router.nexusmods.com), so the page's own\n // origin must be www.nexusmods.com or the browser rejects it with a CORS\n // \"NetworkError\". After session restore the page sits on the account host\n // (users.nexusmods.com / settings), so move to www first.\n if (new URL(this.page.url()).host !== MAIN_HOST) {\n await this.navigate(`https://${MAIN_HOST}/`).catch(() => undefined);\n }\n\n // The page may still be settling a client-side redirect from the preceding\n // navigation (e.g. account-page warm-up), which destroys the execution\n // context mid-evaluate. Wait for the DOM to be ready and retry once.\n for (let attempt = 0; ; attempt++) {\n await this.page\n .waitForLoadState('domcontentloaded', { timeout: 10_000 })\n .catch(() => undefined);\n try {\n return await this.page.evaluate(\n async ({ url, body, headers }) => {\n const res = await fetch(url, {\n method: 'POST',\n credentials: 'include',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(body),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);\n return res.json() as Promise<unknown>;\n },\n { url, body, headers },\n );\n } catch (e) {\n if (attempt >= 2 || !isContextDestroyed(e)) throw e;\n }\n }\n }\n\n async resolveUsername(): Promise<string | null> {\n await this.navigate(ACCOUNT_URL).catch(() => undefined);\n return this.page\n .evaluate(() => {\n const el =\n document.querySelector<HTMLElement>('[data-username]') ??\n document.querySelector<HTMLElement>('#login-name, .username');\n const ds = el?.dataset?.username;\n if (ds) return ds;\n const text = el?.textContent?.trim();\n return text && text.length > 0 ? text : null;\n })\n .catch(() => null);\n }\n\n async resolveDownloadUrl(filePageUrl: string): Promise<ResolvedDownload> {\n try {\n // The manual-download page renders a <mod-file-download> web component\n // whose \"Slow download\" button (in an open shadow root) triggers a POST\n // to GenerateDownloadUrl that returns the signed CDN URL. We drive that\n // button (the trusted path that clears Cloudflare) and capture the\n // resolver's JSON response — then Node streams the URL itself.\n await this.navigate(filePageUrl);\n\n const slowButton = this.page.getByRole('button', { name: /slow download/i });\n await slowButton.waitFor({ state: 'visible', timeout: 30_000 });\n\n // Wait for the resolver POST regardless of status. Matching only 200s\n // meant a failed resolve never matched and we hung the full timeout; the\n // failure response is the signal, so inspect it instead of the page.\n const respPromise = this.page.waitForResponse((r) => /GenerateDownloadUrl/i.test(r.url()), {\n timeout: RESOLVE_TIMEOUT_MS,\n });\n // We don't want the native download — cancel it; we stream the URL.\n this.page.once('download', (d) => void d.cancel().catch(() => undefined));\n await slowButton.click();\n\n const resp = await respPromise;\n if (!resp.ok()) {\n // e.g. Nexus's \"download link could not be retrieved, try again later\".\n // Retryable — surfaced as NetworkError so withRetry backs off and retries.\n throw new NetworkError(`download resolver returned HTTP ${resp.status()}`);\n }\n const cdnUrl = ((await resp.json()) as { url?: string }).url;\n if (!cdnUrl) throw new NetworkError('resolver returned no download URL');\n\n const cookieHeader = (await this.context.cookies())\n .map((c) => `${c.name}=${c.value}`)\n .join('; ');\n const userAgent = await this.page.evaluate(() => navigator.userAgent);\n return { cdnUrl, cookieHeader, userAgent };\n } catch (e) {\n if (e instanceof NetworkError) throw e;\n throw new NetworkError(`failed to resolve download for ${filePageUrl}`, {\n cause: e,\n });\n }\n }\n\n async close(): Promise<void> {\n await this.context.close().catch(() => undefined);\n await rm(this.userDataDir, { recursive: true, force: true }).catch(() => undefined);\n }\n}\n","import type { CookieQueryStrategy, ExportedCookie } from '@mherod/get-cookie';\n\nimport { AuthError } from '@core/errors.js';\nimport type { Cookie } from '@core/types.js';\nimport type { CookieSource } from './CookieSource.js';\n\n/** Chromium variants get-cookie targets by name, sharing Chrome's cookie schema. */\nconst CHROMIUM_VARIANTS = [\n 'chromium',\n 'brave',\n 'edge',\n 'opera',\n 'opera-gx',\n 'vivaldi',\n 'arc',\n 'whale',\n] as const;\n\ntype ChromiumVariant = (typeof CHROMIUM_VARIANTS)[number];\n\nfunction isChromiumVariant(key: string): key is ChromiumVariant {\n return (CHROMIUM_VARIANTS as readonly string[]).includes(key);\n}\n\n/** Display names for the `--from` values we accept, for messages. */\nconst DISPLAY: Record<string, string> = {\n chrome: 'Chrome',\n chromium: 'Chromium',\n brave: 'Brave',\n edge: 'Edge',\n opera: 'Opera',\n 'opera-gx': 'Opera GX',\n vivaldi: 'Vivaldi',\n arc: 'Arc',\n whale: 'Whale',\n firefox: 'Firefox',\n safari: 'Safari',\n};\n\n/**\n * Reads cookies directly from an installed browser via `@mherod/get-cookie`,\n * which handles each platform's encryption (macOS Keychain, Windows DPAPI,\n * Linux libsecret). Supports Chrome and its Chromium cousins, Firefox, and\n * Safari — the latter may prompt once for macOS Full Disk Access.\n */\nexport class BrowserCookieSource implements CookieSource {\n readonly browser: string;\n private readonly key: string;\n\n constructor(from: string) {\n this.key = from.toLowerCase();\n if (!(this.key in DISPLAY)) {\n throw new AuthError(\n `unsupported browser '${from}' (supported: ${Object.keys(DISPLAY).join(', ')})`,\n );\n }\n this.browser = DISPLAY[this.key]!;\n }\n\n async read(domainSuffix: string): Promise<Cookie[]> {\n const strategy = await strategyFor(this.key);\n let found: ExportedCookie[];\n try {\n // '%' is get-cookie's match-all name pattern; we want every cookie on the\n // domain, not one named cookie.\n found = await strategy.queryCookies('%', domainSuffix);\n } catch (e) {\n throw new AuthError(`could not read ${this.browser} cookies`, { cause: e });\n }\n return found.map(toCookie);\n }\n}\n\n/**\n * Map a `--from` value to its get-cookie strategy. Imports get-cookie lazily —\n * and only after silencing its dotenv banner — so the noise never reaches the\n * user and the dep isn't loaded for commands that don't read a browser.\n */\nasync function strategyFor(key: string): Promise<CookieQueryStrategy> {\n process.env.DOTENV_CONFIG_QUIET = 'true';\n const gc = await import('@mherod/get-cookie');\n if (key === 'chrome') return new gc.ChromeCookieQueryStrategy();\n if (key === 'firefox') return new gc.FirefoxCookieQueryStrategy();\n if (key === 'safari') return new gc.SafariCookieQueryStrategy();\n if (isChromiumVariant(key)) return new gc.ChromiumCookieQueryStrategy(key);\n // Unreachable: the constructor already rejected unknown keys.\n throw new AuthError(`unsupported browser '${key}'`);\n}\n\n/** Convert a get-cookie `ExportedCookie` into our Playwright-shaped `Cookie`. */\nexport function toCookie(c: ExportedCookie): Cookie {\n const cookie: Cookie = {\n name: c.name,\n value: String(c.value),\n domain: c.domain,\n path: c.meta?.path ?? '/',\n };\n if (c.meta?.secure !== undefined) cookie.secure = c.meta.secure;\n if (c.meta?.httpOnly !== undefined) cookie.httpOnly = c.meta.httpOnly;\n const expires = expiryToUnix(c.expiry);\n if (expires !== undefined) cookie.expires = expires;\n return cookie;\n}\n\n/** get-cookie reports expiry as a Date, unix-ish number, or \"Infinity\"; we need unix seconds. */\nfunction expiryToUnix(expiry: ExportedCookie['expiry']): number | undefined {\n if (expiry === undefined || expiry === 'Infinity') return undefined;\n if (expiry instanceof Date) return Math.floor(expiry.getTime() / 1000);\n // A number: already seconds if it looks like seconds, else milliseconds.\n return expiry > 1e12 ? Math.floor(expiry / 1000) : expiry;\n}\n","import { readFile } from 'node:fs/promises';\n\nimport { AuthError } from '@core/errors.js';\nimport type { Cookie } from '@core/types.js';\nimport type { CookieSource } from './CookieSource.js';\n\n/**\n * Reads cookies from a file exported by a browser extension — an alternative to\n * reading the browser's store directly ({@link BrowserCookieSource}) for users\n * who would rather export a file than grant cookie-store access.\n *\n * Auto-detects the two shapes such extensions emit:\n * - a **JSON array** (Cookie-Editor / EditThisCookie style), or\n * - the **Netscape `cookies.txt`** tab-delimited format (curl/wget style).\n */\nexport class FileCookieSource implements CookieSource {\n readonly browser: string;\n\n constructor(private readonly path: string) {\n this.browser = `file ${path}`;\n }\n\n async read(domainSuffix: string): Promise<Cookie[]> {\n let raw: string;\n try {\n raw = await readFile(this.path, 'utf8');\n } catch (e) {\n throw new AuthError(`could not read cookie file ${this.path}`, { cause: e });\n }\n\n const cookies = looksLikeJson(raw) ? parseJson(raw, this.path) : parseNetscape(raw);\n return cookies.filter((c) => hostMatches(c.domain, domainSuffix));\n }\n}\n\n/** A leading `[` or `{` (after whitespace) marks a JSON export; otherwise Netscape. */\nfunction looksLikeJson(raw: string): boolean {\n return /^\\s*[[{]/.test(raw);\n}\n\ninterface JsonCookie {\n name?: string;\n value?: string;\n domain?: string;\n path?: string;\n secure?: boolean;\n httpOnly?: boolean;\n sameSite?: string;\n /** Extensions name expiry either `expirationDate` (Cookie-Editor) or `expires`. */\n expirationDate?: number;\n expires?: number;\n}\n\nfunction parseJson(raw: string, path: string): Cookie[] {\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch (e) {\n throw new AuthError(`cookie file ${path} is not valid JSON`, { cause: e });\n }\n // Accept a bare array or a `{ cookies: [...] }` wrapper.\n const arr = Array.isArray(data)\n ? data\n : Array.isArray((data as { cookies?: unknown }).cookies)\n ? (data as { cookies: unknown[] }).cookies\n : null;\n if (!arr) {\n throw new AuthError(`cookie file ${path} is not a JSON cookie array`);\n }\n\n const cookies: Cookie[] = [];\n for (const entry of arr) {\n const c = entry as JsonCookie;\n if (!c.name || c.value === undefined || !c.domain) continue;\n const expires = c.expirationDate ?? c.expires;\n cookies.push({\n name: c.name,\n value: c.value,\n domain: c.domain,\n path: c.path ?? '/',\n secure: Boolean(c.secure),\n httpOnly: Boolean(c.httpOnly),\n sameSite: sameSiteOf(c.sameSite),\n ...(typeof expires === 'number' && expires > 0 ? { expires: Math.floor(expires) } : {}),\n });\n }\n return cookies;\n}\n\n/**\n * Netscape `cookies.txt`: tab-delimited\n * `domain includeSubdomains path secure expires name value`.\n * Lines starting with `#` are comments, except the `#HttpOnly_` prefix some\n * tools prepend to the domain field.\n */\nfunction parseNetscape(raw: string): Cookie[] {\n const cookies: Cookie[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#HttpOnly_'))) continue;\n\n const f = trimmed.split('\\t');\n if (f.length < 7) continue;\n\n let domain = f[0]!;\n let httpOnly = false;\n if (domain.startsWith('#HttpOnly_')) {\n domain = domain.slice('#HttpOnly_'.length);\n httpOnly = true;\n }\n\n const expires = Number(f[4]);\n const cookie: Cookie = {\n name: f[5]!,\n value: f[6]!,\n domain,\n path: f[2] ?? '/',\n secure: f[3]!.toUpperCase() === 'TRUE',\n httpOnly,\n sameSite: 'Lax',\n };\n if (Number.isFinite(expires) && expires > 0) cookie.expires = expires;\n cookies.push(cookie);\n }\n return cookies;\n}\n\n/** Map an extension's `sameSite` string to Playwright's enum; default `Lax`. */\nfunction sameSiteOf(v: string | undefined): 'Strict' | 'Lax' | 'None' {\n switch (v?.toLowerCase()) {\n case 'strict':\n return 'Strict';\n case 'no_restriction':\n case 'none':\n return 'None';\n default:\n return 'Lax';\n }\n}\n\n/** Whether a cookie's host belongs to `suffix` (exact or a subdomain). */\nfunction hostMatches(domain: string, suffix: string): boolean {\n const host = domain.replace(/^\\./, '').toLowerCase();\n const s = suffix.toLowerCase();\n return host === s || host.endsWith(`.${s}`);\n}\n","import { access, mkdir, rename, rm } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { basename, join } from 'node:path';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport { DownloadError, NetworkError } from '@core/errors.js';\nimport type { DownloadTarget } from '@core/types.js';\nimport type { BrowserSession } from '../browser/Browser.js';\nimport type { Downloader, DownloadProgress } from './Downloader.js';\n\n/**\n * Resolves a file's signed CDN URL through the browser session, then streams it\n * to disk **from Node** (the in-page fetch is blocked by CORS on the CDN host;\n * Node has no CORS restriction). Writes to a `.part` file and renames on\n * success; skips files that already exist.\n */\nexport class BrowserDownloader implements Downloader {\n async fetch(\n target: DownloadTarget,\n outDir: string,\n session: BrowserSession,\n onProgress?: (p: DownloadProgress) => void,\n signal?: AbortSignal,\n ): Promise<string> {\n signal?.throwIfAborted();\n await mkdir(outDir, { recursive: true });\n\n const resolved = await session.resolveDownloadUrl(target.url);\n\n // The signed CDN URL carries the real filename in its path.\n const name = filenameFrom(resolved.cdnUrl) ?? fallbackName(target);\n const finalPath = join(outDir, name);\n if (await exists(finalPath)) return finalPath; // already complete — skip.\n\n const partPath = `${finalPath}.part`;\n\n // The signed CDN URL self-authenticates via its query params. Only Nexus's\n // own hosts expect the session cookie; third-party CDNs reject the\n // unexpected jar with a 400, so send cookies only to nexusmods.com hosts.\n const onNexusHost = isNexusHost(resolved.cdnUrl);\n const headers: Record<string, string> = {\n 'user-agent': resolved.userAgent,\n referer: 'https://www.nexusmods.com/',\n };\n if (onNexusHost) headers.cookie = resolved.cookieHeader;\n\n let res: Response;\n try {\n res = await fetch(resolved.cdnUrl, { headers, ...(signal ? { signal } : {}) });\n } catch (e) {\n if (signal?.aborted) throw e; // propagate the AbortError untouched\n throw new NetworkError(`failed to fetch file ${target.fileId}`, { cause: e });\n }\n if (!res.ok || !res.body) {\n const host = hostOf(resolved.cdnUrl);\n throw new DownloadError(\n `download for file ${target.fileId} returned HTTP ${res.status} (cdn: ${host})`,\n );\n }\n\n const totalBytes = Number(res.headers.get('content-length') ?? 0);\n let receivedBytes = 0;\n const source = Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]);\n source.on('data', (chunk: Buffer) => {\n receivedBytes += chunk.length;\n onProgress?.({ receivedBytes, totalBytes });\n });\n\n try {\n // `pipeline` with `signal` aborts both streams and rejects with an\n // AbortError when the user cancels mid-transfer.\n await pipeline(source, createWriteStream(partPath), signal ? { signal } : {});\n } catch (e) {\n // Either way, the .part is incomplete — there is no resume — so remove it.\n await rm(partPath, { force: true });\n if (signal?.aborted) throw e; // propagate the AbortError untouched\n throw new DownloadError(`failed writing file ${target.fileId}`, { cause: e });\n }\n\n await rename(partPath, finalPath);\n return finalPath;\n }\n}\n\n/** The URL's hostname, or '' if unparseable. */\nfunction hostOf(url: string): string {\n try {\n return new URL(url).hostname;\n } catch {\n return '';\n }\n}\n\n/** Whether a URL is served from a nexusmods.com host (so the session cookie applies). */\nfunction isNexusHost(url: string): boolean {\n const host = hostOf(url);\n return host === 'nexusmods.com' || host.endsWith('.nexusmods.com');\n}\n\n/** Derive the filename from a signed CDN URL's path (decoded). */\nfunction filenameFrom(cdnUrl: string): string | null {\n try {\n const path = new URL(cdnUrl).pathname;\n const base = decodeURIComponent(basename(path));\n return base.length > 0 ? sanitize(base) : null;\n } catch {\n return null;\n }\n}\n\nfunction fallbackName(target: DownloadTarget): string {\n if (target.fileName) {\n const base = sanitize(target.fileName);\n return /\\.[a-z0-9]{2,4}$/i.test(base) ? base : `${base}-${target.fileId}`;\n }\n return `file-${target.fileId}`;\n}\n\nfunction sanitize(name: string): string {\n return name\n .replace(/[/\\\\?%*:|\"<>]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n","import { ScrapeError } from '@core/errors.js';\nimport type { CollectionMember, DownloadTarget, FileCategory, GameDomain } from '@core/types.js';\nimport type { JsonRequest, NexusSite } from './NexusSite.js';\n\nconst BASE = 'https://www.nexusmods.com';\nconst GRAPHQL_URL = 'https://api-router.nexusmods.com/graphql';\nconst SIGN_IN_HOST = 'users.nexusmods.com';\n\nconst COLLECTION_QUERY = `\n query CollectionRevisionMods($slug: String!, $viewAdultContent: Boolean = true) {\n collectionRevision(slug: $slug, viewAdultContent: $viewAdultContent) {\n modFiles {\n fileId\n optional\n file {\n name\n sizeInBytes\n mod {\n modId\n name\n game { domainName }\n }\n }\n }\n }\n }`;\n\n/**\n * Scrapes Nexus pages from raw HTML. Deliberately dependency-free (regex over\n * the markup) so it is fast and trivially unit-testable against fixtures.\n *\n * Selectors are isolated here; when the site drifts, only this file and the\n * fixtures change.\n */\nexport class NexusWebAdapter implements NexusSite {\n modFilesUrl(game: GameDomain, modId: number): string {\n return `${BASE}/${game}/mods/${modId}?tab=files`;\n }\n\n collectionUrl(game: GameDomain, ref: string): string {\n return `${BASE}/games/${game}/collections/${ref}/mods`;\n }\n\n collectionMembersQuery(_game: GameDomain, ref: string): JsonRequest {\n return {\n url: GRAPHQL_URL,\n body: {\n operationName: 'CollectionRevisionMods',\n query: COLLECTION_QUERY,\n variables: { slug: ref, viewAdultContent: true },\n },\n headers: { 'x-graphql-operationname': 'CollectionRevisionMods' },\n };\n }\n\n parseCollectionMembers(json: unknown): CollectionMember[] {\n const modFiles = (json as GqlResponse)?.data?.collectionRevision?.modFiles;\n if (!Array.isArray(modFiles)) {\n throw new ScrapeError('unexpected collection response shape');\n }\n\n const members: CollectionMember[] = [];\n for (const entry of modFiles) {\n const mod = entry.file?.mod;\n const modId = mod?.modId;\n const game = mod?.game?.domainName;\n const fileId = entry.fileId;\n if (!modId || !game || !fileId) continue;\n const sizeBytes = Number(entry.file?.sizeInBytes);\n members.push({\n game,\n modId,\n fileId,\n optional: Boolean(entry.optional),\n ...(entry.file?.name ? { name: entry.file.name } : {}),\n ...(Number.isFinite(sizeBytes) && sizeBytes > 0 ? { sizeBytes } : {}),\n });\n }\n\n if (members.length === 0) {\n throw new ScrapeError('collection has no downloadable files');\n }\n return members;\n }\n\n fileDownloadUrl(game: GameDomain, modId: number, fileId: number): string {\n return `${BASE}/${game}/mods/${modId}?tab=files&file_id=${fileId}`;\n }\n\n parseDownloadTargets(html: string): DownloadTarget[] {\n const base = modBaseUrl(html);\n const targets: DownloadTarget[] = [];\n\n // Each file is a `<dt id=\"file-expander-header-<id>\" data-name=\"..\"\n // data-version=\"..\">` row inside its category section. The `data-id` is the\n // Nexus file id; the actual download href is constructed from it (the file\n // list's download links are otherwise hydrated client-side and absent from\n // static HTML). We segment by category header, then read these rows.\n for (const segment of segmentByCategory(html)) {\n // Match the whole expander-header tag, then read attributes from it\n // (order-independent — Nexus emits id/class/data-* in varying orders).\n const tagRe = /<dt[^>]*id=\"file-expander-header-(\\d+)\"[^>]*>/gi;\n let m: RegExpExecArray | null;\n while ((m = tagRe.exec(segment.body)) !== null) {\n const fileId = Number(m[1]);\n if (!Number.isFinite(fileId)) continue;\n const tag = m[0];\n const name = (/data-name=\"([^\"]*)\"/i.exec(tag)?.[1] ?? '').trim();\n const version = (/data-version=\"([^\"]*)\"/i.exec(tag)?.[1] ?? '').trim();\n const fileName = name ? (version ? `${name} ${version}` : name) : undefined;\n targets.push({\n url: downloadUrl(base, fileId),\n fileId,\n category: segment.category,\n ...(fileName ? { fileName } : {}),\n });\n }\n }\n\n return targets;\n }\n\n isAuthRedirect(landedUrl: string): boolean {\n // An expired/invalid session is bounced to the sign-in host. Compare the\n // host exactly (a substring match would also flag the main site's own\n // `users.nexusmods.com`-less paths inconsistently).\n try {\n return new URL(landedUrl).host === SIGN_IN_HOST;\n } catch {\n return false;\n }\n }\n}\n\ninterface GqlResponse {\n data?: {\n collectionRevision?: {\n modFiles?: {\n fileId?: number;\n optional?: boolean;\n file?: {\n name?: string;\n sizeInBytes?: string;\n mod?: {\n modId?: number;\n name?: string;\n game?: { domainName?: GameDomain };\n };\n };\n }[];\n };\n };\n}\n\ninterface Segment {\n category: FileCategory;\n body: string;\n}\n\n/**\n * Split the files page into per-category segments. The live Nexus page groups\n * files into containers identified by `id=\"file-container-<cat>-files\"` (e.g.\n * `main-files`, `optional-files`, `old-files`, `miscellaneous-files`). As a\n * fallback (and for fixtures), legacy `<h3>Main files</h3>`-style headers are\n * also recognised. Content before the first marker is treated as `unknown`.\n */\nfunction segmentByCategory(html: string): Segment[] {\n const markerRe =\n // 1) Live container ids: id=\"file-container-main-files\"\n // 2) Legacy text headers: <h3>Main files</h3> / \"Old versions\"\n /id=\"file-container-(main|optional|old|miscellaneous)-files\"|<(?:h[1-6]|div|dt)[^>]*>\\s*((?:main|optional|old|miscellaneous)[^<]*?files?|old versions?)\\s*<\\/(?:h[1-6]|div|dt)>/gi;\n\n const marks: { index: number; category: FileCategory }[] = [];\n let m: RegExpExecArray | null;\n while ((m = markerRe.exec(html)) !== null) {\n const category = m[1] ? categoryOf(m[1]) : categoryOf(m[2] ?? '');\n marks.push({ index: m.index, category });\n }\n\n if (marks.length === 0) {\n return [{ category: 'unknown', body: html }];\n }\n\n const segments: Segment[] = [];\n for (let i = 0; i < marks.length; i++) {\n const start = marks[i]!.index;\n const end = i + 1 < marks.length ? marks[i + 1]!.index : html.length;\n segments.push({ category: marks[i]!.category, body: html.slice(start, end) });\n }\n return segments;\n}\n\nfunction categoryOf(label: string): FileCategory {\n const h = label.toLowerCase();\n if (h.startsWith('main')) return 'main';\n if (h.startsWith('optional')) return 'optional';\n if (h.startsWith('old')) return 'old';\n if (h.startsWith('misc')) return 'miscellaneous';\n return 'unknown';\n}\n\n/** Extract the `https://.../<game>/mods/<id>` base from the page's og:url. */\nfunction modBaseUrl(html: string): string {\n const og = /property=\"og:url\"\\s+content=\"([^\"]+)\"/i.exec(html);\n if (og?.[1]) return og[1].replace(/[?#].*$/, '');\n const path = /\\/[a-z0-9]+\\/mods\\/\\d+/i.exec(html);\n return path ? `${BASE}${path[0]}` : BASE;\n}\n\n/**\n * Build the \"Manual download\" URL for a file id (no `nmm=1` — that one hands\n * off to a mod manager). This page presents the free slow-download button.\n */\nfunction downloadUrl(base: string, fileId: number): string {\n return `${base}?tab=files&file_id=${fileId}`;\n}\n","import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\nimport { sessionFile } from '@config/paths.js';\nimport type { Session } from '@core/types.js';\nimport type { SessionStore } from './SessionStore.js';\n\n/** Stores the session as a `600`-mode JSON file in the OS config dir. */\nexport class FileSessionStore implements SessionStore {\n constructor(private readonly path: string = sessionFile()) {}\n\n async save(s: Session): Promise<void> {\n await mkdir(dirname(this.path), { recursive: true });\n await writeFile(this.path, JSON.stringify(s, null, 2), { mode: 0o600 });\n }\n\n async load(): Promise<Session | null> {\n let raw: string;\n try {\n raw = await readFile(this.path, 'utf8');\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw e;\n }\n const parsed = JSON.parse(raw) as Session;\n if (!parsed.username || !parsed.cookies) return null;\n return parsed;\n }\n\n async clear(): Promise<void> {\n await rm(this.path, { force: true });\n }\n}\n","import { CamoufoxBrowser } from '@adapters/browser/CamoufoxBrowser.js';\nimport { BrowserCookieSource } from '@adapters/cookies/BrowserCookieSource.js';\nimport type { CookieSource } from '@adapters/cookies/CookieSource.js';\nimport { FileCookieSource } from '@adapters/cookies/FileCookieSource.js';\nimport { BrowserDownloader } from '@adapters/download/BrowserDownloader.js';\nimport { NexusWebAdapter } from '@adapters/nexus/NexusWebAdapter.js';\nimport { FileSessionStore } from '@adapters/session/FileSessionStore.js';\n\n/**\n * Composition root: the only place concrete adapters are constructed. Commands\n * receive these via their handlers; the app layer never imports them.\n */\nexport function buildDeps() {\n return {\n browser: new CamoufoxBrowser(),\n store: new FileSessionStore(),\n site: new NexusWebAdapter(),\n downloader: new BrowserDownloader(),\n };\n}\n\nexport type Deps = ReturnType<typeof buildDeps>;\n\n/** Domain suffix for Nexus cookies. */\nexport const NEXUS_COOKIE_DOMAIN = 'nexusmods.com';\n\n/** Resolve a cookie source by browser name. */\nexport function cookieSourceFor(browser: string): CookieSource {\n return new BrowserCookieSource(browser);\n}\n\n/** Resolve a cookie source from an exported cookie file (--file). */\nexport function fileCookieSource(path: string): CookieSource {\n return new FileCookieSource(path);\n}\n","import ora, { type Ora } from 'ora';\nimport type { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';\n\nimport { downloadCollection } from '@app/downloadCollection.js';\nimport { downloadMod } from '@app/downloadMod.js';\nimport { restoreSession } from '@app/restoreSession.js';\nimport { defaultOutDir } from '@config/paths.js';\nimport { basename } from 'node:path';\nimport { AuthError, CancelError, isCancel } from '@core/errors.js';\nimport { type DownloadReport, type ModResult, summarize } from '@core/types.js';\nimport { parseNexusUrl } from '@adapters/nexus/parseNexusUrl.js';\nimport { out } from '../output.js';\nimport { buildDeps } from '../wiring.js';\n\ninterface DownloadArgs {\n target?: string;\n game: string;\n mod?: number;\n collection?: string;\n out?: string;\n concurrency: number;\n 'dry-run': boolean;\n optional: boolean;\n headful: boolean;\n verbose: boolean;\n}\n\nconst RETRY_ATTEMPTS = 3;\nconst RETRY_BASE_DELAY_MS = 1_000;\n\nexport const downloadCommand: CommandModule = {\n command: 'download [target]',\n describe: 'Download a mod or a collection',\n builder: (y: Argv) =>\n y\n .positional('target', {\n type: 'string',\n describe: 'A nexusmods.com mod or collection URL (or use --game with --mod/--collection)',\n })\n .option('game', {\n type: 'string',\n describe: 'Nexus game domain (e.g. skyrimspecialedition)',\n })\n .option('mod', { type: 'number', describe: 'Numeric mod id' })\n .option('collection', { type: 'string', describe: 'Collection slug or id' })\n .option('out', { type: 'string', describe: 'Output directory' })\n .option('concurrency', { type: 'number', default: 2 })\n .option('dry-run', {\n type: 'boolean',\n default: false,\n describe: 'List what would be downloaded without fetching',\n })\n .option('optional', {\n type: 'boolean',\n default: false,\n describe: 'For collections, also download files marked optional',\n })\n .option('headful', {\n type: 'boolean',\n default: false,\n describe: 'Show the browser window (useful for debugging)',\n })\n .conflicts('mod', 'collection')\n .check((argv) => {\n // A URL positional supplies game + mod/collection; resolve it into the\n // same fields the flags use so the handler reads one shape.\n if (typeof argv.target === 'string') {\n const ref = parseNexusUrl(argv.target);\n if (!ref) {\n throw new Error(`not a recognised Nexus mod or collection URL: ${argv.target}`);\n }\n argv.game = ref.game;\n if ('modId' in ref) argv.mod = ref.modId;\n else argv.collection = ref.collection;\n }\n if (argv.game === undefined) {\n throw new Error('provide a Nexus URL, or --game with --mod/--collection');\n }\n if (argv.mod === undefined && argv.collection === undefined) {\n throw new Error('provide a Nexus URL, or --mod / --collection');\n }\n return true;\n }),\n handler: async (raw: ArgumentsCamelCase) => {\n const argv = raw as unknown as DownloadArgs;\n const outDir = argv.out ?? defaultOutDir(argv.game);\n const deps = buildDeps();\n\n // One spinner spans the whole run: the (slow) session restore + Cloudflare\n // warm-up, then per-file progress. ora auto-disables on non-TTY.\n //\n // discardStdin:false is REQUIRED: ora's default stdin discarder puts the TTY\n // in raw mode, which disables the terminal's SIGINT generation — Ctrl+C then\n // never reaches our handler and the run can't be cancelled.\n const spinner = ora({ text: 'Restoring session…', discardStdin: false }).start();\n\n // Ctrl+C: first press aborts gracefully (stop new work, abort the in-flight\n // file, close the browser, print a summary); a second press hard-exits in\n // case cleanup hangs. The handler is removed in `finally`.\n const controller = new AbortController();\n const onSigint = (): void => {\n if (controller.signal.aborted) {\n spinner.stop();\n out.warn('forced quit');\n process.exit(130);\n }\n controller.abort(new CancelError('cancelled by user'));\n spinner.text = 'Cancelling… (press Ctrl+C again to force quit)';\n };\n process.on('SIGINT', onSigint);\n\n let session;\n try {\n session = await restoreSession(deps, argv.headful);\n } catch (e) {\n spinner.stop();\n process.removeListener('SIGINT', onSigint);\n if (isCancel(e) || controller.signal.aborted) {\n out.warn('cancelled');\n process.exitCode = 130;\n return;\n }\n out.error(e, argv.verbose);\n process.exitCode = e instanceof AuthError ? 2 : 1;\n return;\n }\n\n // Tick once a second, independent of download events — ora only re-renders\n // text we reassign, so without this the run-elapsed clock and the per-file\n // elapsed/speed/ETA would freeze between byte/file callbacks.\n const runStart = Date.now();\n const progress = new FileProgress();\n const global = argv.collection !== undefined ? new GlobalProgress() : null;\n const ticker = setInterval(() => {\n if (argv['dry-run']) return;\n spinner.text = composeStatus(progress, global, Date.now() - runStart);\n }, 1000);\n if (typeof ticker.unref === 'function') ticker.unref();\n\n const signal = controller.signal;\n try {\n const report =\n argv.collection !== undefined\n ? await runCollection(\n deps,\n session,\n argv,\n outDir,\n spinner,\n progress,\n global!,\n runStart,\n signal,\n )\n : await runMod(deps, session, argv, outDir, spinner, progress, runStart, signal);\n\n clearInterval(ticker);\n finish(spinner, report, argv['dry-run'], Date.now() - runStart);\n process.exitCode = report.failed > 0 ? 1 : 0;\n } catch (e) {\n clearInterval(ticker);\n spinner.stop();\n if (isCancel(e) || signal.aborted) {\n out.warn('cancelled — partial downloads removed');\n process.exitCode = 130;\n } else {\n out.error(e, argv.verbose);\n process.exitCode = e instanceof AuthError ? 2 : 1;\n }\n } finally {\n process.removeListener('SIGINT', onSigint);\n await session.close();\n }\n },\n};\n\nasync function runMod(\n deps: ReturnType<typeof buildDeps>,\n session: Awaited<ReturnType<typeof restoreSession>>,\n argv: DownloadArgs,\n outDir: string,\n spinner: Ora,\n progress: FileProgress,\n runStart: number,\n signal: AbortSignal,\n): Promise<DownloadReport> {\n const verb = argv['dry-run'] ? 'Resolving' : 'Downloading';\n progress.start(`${verb} mod ${argv.mod}`);\n spinner.text = progress.render();\n const result: ModResult = await downloadMod(deps, session, {\n game: argv.game,\n modId: argv.mod!,\n outDir,\n dryRun: argv['dry-run'],\n retryAttempts: RETRY_ATTEMPTS,\n retryBaseDelayMs: RETRY_BASE_DELAY_MS,\n signal,\n onFileProgress: (p) => {\n progress.update(p.receivedBytes, p.totalBytes);\n spinner.text = composeStatus(progress, null, Date.now() - runStart);\n },\n });\n progress.done();\n return summarize([result]);\n}\n\nasync function runCollection(\n deps: ReturnType<typeof buildDeps>,\n session: Awaited<ReturnType<typeof restoreSession>>,\n argv: DownloadArgs,\n outDir: string,\n spinner: Ora,\n progress: FileProgress,\n global: GlobalProgress,\n runStart: number,\n signal: AbortSignal,\n): Promise<DownloadReport> {\n spinner.text = 'Resolving collection…';\n return downloadCollection(deps, session, {\n game: argv.game,\n ref: argv.collection!,\n outDir,\n concurrency: argv.concurrency,\n dryRun: argv['dry-run'],\n includeOptional: argv.optional,\n retryAttempts: RETRY_ATTEMPTS,\n retryBaseDelayMs: RETRY_BASE_DELAY_MS,\n signal,\n onResolved: (members) => {\n global.setTotal(members);\n global.begin();\n },\n onStart: (member, i, total) => {\n const name = member.name ?? `mod ${member.modId}`;\n const verb = argv['dry-run'] ? 'Resolving' : 'Downloading';\n progress.start(`[${i}/${total}] ${verb} ${name}`);\n global.startFile(member.sizeBytes ?? 0);\n spinner.text = composeStatus(progress, global, Date.now() - runStart);\n },\n onFileProgress: (p) => {\n progress.update(p.receivedBytes, p.totalBytes);\n global.setCurrent(p.receivedBytes);\n spinner.text = composeStatus(progress, global, Date.now() - runStart);\n },\n // Failures persist a line above the spinner.\n onProgress: (r) => {\n progress.done();\n global.completeFile(r.ok);\n if (!r.ok) {\n const text = `mod ${r.modId} failed: ${r.error}`;\n spinner.stopAndPersist({ symbol: '✗', text });\n spinner.start();\n }\n },\n });\n}\n\n/** Stop the spinner with a final summary line and (for dry-run) the listing. */\nfunction finish(spinner: Ora, report: DownloadReport, dryRun: boolean, elapsedMs: number): void {\n if (dryRun) {\n spinner.stop();\n for (const r of report.results) {\n out.info(`mod ${r.modId}: ${r.files.join(', ') || '(none)'}`);\n }\n out.info(`dry run — ${report.succeeded} resolvable, ${report.failed} not`);\n return;\n }\n\n const took = `in ${clock(elapsedMs / 1000)}`;\n\n // For a single mod, list the files written; for a collection, just the tally.\n if (report.results.length === 1 && report.results[0]?.ok) {\n const files = report.results[0].files.map((f) => basename(f));\n spinner.succeed(`Downloaded ${files.join(', ')} ${took}`);\n return;\n }\n\n const msg = `${report.succeeded} downloaded, ${report.failed} failed ${took}`;\n if (report.failed > 0) spinner.warn(msg);\n else spinner.succeed(msg);\n}\n\n/**\n * Tracks a whole collection's transfer for a global ETA: known total bytes\n * (summed from the API's reported file sizes) against bytes downloaded so far\n * (completed files + the in-flight file). ETA uses the overall average rate.\n */\nclass GlobalProgress {\n private startedAt = Date.now();\n private totalBytes = 0;\n private completedBytes = 0;\n private currentBytes = 0;\n /** The in-flight file's known size from the API (for skipped files). */\n private currentSize = 0;\n\n /** Set the known total from the resolved member list. */\n setTotal(members: { sizeBytes?: number }[]): void {\n this.totalBytes = members.reduce((sum, m) => sum + (m.sizeBytes ?? 0), 0);\n }\n\n /** Begin the timer once downloading actually starts. */\n begin(): void {\n this.startedAt = Date.now();\n }\n\n /** Note the file about to download and its known size. */\n startFile(sizeBytes: number): void {\n this.currentBytes = 0;\n this.currentSize = sizeBytes;\n }\n\n /** Update the in-flight file's streamed byte count. */\n setCurrent(bytes: number): void {\n this.currentBytes = bytes;\n }\n\n /**\n * Settle the in-flight file. On success, credit its bytes toward the total —\n * the streamed amount, or the known size when nothing streamed (a file that\n * already existed and was skipped). On failure, discard it: a file that\n * errored out must not count as downloaded.\n */\n completeFile(ok: boolean): void {\n if (ok) {\n this.completedBytes += Math.max(this.currentBytes, this.currentSize);\n }\n this.currentBytes = 0;\n this.currentSize = 0;\n }\n\n /**\n * Render the secondary line:\n * total 18% 240 MB/1.3 GB elapsed 02:15 ETA 48:30\n * `elapsedMs` is the whole-run elapsed (so both ETAs share one clock source).\n */\n render(runElapsedMs: number): string {\n if (this.totalBytes <= 0) return '';\n const done = this.completedBytes + this.currentBytes;\n const elapsedMs = Math.max(Date.now() - this.startedAt, 1);\n const rate = done / (elapsedMs / 1000);\n const pct = Math.floor((done / this.totalBytes) * 100);\n\n const cols = [\n 'total',\n `${pct}%`,\n `${size(done)}/${size(this.totalBytes)}`,\n `elapsed ${clock(runElapsedMs / 1000)}`,\n ];\n if (rate > 0 && done > 0) {\n cols.push(`ETA ${clock((this.totalBytes - done) / rate)}`);\n }\n return cols.join(' ');\n }\n}\n\n/**\n * Tracks one file's transfer and renders the primary status line:\n * [3/476] SkyUI 47% 1.2/2.6 MB 3.4 MB/s ETA 00:04\n * Speed/ETA use the overall average (received ÷ elapsed) — stable, unlike\n * instantaneous chunk-to-chunk rates.\n */\nclass FileProgress {\n private startedAt = 0;\n private received = 0;\n private total = 0;\n private label = '';\n private active = false;\n\n /** Reset for a new file. `label` is the position + name, e.g. \"[3/476] SkyUI\". */\n start(label: string): void {\n this.startedAt = Date.now();\n this.received = 0;\n this.total = 0;\n this.label = label;\n this.active = true;\n }\n\n /** Record the latest byte counts (called from the download callback). */\n update(received: number, total: number): void {\n this.received = received;\n this.total = total;\n this.active = true;\n }\n\n /** Render the file line against *now* so elapsed/speed/ETA advance live. */\n render(): string {\n if (!this.label) return '';\n if (!this.active || this.received === 0) return this.label; // resolving…\n\n const elapsedMs = Math.max(Date.now() - this.startedAt, 1);\n const rate = this.received / (elapsedMs / 1000);\n const cols: string[] = [this.label];\n\n if (this.total > 0) {\n cols.push(`${Math.floor((this.received / this.total) * 100)}%`);\n cols.push(`${size(this.received)}/${size(this.total)}`);\n } else {\n cols.push(size(this.received));\n }\n cols.push(`${size(rate)}/s`);\n if (this.total > 0 && rate > 0) {\n cols.push(`ETA ${clock((this.total - this.received) / rate)}`);\n }\n return cols.join(' ');\n }\n\n /** Mark the file finished so the ticker stops rendering its line. */\n done(): void {\n this.active = false;\n }\n}\n\n/**\n * Compose the spinner status: the file line, and (for collections) a second\n * \"total\" line below it. The spinner glyph prefixes the first line.\n */\nfunction composeStatus(\n progress: FileProgress,\n global: GlobalProgress | null,\n runElapsedMs: number,\n): string {\n const fileLine = progress.render();\n const totalLine = global?.render(runElapsedMs) ?? '';\n // Indent the second line to align under the first (past the spinner glyph).\n return totalLine ? `${fileLine}\\n ${totalLine}` : fileLine;\n}\n\n/** Auto-scale bytes to KB/MB/GB with one decimal, e.g. \"1.3 GB\". */\nfunction size(bytes: number): string {\n if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;\n if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`;\n if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${Math.round(bytes)} B`;\n}\n\n/** Format seconds as zero-padded HH:MM:SS. */\nfunction clock(seconds: number): string {\n const s = Math.max(0, Math.floor(seconds));\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n const sec = s % 60;\n return [h, m, sec].map((n) => String(n).padStart(2, '0')).join(':');\n}\n","import type { Browser } from '@adapters/browser/Browser.js';\nimport type { CookieSource } from '@adapters/cookies/CookieSource.js';\nimport type { SessionStore } from '@adapters/session/SessionStore.js';\nimport { AuthError } from '@core/errors.js';\nimport type { Session } from '@core/types.js';\n\nexport interface ImportDeps {\n source: CookieSource;\n browser: Browser;\n store: SessionStore;\n}\n\nexport interface ImportParams {\n /** Cookie host suffix to import (e.g. `nexusmods.com`). */\n domainSuffix: string;\n /** Validate the cookies authenticate before saving. */\n validate: boolean;\n}\n\n/**\n * Import Nexus cookies from the user's real browser, optionally verify they\n * authenticate, and persist them as the session. No interactive login — the\n * user already cleared Cloudflare + signed in via their normal browser.\n */\nexport async function importSession(deps: ImportDeps, params: ImportParams): Promise<Session> {\n const cookies = await deps.source.read(params.domainSuffix);\n if (cookies.length === 0) {\n throw new AuthError(\n `no ${params.domainSuffix} cookies found in ${deps.source.browser} — ` +\n 'log into Nexus there first',\n );\n }\n\n let username = 'nexus-user';\n if (params.validate) {\n const session = await deps.browser.launch({ headful: false });\n try {\n await session.setCookies(cookies);\n if (!(await session.isLoggedIn())) {\n throw new AuthError(`imported ${deps.source.browser} cookies are not logged in to Nexus`);\n }\n username = (await session.resolveUsername()) ?? username;\n } finally {\n await session.close();\n }\n }\n\n const saved: Session = {\n username,\n cookies,\n capturedAt: new Date().toISOString(),\n };\n await deps.store.save(saved);\n return saved;\n}\n","import type { ArgumentsCamelCase, Argv, CommandModule } from 'yargs';\n\nimport { importSession } from '@app/importSession.js';\nimport { out } from '../output.js';\nimport { buildDeps, cookieSourceFor, fileCookieSource, NEXUS_COOKIE_DOMAIN } from '../wiring.js';\n\ninterface ImportArgs {\n from?: string;\n file?: string;\n validate: boolean;\n verbose: boolean;\n}\n\nexport const importCommand: CommandModule = {\n command: 'import',\n describe: 'Import Nexus cookies from your existing browser session',\n builder: (y: Argv) =>\n y\n // No yargs `default` here: a default value would always count as \"set\"\n // and trip `.conflicts('file', 'from')`, making `--file` unusable. The\n // chrome fallback is applied in the handler instead.\n .option('from', {\n type: 'string',\n describe:\n 'Browser to import cookies from: chrome (default), brave, edge, opera, vivaldi, arc, firefox, safari',\n })\n .option('file', {\n type: 'string',\n describe: 'Import from an exported cookie file (cookies.txt or JSON) instead of a browser',\n })\n .conflicts('file', 'from')\n .option('validate', {\n type: 'boolean',\n default: true,\n describe: 'Open a headless browser to confirm the cookies are logged in',\n }),\n handler: async (raw: ArgumentsCamelCase) => {\n const argv = raw as unknown as ImportArgs;\n const { browser, store } = buildDeps();\n try {\n const source = argv.file\n ? fileCookieSource(argv.file)\n : cookieSourceFor(argv.from ?? 'chrome');\n const session = await importSession(\n { source, browser, store },\n { domainSuffix: NEXUS_COOKIE_DOMAIN, validate: argv.validate },\n );\n out.success(\n `imported ${session.cookies.length} cookie(s) from ${source.browser}` +\n (session.username !== 'nexus-user' ? ` for ${session.username}` : ''),\n );\n process.exitCode = 0;\n } catch (e) {\n out.error(e, argv.verbose);\n process.exitCode = 1;\n }\n },\n};\n","import type { CommandModule } from 'yargs';\n\nimport { out } from '../output.js';\nimport { buildDeps } from '../wiring.js';\n\nexport const logoutCommand: CommandModule = {\n command: 'logout',\n describe: 'Clear the imported session',\n handler: async () => {\n const { store } = buildDeps();\n const existing = await store.load();\n await store.clear();\n if (existing) {\n out.success(`logged out (${existing.username})`);\n } else {\n out.info('no session to clear');\n }\n process.exitCode = 0;\n },\n};\n","import yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { downloadCommand } from './commands/download.js';\nimport { importCommand } from './commands/import.js';\nimport { logoutCommand } from './commands/logout.js';\n\nawait yargs(hideBin(process.argv))\n .scriptName('nexus')\n .usage('$0 <command> [options]')\n .option('verbose', {\n type: 'boolean',\n default: false,\n describe: 'Print full stack traces on error',\n global: true,\n })\n .command(importCommand)\n .command(logoutCommand)\n .command(downloadCommand)\n .demandCommand(1, 'a command is required')\n .strict()\n .help()\n .parseAsync();\n"],"names":["defaultSleep","SIGN_IN_HOST","url","body","headers"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlelieser/nexus-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Unofficial CLI for non-premium Nexus users.",
5
5
  "type": "module",
6
6
  "author": "Carlos Santos <hello@carlelieser.com>",