From e87860be9f4a0aed0ef1b2a67f9d1bf97bdf5d6e Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:32:51 +0100 Subject: [PATCH] feat(wallet): migrate cocod data to routstrd wallet directory --- IMPLEMENTATION.md | 253 ++++++++++++++++++++++ README.md | 11 + SKILL.md | 4 +- src/cli.test.ts | 4 +- src/cli.ts | 48 +++- src/daemon/index.ts | 32 ++- src/daemon/wallet/coco-client.npc.test.ts | 22 +- src/daemon/wallet/coco-client.test.ts | 44 +++- src/daemon/wallet/coco-client.ts | 93 +++++--- src/daemon/wallet/migration.test.ts | 107 +++++++++ src/daemon/wallet/migration.ts | 136 ++++++++++++ src/daemon/wallet/paths.ts | 33 +++ src/start-daemon.ts | 1 - 13 files changed, 721 insertions(+), 67 deletions(-) create mode 100644 IMPLEMENTATION.md create mode 100644 src/daemon/wallet/migration.test.ts create mode 100644 src/daemon/wallet/migration.ts create mode 100644 src/daemon/wallet/paths.ts diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 0000000..9e1f571 --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,253 @@ +# Migrate wallet storage from `~/.cocod` to `~/.routstrd/wallet` + +Task: [`routstrd-migrate-cocod-to-routstrd-files`](https://github.com/nodestrich/routstrd) +Event ID: `2f420644005154c5b106017c4a753854bb2660ba834cde7e6e267532cb5448e9` +Priority: 997 · Status: open + +--- + +## Baseline + +This plan is based on `coco-integration` at `86ac688` (after the default-mint, NPC/npubx.cash, and Windows-support changes). + +The task branch is currently based on `04f8c22`; rebase it onto the latest `coco-integration` before implementing this plan. + +## Goal + +Move the in-process Cashu wallet's persistent data from: + +```text +~/.cocod/config.json +~/.cocod/coco.db +``` + +to: + +```text +~/.routstrd/wallet/config.json +~/.routstrd/wallet/coco.db +``` + +The migration must preserve the mnemonic, proofs, default mint, and NPC identity. Existing users must not accidentally get a new wallet, and `cocod` and routstrd must never open the same database concurrently. + +Fresh installs must use `~/.routstrd/wallet` immediately. Existing installs must be migrated automatically and safely; merely continuing to run from `~/.cocod` is not sufficient for this task. + +--- + +## Important corrections to the previous plan + +### 1. Do not rename or migrate `cocod.sock` + +The in-process `coco-core` wallet does not expose a Unix socket. `cocod.sock` belongs to the legacy external `cocod` daemon and is used only to determine whether that daemon is still alive. + +There should therefore be no `wallet.sock`. Do not copy a socket inode into the new directory. + +### 2. Keep legacy-process paths separate from wallet-data paths + +On the current base, `src/daemon/wallet/coco-client.ts` derives all of these from one `CONFIG_DIR`: + +- wallet config and database; +- legacy cocod socket; +- legacy cocod PID/process lock. + +That coupling must be removed. After migration: + +- wallet data comes from `~/.routstrd/wallet`; +- probes and shutdown logic for an external legacy cocod continue to use `~/.cocod/cocod.sock` and `~/.cocod/cocod.pid`; +- routstrd uses `~/.routstrd/wallet/wallet.pid` as the in-process wallet lock. + +While routstrd owns the migrated wallet, it should also retain the existing legacy-cocod exclusion mechanism (claiming the legacy `cocod.pid`) so an old cocod cannot be started against a stale or partially migrated legacy wallet. Acquisition and release of both locks must be rollback-safe. + +### 3. Do not rename `cocodPath` + +`RoutstrdConfig.cocodPath` is an executable path for the external compatibility client, not a wallet storage path. Renaming it to `walletPath` would change its meaning and would not help this migration. + +The current daemon creates `createCocoClient()` directly and injects it into `createWalletAdapter()`, so `cocodPath` is effectively bypassed on the normal in-process path. Cleanup or removal of that compatibility setting is a separate task. + +### 4. Do not implement fallback as the steady state + +A resolver that permanently falls back to `~/.cocod` does not move the data. Legacy detection is needed to initiate migration, but successful startup should resolve to the canonical directory afterward. + +### 5. Account for the updated base + +The latest `coco-integration` adds: + +- `defaultMintUrl` persistence in the wallet's `config.json`; +- the NPC plugin, whose Nostr identity is derived from the same mnemonic; +- `src/daemon/wallet/coco-client.npc.test.ts`; +- Windows support and `USERPROFILE` fallback. + +Migration must preserve the complete config JSON rather than reconstructing selected fields. Path construction must use `path.join` rather than hard-coded `/` separators. + +--- + +## Current runtime path references on the latest base + +### Wallet data and legacy process coordination + +| File | Current responsibility | Required change | +|---|---|---| +| `src/daemon/wallet/coco-client.ts` | Uses one `.cocod` directory for config, DB, socket, and PID; owns the in-process wallet and legacy-cocod guard | Split canonical data paths from legacy process paths; migrate before opening the DB; use `wallet.pid` for the in-process lock | +| `src/daemon/wallet/cocod-client.ts` | External cocod compatibility client; defaults to `.cocod/cocod.sock` | Keep legacy defaults and clarify that they are external-cocod paths | +| `src/cli.ts` | Initializes `.cocod`; two restart paths wait on `.cocod/cocod.pid` | Initialize/migrate the canonical wallet and wait on `wallet.pid` | +| `src/utils/config.ts` | Defines `~/.routstrd`, but no wallet subdirectory | Export canonical and legacy wallet/process path helpers or constants | +| `src/daemon/index.ts` | Calls `createCocoClient()` before building the adapter | Ensure migration occurs before the client opens the DB | + +### Tests and documentation + +| File | Required update | +|---|---| +| `src/cli.test.ts` | Change fresh-wallet expectations and add migration coverage | +| `src/daemon/wallet/coco-client.test.ts` | Update data/lock paths and add migration/dual-lock tests | +| `src/daemon/wallet/coco-client.npc.test.ts` | Use the canonical wallet layout in fixtures; verify migrated mnemonic/config still drives NPC identity | +| `README.md` | Document the new location and migration behavior; retain legacy cocod terminology only where discussing compatibility | +| `SKILL.md` | Update wallet storage and environment-variable documentation | + +The fixture `src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz` keeps its name because it records the fixture's provenance. + +--- + +## Target path model + +Define the path model in one dependency-light module (for example `src/daemon/wallet/paths.ts`, or in `src/utils/config.ts` if that does not introduce a cycle): + +```text +ROUTSTRD config root process.env.ROUTSTRD_DIR || ~/.routstrd +canonical wallet directory process.env.ROUTSTRD_WALLET_DIR || /wallet +canonical wallet config /config.json +canonical wallet database /coco.db +canonical wallet lock process.env.ROUTSTRD_WALLET_PID || /wallet.pid + +legacy cocod directory process.env.COCOD_DIR || ~/.cocod +legacy cocod socket process.env.COCOD_SOCKET || /cocod.sock +legacy cocod PID process.env.COCOD_PID || /cocod.pid +``` + +Use `HOME` with `USERPROFILE` fallback, matching the updated Windows-support base. Use functions or injectable path objects where tests need to change environment variables after module import. + +`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` remain compatibility controls for locating an old external cocod. They must not redirect the new in-process wallet away from `~/.routstrd/wallet`. `ROUTSTRD_WALLET_DIR` is the new explicit data-directory override. + +--- + +## Implementation plan + +### Phase 1 — Centralize and separate paths + +1. Add the canonical wallet and legacy cocod path definitions above. +2. Remove the local `.cocod` path construction from `coco-client.ts` and `cli.ts`. +3. Change `CreateCocoClientOptions` so tests/tooling can independently override: + - wallet data directory; + - wallet lock path; + - legacy cocod socket and PID paths. +4. Keep `cocod-client.ts` pointed at the legacy socket. Do not make it import a resolver that prefers the new wallet directory. + +### Phase 2 — Add a safe automatic migration primitive + +Add an idempotent `migrateLegacyWallet()` helper with injectable paths/filesystem operations for tests. + +Preconditions and behavior: + +1. If the canonical wallet already contains both `config.json` and `coco.db`, return `already-current` and do not touch legacy data. +2. If neither canonical nor legacy wallet data exists, return `fresh`; initialization will create the canonical directory. +3. If only the legacy wallet exists: + - first verify that legacy cocod is not running using the existing PID and socket guard; + - create a private staging directory under `~/.routstrd` with mode `0700`; + - copy the complete `config.json` and `coco.db` into staging without parsing or rewriting them; + - apply `0600` to both files; + - validate that the staged files exist and have the expected byte sizes; + - atomically rename the staged directory to `wallet`; + - only after the canonical directory is committed, remove the legacy `config.json` and `coco.db`; + - never copy `cocod.sock` or `cocod.pid`. +4. If canonical storage is partial, legacy storage is partial, both contain wallet data, or files conflict, stop with an actionable error. Never merge databases and never generate a new mnemonic over an ambiguous state. +5. Clean up an uncommitted staging directory after failure. A committed canonical wallet remains authoritative if cleanup of the old files fails; report that cleanup warning clearly. + +The migration should preserve unknown config fields, including `defaultMintUrl`, and preserve the database byte-for-byte. This also preserves the NPC identity because that identity is derived from the mnemonic. + +A staging copy plus atomic directory rename is preferred over two independent file renames: a crash must not expose a half-created canonical wallet. Copying also permits a clear rollback before commit and supports a legacy directory located on another filesystem through `COCOD_DIR`. + +### Phase 3 — Run migration from every wallet-opening path + +1. **CLI onboarding/init:** run migration before `initializeWallet()`. Only initialize a new mnemonic when migration reports `fresh`. +2. **Daemon direct startup:** run migration before `createCocoClient()` opens `coco.db`. This covers users who invoke the daemon without rerunning onboarding. +3. **Start/restart/update/service paths:** retain `stopLegacyCocod()` before migration/startup where those paths already stop legacy cocod. Direct daemon startup should refuse with the existing actionable error rather than silently running two wallet engines. +4. Print a concise success message showing old and new directories, but never print config contents or the mnemonic during migration. + +A separate `routstrd wallet migrate` command is optional as a manual recovery/preview entry point, but it must call the same migration primitive. It is not a substitute for automatic migration. + +### Phase 4 — Separate process locking + +Refactor the current `claimLegacyCocodPidFile()` behavior into explicit responsibilities: + +1. Claim `wallet.pid` for the lifetime of the in-process wallet to prevent two routstrd wallet instances from opening `coco.db`. +2. Continue claiming the legacy `cocod.pid` while routstrd is active, after verifying no real cocod owns it, to prevent an old cocod from starting. +3. If either claim fails, release any claim already acquired before returning the error. +4. On `CocodClient.dispose()`, startup failure, and daemon shutdown, release only PID files still owned by the current process. +5. Keep `stopLegacyCocod()` and `assertLegacyCocodNotRunning()` operating only on legacy cocod paths. +6. Update both PID-release waits in `src/cli.ts` (`restartDaemonsAfterUpdate` and `restart`) to wait for the canonical `wallet.pid` rather than `.cocod/cocod.pid`. + +There is no canonical wallet socket. + +### Phase 5 — Update initialization and client defaults + +1. Make `initializeWallet()` default to the canonical wallet directory. +2. Preserve directory mode `0700` and config mode `0600` on both migrated and fresh wallets. +3. In `createCocoClient()`, derive `config.json` and `coco.db` from the canonical wallet directory, but take legacy guard paths separately. +4. Update the wallet-access comment near `unlock()` to reference `~/.routstrd/wallet/config.json`. +5. Leave `CocodClient`, `resolveCocodExecutable()`, and `cocodPath` compatibility behavior unchanged. + +### Phase 6 — Tests + +Add or update tests for: + +- fresh initialization creates `~/.routstrd/wallet`, not `.cocod`; +- a legacy config and database migrate byte-for-byte; +- `defaultMintUrl` and unknown config fields survive migration; +- the NPC-derived identity is unchanged after migration; +- migration never copies socket or PID files; +- migration refuses while a real legacy cocod is running; +- stale legacy socket/PID handling remains safe; +- an existing complete canonical wallet wins without modifying it; +- partial canonical, partial legacy, and conflicting dual-wallet states fail without generating a mnemonic; +- staging cleanup and retry after an interrupted migration; +- custom `ROUTSTRD_DIR`, `ROUTSTRD_WALLET_DIR`, and legacy `COCOD_DIR` paths; +- Windows-compatible path construction; +- acquiring the second process lock rolls back the first on failure; +- `dispose()` releases both owned locks and does not unlink another process's lock; +- both CLI restart paths wait for `wallet.pid`. + +Keep `cocod-0.0.24-wallet.db.gz` unchanged and continue using it to prove the migrated database opens successfully with the current in-process wallet. + +### Phase 7 — Documentation + +Update `README.md` and `SKILL.md` to state: + +- wallet data is stored in `~/.routstrd/wallet`; +- existing `~/.cocod` data is migrated automatically on first startup; +- users must back up the mnemonic before migration; +- `ROUTSTRD_WALLET_DIR` overrides the canonical wallet directory; +- `COCOD_DIR`, `COCOD_SOCKET`, `COCOD_PID`, and `cocodPath` refer only to legacy external-cocod compatibility. + +Do not replace every use of the word `cocod`: references to the external daemon, compatibility client, legacy guard, package, and fixture are still accurate. + +--- + +## Acceptance criteria + +- A fresh install creates wallet data only under `~/.routstrd/wallet`. +- Starting from a valid `.cocod` wallet results in the same config and database under the canonical directory without changing balances, mnemonic-derived NPC identity, or default mint. +- No startup path silently creates a new mnemonic when recoverable legacy data exists. +- A running legacy cocod blocks migration and database opening. +- The new wallet directory contains no copied Unix socket and uses `wallet.pid` only as an in-process lock. +- Legacy cocod probing and exclusion continue to use `.cocod/cocod.sock` and `.cocod/cocod.pid`. +- The implementation works with `USERPROFILE`/Windows path construction and custom directory overrides. +- All wallet, CLI, typecheck, and build tests pass on the rebased `coco-integration` branch. + +--- + +## Suggested commit sequence + +1. `refactor(wallet): separate routstrd wallet paths from legacy cocod paths` +2. `feat(wallet): atomically migrate legacy cocod wallet data` +3. `fix(wallet): use independent routstrd and legacy cocod process locks` +4. `test(wallet): cover migration, conflicts, NPC identity, and lock rollback` +5. `docs: document routstrd wallet storage and legacy migration` diff --git a/README.md b/README.md index f944210..f776e15 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,17 @@ Response: } ``` +## Wallet storage + +The in-process Cashu wallet stores its mnemonic and proof database in +`~/.routstrd/wallet/`. On first startup, an existing wallet in `~/.cocod/` is +migrated automatically after routstrd verifies that the legacy cocod daemon is +not running. Back up your mnemonic before upgrading. + +Set `ROUTSTRD_WALLET_DIR` to override the canonical wallet directory. The +`COCOD_DIR`, `COCOD_SOCKET`, and `COCOD_PID` variables are retained only for +locating and excluding a legacy external cocod process. + ## Configuration Configuration is stored in `~/.routstrd/config.json`: diff --git a/SKILL.md b/SKILL.md index 0ac5272..1ac182a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -170,7 +170,9 @@ Refresh routstr21 models from Nostr and re-run integrations for all registered c | Variable | Default | Description | |----------|---------|-------------| | `ROUTSTRD_DIR` | `~/.routstrd` | Config directory | -| `COCOD_DIR` | `~/.cocod` | Wallet config directory | +| `ROUTSTRD_WALLET_DIR` | `~/.routstrd/wallet` | In-process Cashu wallet data directory | +| `ROUTSTRD_WALLET_PID` | `/wallet.pid` | In-process wallet lock path | +| `COCOD_DIR` | `~/.cocod` | Legacy external cocod compatibility directory | ### `routstrd mode` diff --git a/src/cli.test.ts b/src/cli.test.ts index a617dfb..0886893 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -24,7 +24,7 @@ afterEach(() => { describe("initializeWallet", () => { test("creates the wallet directory and config with restrictive permissions", () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); initializeWallet(walletDir); @@ -39,7 +39,7 @@ describe("initializeWallet", () => { }); test("repairs permissions without replacing an existing wallet", () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); const walletConfig = join(walletDir, "config.json"); const existingConfig = JSON.stringify({ mnemonic: "existing seed" }); diff --git a/src/cli.ts b/src/cli.ts index cd38571..5fab864 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -17,6 +17,7 @@ import { } from "./utils/clients"; import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs"; import { execSync } from "child_process"; +import { dirname, join } from "path"; import { CONFIG_DIR, DB_PATH, @@ -27,7 +28,18 @@ import { } from "./utils/config"; import { logger } from "./utils/logger"; import { setupIntegration, runIntegrationsForClients } from "./integrations"; -import { stopLegacyCocod } from "./daemon/wallet/coco-client"; +import { + assertLegacyCocodNotRunning, + claimLegacyCocodPidFile, + stopLegacyCocod, +} from "./daemon/wallet/coco-client"; +import { migrateLegacyWallet } from "./daemon/wallet/migration"; +import { + legacyCocodPidPath, + legacyCocodSocketPath, + walletDir as defaultWalletDir, + walletPidPath, +} from "./daemon/wallet/paths"; import { getClientsList } from "./utils/clients"; import * as QRCode from "qrcode"; import { normalizeNostrPubkey, npubFromPubkey, npubFromSecretKey } from "./utils/nip98"; @@ -85,12 +97,8 @@ async function printLightningInvoice(invoice: string): Promise { console.log(`${qr}\nInvoice:\n${invoice}`); } -export function initializeWallet( - walletDir = - process.env.COCOD_DIR || - `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`, -): void { - const walletConfig = `${walletDir}/config.json`; +export function initializeWallet(walletDir = defaultWalletDir()): void { + const walletConfig = join(walletDir, "config.json"); // The wallet directory and config contain the plaintext seed phrase. Correct // permissions on existing installations as well as newly created ones. @@ -144,8 +152,8 @@ async function restartDaemonsAfterUpdate(): Promise { await callDaemon("/stop", { method: "POST" }); - // Wait for HTTP health check to fail AND pidfile to be released. - const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + // Wait for HTTP health check to fail AND wallet lock to be released. + const pidFilePath = walletPidPath(); for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); const healthDown = !(await isDaemonRunning()); @@ -232,6 +240,24 @@ async function initDaemon(): Promise { } console.log(`Database will be stored at: ${DB_PATH}`); + const migration = await migrateLegacyWallet({ + assertLegacyStopped: () => + assertLegacyCocodNotRunning({ + socketPath: legacyCocodSocketPath(), + pidFilePath: legacyCocodPidPath(), + }), + acquireLegacyLock: () => { + mkdirSync(dirname(legacyCocodPidPath()), { + recursive: true, + mode: 0o700, + }); + return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() }); + }, + }); + if (migration.status === "migrated") { + console.log(`Migrated wallet from ${migration.from} to ${migration.to}.`); + for (const warning of migration.cleanupWarnings) console.warn(warning); + } initializeWallet(); await startDaemon({ port: String(config.port || 8008), host: config.host || undefined }); @@ -1833,8 +1859,8 @@ program console.log("Stopping daemon..."); await callDaemon("/stop", { method: "POST" }); - // Wait for HTTP health check to fail AND pidfile to be released. - const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + // Wait for HTTP health check to fail AND wallet lock to be released. + const pidFilePath = walletPidPath(); for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); const healthDown = !(await isDaemonRunning()); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 6dc587a..0e3bb4b 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -54,7 +54,18 @@ import { createDaemonRequestHandler } from "./http"; import { FileRequestResponseLogSink } from "./request-response-log-sink"; import { refreshModelsAndIntegrations } from "../integrations"; import { RoutstrClient } from "@routstr/sdk"; -import { createCocoClient } from "./wallet/coco-client"; +import { mkdirSync } from "fs"; +import { dirname } from "path"; +import { + assertLegacyCocodNotRunning, + claimLegacyCocodPidFile, + createCocoClient, +} from "./wallet/coco-client"; +import { migrateLegacyWallet } from "./wallet/migration"; +import { + legacyCocodPidPath, + legacyCocodSocketPath, +} from "./wallet/paths"; // Global error handlers — the daemon is spawned detached with stdout/stderr // redirected to a file, so without these, uncaught async errors would kill @@ -115,6 +126,25 @@ async function main(): Promise { const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } = createModelService(modelManager, providerManager, store); + const migration = await migrateLegacyWallet({ + assertLegacyStopped: () => + assertLegacyCocodNotRunning({ + socketPath: legacyCocodSocketPath(), + pidFilePath: legacyCocodPidPath(), + }), + acquireLegacyLock: () => { + mkdirSync(dirname(legacyCocodPidPath()), { + recursive: true, + mode: 0o700, + }); + return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() }); + }, + }); + if (migration.status === "migrated") { + startupProgress(`Wallet migrated from ${migration.from} to ${migration.to}.`); + for (const warning of migration.cleanupWarnings) logger.warn(warning); + } + const walletClient = await createCocoClient(); // ── Auto-refill configuration ──────────────────────────────── diff --git a/src/daemon/wallet/coco-client.npc.test.ts b/src/daemon/wallet/coco-client.npc.test.ts index d348473..b1ae0fc 100644 --- a/src/daemon/wallet/coco-client.npc.test.ts +++ b/src/daemon/wallet/coco-client.npc.test.ts @@ -49,7 +49,7 @@ const tempDirs: string[] = []; function makeWalletDir(): string { const dir = mkdtempSync(join(tmpdir(), "routstrd-coco-npc-test-")); tempDirs.push(dir); - const walletDir = join(dir, ".cocod"); + const walletDir = join(dir, "wallet"); mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -58,6 +58,14 @@ function makeWalletDir(): string { return walletDir; } +function testClientOptions(walletDir = makeWalletDir()) { + return { + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }; +} + function resetNpcState(): void { npcState.info = { name: "alice", pubkey: "ab".repeat(32) }; npcState.usernameResult = { success: true }; @@ -74,7 +82,7 @@ afterEach(() => { describe("in-process coco client NPC integration", () => { it("registers the plugin and exposes the wallet's NPC lightning address", async () => { - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const info = await client.getNpcAddress(); expect(info).toEqual({ @@ -89,7 +97,7 @@ describe("in-process coco client NPC integration", () => { it("falls back to the npub address when no username is set", async () => { npcState.info = { name: null, pubkey: "ab".repeat(32) }; - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const info = await client.getNpcAddress(); expect(info.address).toStartWith("npub1"); @@ -106,7 +114,7 @@ describe("in-process coco client NPC integration", () => { success: false, pr: { amount: 21, mints: ["https://mint.example.com"] }, }; - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const result = await client.setNpcUsername("bob"); expect(result).toEqual({ @@ -122,7 +130,7 @@ describe("in-process coco client NPC integration", () => { }); it("confirms the claim fee payment when requested", async () => { - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { const result = await client.setNpcUsername("bob", true); expect(result).toEqual({ success: true }); @@ -135,7 +143,7 @@ describe("in-process coco client NPC integration", () => { }); it("triggers a manual quote sync", async () => { - const client = await createCocoClient({ configDir: makeWalletDir() }); + const client = await createCocoClient(testClientOptions()); try { await client.syncNpc(); expect(npcState.syncCalls).toBe(1); @@ -146,7 +154,7 @@ describe("in-process coco client NPC integration", () => { it("rejects NPC calls with a clear error when the plugin is disabled", async () => { const client = await createCocoClient({ - configDir: makeWalletDir(), + ...testClientOptions(), enableNpc: false, }); try { diff --git a/src/daemon/wallet/coco-client.test.ts b/src/daemon/wallet/coco-client.test.ts index 8ce421e..b9dd10a 100644 --- a/src/daemon/wallet/coco-client.test.ts +++ b/src/daemon/wallet/coco-client.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, mock } from "bun:test"; import { gunzipSync } from "bun"; import { + existsSync, mkdtempSync, mkdirSync, readFileSync, @@ -43,7 +44,7 @@ function socketOnly(path: string): boolean { describe("default mint functionality", () => { it("automatically adds default mint when no mints exist", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -55,8 +56,19 @@ describe("default mint functionality", () => { }), ); - const client = await createCocoClient({ configDir: walletDir }); + const client = await createCocoClient({ + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }); try { + expect(readFileSync(join(walletDir, "wallet.pid"), "utf8")).toBe( + String(process.pid), + ); + expect(readFileSync(join(walletDir, "legacy-cocod.pid"), "utf8")).toBe( + String(process.pid), + ); + const mints = await client.listMints(); expect(mints).toContain("https://mint.cubabitcoin.org"); @@ -65,10 +77,12 @@ describe("default mint functionality", () => { } finally { await client.dispose?.(); } + expect(existsSync(join(walletDir, "wallet.pid"))).toBe(false); + expect(existsSync(join(walletDir, "legacy-cocod.pid"))).toBe(false); }); it("respects existing default mint in config", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); const configuredDefault = "https://mint.cubabitcoin.org"; mkdirSync(walletDir, { recursive: true }); @@ -84,7 +98,11 @@ describe("default mint functionality", () => { }), ); - const client = await createCocoClient({ configDir: walletDir }); + const client = await createCocoClient({ + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }); try { // Should respect the configured default mint const defaultMint = await client.getDefaultMint(); @@ -99,7 +117,7 @@ describe("default mint functionality", () => { }); it("allows setting default mint to an already trusted mint", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); mkdirSync(walletDir, { recursive: true }); writeFileSync( join(walletDir, "config.json"), @@ -111,7 +129,11 @@ describe("default mint functionality", () => { }), ); - const client = await createCocoClient({ configDir: walletDir }); + const client = await createCocoClient({ + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), + }); try { // The initial default should be the auto-added Cuba mint const initialDefault = await client.getDefaultMint(); @@ -133,7 +155,7 @@ describe("default mint functionality", () => { describe("legacy cocod wallet migration", () => { it("opens an existing unencrypted config and preserves database balances", async () => { - const walletDir = join(makeTempDir(), ".cocod"); + const walletDir = join(makeTempDir(), "wallet"); const mintUrl = "https://mint.example.com"; mkdirSync(walletDir, { recursive: true }); @@ -164,7 +186,9 @@ describe("legacy cocod wallet migration", () => { // NPC is disabled here: this test verifies database migration, and the // real plugin would otherwise open a websocket to npubx.cash. const client = await createCocoClient({ - configDir: walletDir, + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), enableNpc: false, }); try { @@ -177,7 +201,9 @@ describe("legacy cocod wallet migration", () => { // Prove the migrated schema remains reopenable and the pre-existing // proofs survive a complete in-process wallet restart. const reopenedClient = await createCocoClient({ - configDir: walletDir, + walletDir, + legacySocketPath: join(walletDir, "legacy-cocod.sock"), + legacyPidPath: join(walletDir, "legacy-cocod.pid"), enableNpc: false, }); try { diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 2b351a3..3a90cb0 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -16,13 +16,14 @@ import { finalizeEvent, nip19, type EventTemplate } from "nostr-tools"; import { closeSync, existsSync, + mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "fs"; -import { join } from "path"; +import { dirname, join } from "path"; import { mnemonicToSeedSync } from "@scure/bip39"; import type { CocodClient, @@ -31,20 +32,15 @@ import type { NpcUsernameResult, } from "./cocod-client"; import { logger } from "../../utils/logger"; +import { + legacyCocodPidPath, + legacyCocodSocketPath, + walletDir as defaultWalletDir, + walletPidPath as defaultWalletPidPath, +} from "./paths"; const NPC_DEFAULT_BASE_URL = "https://npubx.cash"; -const CONFIG_DIR = - process.env.COCOD_DIR || - `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`; - -const CONFIG_FILE = join(CONFIG_DIR, "config.json"); -const DB_PATH = join(CONFIG_DIR, "coco.db"); -const LEGACY_COCOD_SOCKET = - process.env.COCOD_SOCKET || join(CONFIG_DIR, "cocod.sock"); -const LEGACY_COCOD_PID_FILE = - process.env.COCOD_PID || join(CONFIG_DIR, "cocod.pid"); - const STALE_SOCKET_ERROR_CODES = new Set([ "ECONNREFUSED", "ENOENT", @@ -167,7 +163,7 @@ function createCocoLogger(bindings: Record = {}): CocoLogger { }; } -function loadConfig(configFile: string = CONFIG_FILE): CocodConfig { +function loadConfig(configFile: string): CocodConfig { if (!existsSync(configFile)) { throw new Error( `Config file not found at ${configFile}. Run 'routstrd onboard' first.`, @@ -182,10 +178,7 @@ function loadConfig(configFile: string = CONFIG_FILE): CocodConfig { return config; } -function saveConfig( - config: CocodConfig, - configFile: string = CONFIG_FILE, -): void { +function saveConfig(config: CocodConfig, configFile: string): void { const temporaryFile = `${configFile}.${process.pid}.tmp`; try { writeFileSync(temporaryFile, JSON.stringify(config, null, 2), { @@ -239,8 +232,8 @@ function hasErrorCode(error: unknown, codes: Set): boolean { export async function assertLegacyCocodNotRunning( options: LegacyCocodGuardOptions = {}, ): Promise { - const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; - const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const socketPath = options.socketPath || legacyCocodSocketPath(); + const pidFilePath = options.pidFilePath || legacyCocodPidPath(); const pathExists = options.pathExists || existsSync; const readFile = options.readFile || ((path) => readFileSync(path, "utf-8")); const isProcessRunning = options.isProcessRunning || defaultIsProcessRunning; @@ -325,8 +318,8 @@ export async function assertLegacyCocodNotRunning( export async function stopLegacyCocod( options: LegacyCocodStopOptions = {}, ): Promise { - const socketPath = options.socketPath || LEGACY_COCOD_SOCKET; - const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + const socketPath = options.socketPath || legacyCocodSocketPath(); + const pidFilePath = options.pidFilePath || legacyCocodPidPath(); const pathExists = options.pathExists || existsSync; const readFile = options.readFile || ((path: string) => readFileSync(path, "utf-8")); @@ -416,7 +409,14 @@ export async function stopLegacyCocod( export function claimLegacyCocodPidFile( options: LegacyCocodPidClaimOptions = {}, ): () => void { - const pidFilePath = options.pidFilePath || LEGACY_COCOD_PID_FILE; + return claimPidFile({ + ...options, + pidFilePath: options.pidFilePath || legacyCocodPidPath(), + }); +} + +function claimPidFile(options: LegacyCocodPidClaimOptions & { pidFilePath: string }): () => void { + const pidFilePath = options.pidFilePath; const pid = options.pid ?? process.pid; const openExclusive = options.openExclusive || ((path: string) => openSync(path, "wx", 0o600)); @@ -507,10 +507,15 @@ export function claimLegacyCocodPidFile( } export interface CreateCocoClientOptions { - /** Override the wallet directory, primarily for migration tests and tooling. */ + /** Override the canonical wallet data directory. */ + walletDir?: string; + /** Deprecated alias retained for existing callers during migration. */ configDir?: string; - socketPath?: string; - pidFilePath?: string; + /** Override the in-process wallet lock path. */ + walletPidPath?: string; + /** Override legacy external-cocod coordination paths. */ + legacySocketPath?: string; + legacyPidPath?: string; /** Set to false to skip NPC (npubx.cash) plugin registration. Default: true. */ enableNpc?: boolean; /** NPC server base URL. Default: https://npubx.cash */ @@ -520,20 +525,36 @@ export interface CreateCocoClientOptions { export async function createCocoClient( options: CreateCocoClientOptions = {}, ): Promise { - const configDir = options.configDir || CONFIG_DIR; + const configDir = options.walletDir || options.configDir || defaultWalletDir(); const configFile = join(configDir, "config.json"); const dbPath = join(configDir, "coco.db"); - const socketPath = - options.socketPath || - (options.configDir ? join(configDir, "cocod.sock") : LEGACY_COCOD_SOCKET); - const pidFilePath = - options.pidFilePath || - (options.configDir ? join(configDir, "cocod.pid") : LEGACY_COCOD_PID_FILE); + const walletPidFile = + options.walletPidPath || + (options.walletDir || options.configDir + ? join(configDir, "wallet.pid") + : defaultWalletPidPath()); + const legacySocket = options.legacySocketPath || legacyCocodSocketPath(); + const legacyPidFile = options.legacyPidPath || legacyCocodPidPath(); const npcBaseUrl = options.npcBaseUrl || NPC_DEFAULT_BASE_URL; const npcAddressDomain = new URL(npcBaseUrl).host; - await assertLegacyCocodNotRunning({ socketPath, pidFilePath }); - const releaseLegacyPidClaim = claimLegacyCocodPidFile({ pidFilePath }); + await assertLegacyCocodNotRunning({ + socketPath: legacySocket, + pidFilePath: legacyPidFile, + }); + // The canonical wallet directory is created by initialization/migration. + // Keep a legacy PID claim as an exclusion fence for old cocod binaries. + mkdirSync(dirname(legacyPidFile), { recursive: true, mode: 0o700 }); + const releaseWalletPidClaim = claimPidFile({ pidFilePath: walletPidFile }); + let releaseLegacyPidClaim: () => void; + try { + releaseLegacyPidClaim = claimLegacyCocodPidFile({ + pidFilePath: legacyPidFile, + }); + } catch (error) { + releaseWalletPidClaim(); + throw error; + } let database: Database | undefined; let coco: Awaited> | undefined; @@ -616,6 +637,7 @@ export async function createCocoClient( } catch (error) { database?.close(); releaseLegacyPidClaim(); + releaseWalletPidClaim(); throw error; } @@ -653,7 +675,7 @@ export async function createCocoClient( async unlock(_passphrase: string): Promise { // coco-core does not support passphrase locking. - // Wallet access is controlled via the mnemonic in ~/.cocod/config.json. + // Wallet access is controlled by ~/.routstrd/wallet/config.json. return "wallet does not require unlocking"; }, @@ -756,6 +778,7 @@ export async function createCocoClient( database.close(); } finally { releaseLegacyPidClaim(); + releaseWalletPidClaim(); } } }, diff --git a/src/daemon/wallet/migration.test.ts b/src/daemon/wallet/migration.test.ts new file mode 100644 index 0000000..e61892a --- /dev/null +++ b/src/daemon/wallet/migration.test.ts @@ -0,0 +1,107 @@ +import { afterEach, describe, expect, it, mock } from "bun:test"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { migrateLegacyWallet } from "./migration"; + +const roots: string[] = []; +function root(): string { + const path = mkdtempSync(join(tmpdir(), "routstrd-migration-")); + roots.push(path); + return path; +} +afterEach(() => { + for (const path of roots.splice(0)) rmSync(path, { recursive: true, force: true }); +}); + +describe("migrateLegacyWallet", () => { + it("copies wallet data byte-for-byte and leaves process files behind", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + const walletDir = join(base, ".routstrd", "wallet"); + mkdirSync(legacyDir); + const config = JSON.stringify({ + mnemonic: "seed words", + encrypted: false, + defaultMintUrl: "https://mint.example", + unknown: { preserved: true }, + }); + const database = new Uint8Array([0, 1, 2, 3, 255]); + writeFileSync(join(legacyDir, "config.json"), config); + writeFileSync(join(legacyDir, "coco.db"), database); + writeFileSync(join(legacyDir, "cocod.pid"), "123"); + writeFileSync(join(legacyDir, "cocod.sock"), "not-a-real-socket"); + const assertStopped = mock(async () => {}); + const releaseLock = mock(() => {}); + const acquireLock = mock(() => releaseLock); + + const result = await migrateLegacyWallet({ + walletDir, + legacyDir, + assertLegacyStopped: assertStopped, + acquireLegacyLock: acquireLock, + }); + + expect(result.status).toBe("migrated"); + expect(assertStopped).toHaveBeenCalledTimes(2); + expect(acquireLock).toHaveBeenCalledTimes(1); + expect(releaseLock).toHaveBeenCalledTimes(1); + expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe(config); + expect(readFileSync(join(walletDir, "coco.db"))).toEqual(Buffer.from(database)); + expect(statSync(walletDir).mode & 0o777).toBe(0o700); + expect(statSync(join(walletDir, "config.json")).mode & 0o777).toBe(0o600); + expect(existsSync(join(walletDir, "cocod.pid"))).toBe(false); + expect(existsSync(join(walletDir, "cocod.sock"))).toBe(false); + expect(existsSync(join(legacyDir, "config.json"))).toBe(false); + expect(existsSync(join(legacyDir, "coco.db"))).toBe(false); + expect(existsSync(join(legacyDir, "cocod.pid"))).toBe(true); + expect(existsSync(join(legacyDir, "cocod.sock"))).toBe(true); + }); + + it("treats a config-only initialized wallet as migratable", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + const walletDir = join(base, ".routstrd", "wallet"); + mkdirSync(legacyDir); + writeFileSync(join(legacyDir, "config.json"), '{"mnemonic":"seed"}'); + + expect((await migrateLegacyWallet({ walletDir, legacyDir })).status).toBe("migrated"); + expect(existsSync(join(walletDir, "config.json"))).toBe(true); + expect(existsSync(join(walletDir, "coco.db"))).toBe(false); + }); + + it("refuses a database without its mnemonic config", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + mkdirSync(legacyDir); + writeFileSync(join(legacyDir, "coco.db"), "database"); + + await expect(migrateLegacyWallet({ walletDir: join(base, "wallet"), legacyDir })).rejects.toThrow( + "without config.json", + ); + }); + + it("refuses when both canonical and legacy configs exist", async () => { + const base = root(); + const legacyDir = join(base, ".cocod"); + const walletDir = join(base, ".routstrd", "wallet"); + mkdirSync(legacyDir, { recursive: true }); + mkdirSync(walletDir, { recursive: true }); + writeFileSync(join(legacyDir, "config.json"), "legacy"); + writeFileSync(join(walletDir, "config.json"), "current"); + + await expect(migrateLegacyWallet({ walletDir, legacyDir })).rejects.toThrow( + "both", + ); + expect(readFileSync(join(walletDir, "config.json"), "utf8")).toBe("current"); + expect(readFileSync(join(legacyDir, "config.json"), "utf8")).toBe("legacy"); + }); +}); diff --git a/src/daemon/wallet/migration.ts b/src/daemon/wallet/migration.ts new file mode 100644 index 0000000..ab55632 --- /dev/null +++ b/src/daemon/wallet/migration.ts @@ -0,0 +1,136 @@ +import { + chmodSync, + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + statSync, + unlinkSync, +} from "fs"; +import { dirname, join } from "path"; +import { legacyCocodDir, walletDir } from "./paths"; + +export type WalletMigrationResult = + | { status: "fresh" } + | { status: "already-current" } + | { status: "migrated"; from: string; to: string; cleanupWarnings: string[] }; + +export interface WalletMigrationOptions { + walletDir?: string; + legacyDir?: string; + /** Called after state validation and before legacy files are copied. */ + assertLegacyStopped?: () => void | Promise; + /** Claims the legacy cocod exclusion lock for the duration of the copy. */ + acquireLegacyLock?: () => (() => void) | Promise<() => void>; +} + +type WalletState = "absent" | "database-only" | "initialized"; + +function state(configPath: string, dbPath: string): WalletState { + const hasConfig = existsSync(configPath); + const hasDb = existsSync(dbPath); + if (hasConfig) return "initialized"; + if (hasDb) return "database-only"; + return "absent"; +} + +function filesEqual(left: string, right: string): boolean { + if (!existsSync(left) || !existsSync(right)) return false; + if (statSync(left).size !== statSync(right).size) return false; + return readFileSync(left).equals(readFileSync(right)); +} + +/** + * Copy a legacy cocod wallet into the canonical routstrd wallet directory. + * The staging directory is renamed atomically, so the canonical path is never + * exposed with only one of its two required files. + */ +export async function migrateLegacyWallet( + options: WalletMigrationOptions = {}, +): Promise { + const targetDir = options.walletDir || walletDir(); + const sourceDir = options.legacyDir || legacyCocodDir(); + const targetConfig = join(targetDir, "config.json"); + const targetDb = join(targetDir, "coco.db"); + const sourceConfig = join(sourceDir, "config.json"); + const sourceDb = join(sourceDir, "coco.db"); + const targetState = state(targetConfig, targetDb); + const sourceState = state(sourceConfig, sourceDb); + + // config.json is sufficient for a newly initialized wallet; coco.db is + // created on first open. A database without its mnemonic is never usable. + if (targetState === "initialized" && sourceState === "initialized") { + // A prior migration may have committed successfully but failed to remove + // its source files. Identical leftovers are safe; divergent wallets are not. + const configsMatch = filesEqual(targetConfig, sourceConfig); + const databasesMatch = + !existsSync(targetDb) && !existsSync(sourceDb) + ? true + : filesEqual(targetDb, sourceDb); + if (configsMatch && databasesMatch) return { status: "already-current" }; + throw new Error( + `Cannot migrate wallet: both ${targetDir} and ${sourceDir} contain different wallet data. ` + + "Refusing to choose a mnemonic or merge wallet databases automatically.", + ); + } + if (targetState === "initialized") return { status: "already-current" }; + if (targetState === "database-only" || sourceState === "database-only") { + throw new Error( + `Cannot migrate wallet: ${targetDir} is ${targetState} and ${sourceDir} is ${sourceState}. ` + + "A coco.db without config.json cannot be opened; restore the matching config first.", + ); + } + if (sourceState === "absent") return { status: "fresh" }; + + await options.assertLegacyStopped?.(); + const releaseLegacyLock = await options.acquireLegacyLock?.(); + const stagingDir = join( + dirname(targetDir), + `.${targetDir.split(/[\\/]/).at(-1) || "wallet"}.staging-${process.pid}-${Date.now()}`, + ); + + try { + // Recheck after claiming the exclusion lock to close the probe/claim race. + await options.assertLegacyStopped?.(); + mkdirSync(dirname(targetDir), { recursive: true, mode: 0o700 }); + mkdirSync(stagingDir, { mode: 0o700 }); + const stagedConfig = join(stagingDir, "config.json"); + const stagedDb = join(stagingDir, "coco.db"); + copyFileSync(sourceConfig, stagedConfig); + chmodSync(stagedConfig, 0o600); + const hasSourceDb = existsSync(sourceDb); + if (hasSourceDb) { + copyFileSync(sourceDb, stagedDb); + chmodSync(stagedDb, 0o600); + } + + if ( + statSync(stagedConfig).size !== statSync(sourceConfig).size || + (hasSourceDb && statSync(stagedDb).size !== statSync(sourceDb).size) + ) { + throw new Error("Cannot migrate wallet: staged files failed size validation."); + } + + renameSync(stagingDir, targetDir); + } catch (error) { + rmSync(stagingDir, { recursive: true, force: true }); + throw error; + } finally { + releaseLegacyLock?.(); + } + + const cleanupWarnings: string[] = []; + for (const path of [sourceConfig, sourceDb]) { + if (!existsSync(path)) continue; + try { + unlinkSync(path); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + cleanupWarnings.push(`Could not remove legacy wallet file ${path}: ${message}`); + } + } + + return { status: "migrated", from: sourceDir, to: targetDir, cleanupWarnings }; +} diff --git a/src/daemon/wallet/paths.ts b/src/daemon/wallet/paths.ts new file mode 100644 index 0000000..bdbd3e7 --- /dev/null +++ b/src/daemon/wallet/paths.ts @@ -0,0 +1,33 @@ +import { join } from "path"; + +function homeDir(): string { + return process.env.HOME || process.env.USERPROFILE || ""; +} + +/** Root directory for routstrd state. Evaluated lazily so env overrides work in tests. */ +export function routstrdConfigDir(): string { + return process.env.ROUTSTRD_DIR || join(homeDir(), ".routstrd"); +} + +/** Canonical directory for the in-process Cashu wallet. */ +export function walletDir(): string { + return process.env.ROUTSTRD_WALLET_DIR || join(routstrdConfigDir(), "wallet"); +} + +/** Lock owned by an in-process routstrd wallet instance. */ +export function walletPidPath(): string { + return process.env.ROUTSTRD_WALLET_PID || join(walletDir(), "wallet.pid"); +} + +/** Legacy external cocod directory. This is not the routstrd wallet directory. */ +export function legacyCocodDir(): string { + return process.env.COCOD_DIR || join(homeDir(), ".cocod"); +} + +export function legacyCocodSocketPath(): string { + return process.env.COCOD_SOCKET || join(legacyCocodDir(), "cocod.sock"); +} + +export function legacyCocodPidPath(): string { + return process.env.COCOD_PID || join(legacyCocodDir(), "cocod.pid"); +} diff --git a/src/start-daemon.ts b/src/start-daemon.ts index 9e45702..a9c58a5 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -9,7 +9,6 @@ import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; import { fileURLToPath } from "url"; -import { existsSync } from "fs"; const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`; const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;