From 8a8a0724a1adcabaff233b60ce24f067b7e356f8 Mon Sep 17 00:00:00 2001 From: wu-json Date: Sun, 22 Feb 2026 09:47:41 -0800 Subject: [PATCH] feat: oxfmt and oxlint --- .aqua/aqua-checksums.json | 264 +++++++++++++------------- .github/workflows/source-quality.yaml | 6 + .oxfmtrc.json | 5 + .oxlintrc.json | 8 + CLAUDE.md | 9 + README.md | 66 ++++--- biome.json | 34 ---- bun.lock | 85 ++++++++- examples/exec-probe/curse.toml | 2 +- examples/exec-probe/server.ts | 2 +- examples/flickering-test/README.md | 8 +- examples/large/api-server.ts | 19 +- examples/large/db-migrate.ts | 4 +- examples/large/mock-analytics.ts | 10 +- examples/large/mock-backup.ts | 41 +--- examples/large/mock-cache.ts | 20 +- examples/large/mock-database.ts | 9 +- examples/large/mock-email.ts | 13 +- examples/large/mock-file-processor.ts | 41 ++-- examples/large/mock-logs.ts | 16 +- examples/large/mock-queue.ts | 31 +-- examples/large/seed-data.ts | 4 +- examples/large/web-server.ts | 23 +-- justfile | 8 +- package.json | 21 +- scripts/build.ts | 2 +- src/env.ts | 6 +- src/hooks/useProcessManager.tsx | 82 +++----- src/index.ts | 9 +- src/lib/Dequeue.test.ts | 70 +++---- src/lib/Dequeue.ts | 10 +- src/lib/LogBuffer.ts | 19 +- src/lib/invariant.ts | 5 +- src/parser.ts | 8 +- src/ui/View.tsx | 25 +-- src/ui/components/LogTailPreview.tsx | 11 +- src/ui/components/ShortcutFooter.tsx | 11 +- src/ui/views/LogPage.tsx | 215 +++++---------------- src/ui/views/MainPage.tsx | 55 ++---- 39 files changed, 501 insertions(+), 776 deletions(-) create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json delete mode 100644 biome.json diff --git a/.aqua/aqua-checksums.json b/.aqua/aqua-checksums.json index d4827ce..181ca92 100644 --- a/.aqua/aqua-checksums.json +++ b/.aqua/aqua-checksums.json @@ -1,134 +1,134 @@ { - "checksums": [ - { - "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_darwin_all.tar.gz", - "checksum": "E5664B4DE41C6696358BD456F5283E86F0BD30E65AB65FAAD349293D804F088E", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_linux_amd64.tar.gz", - "checksum": "89A0BD658256C8561272EF2927EC0240C860779B042598C0456864583C88F702", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_linux_arm64.tar.gz", - "checksum": "59B973E51E65BB153B97A1D4CA1E46347136EEDC0DA60594F6BB5B87C8B1AD45", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_windows_amd64.zip", - "checksum": "74D8F24C312F2FFBD0866A2D38780B076A3D5EDF6690FBB17783358E3E88AFB4", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_windows_arm64.zip", - "checksum": "2622A9322D6786363E746F6A5CA4884F858C59289571D567B46C18D2C2E160ED", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-aarch64-apple-darwin.tar.gz", - "checksum": "BB0D35F6CA04709B798A19217693C16F4086170C580CC3B5C2531AD2794D2E32", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-aarch64-unknown-linux-musl.tar.gz", - "checksum": "4FCD8310081C32742EB984B1FDD0EEE2E5D4D0F1BE9629318012D42606EC9B3E", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-x86_64-apple-darwin.tar.gz", - "checksum": "687F66A6BD4D7D946EF5FF1E3EFEBB3D39DADAD151A8C6B1DE884CC93ADC06A5", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-x86_64-pc-windows-msvc.zip", - "checksum": "04BE7B6D7F8419288CE75532F1962CEE1756992E494E6C8063BB3AB8DB21B52C", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-x86_64-unknown-linux-musl.tar.gz", - "checksum": "A1BC93654F31669FD964EA3011A5E5E9676B9B6F8ADCD762606E5140632EA72D", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Darwin_all.tar.gz", - "checksum": "7AF0CE76660556F8A7CEB6C3D1B438E170EE3F551D5408153830697EB25F5962", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Linux_arm64.tar.gz", - "checksum": "A841DB4911C1DC32FB8EB1D4AFE91AE218C71BBC3B18379F9CDDB2EE899ACA31", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Linux_x86_64.tar.gz", - "checksum": "FD64C4BAEA57043A63CC209E8AD3FA54A7E3EDA88BF0D053D1AF9B577F328EAC", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Windows_arm64.zip", - "checksum": "D2FF86ED139485397DE82274628D6C1F725AD33614F81F043C798FDA46030D23", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Windows_x86_64.zip", - "checksum": "2D7E94D17806ED2EB704870716E66218AC8D062BFDB7DC105E575564AA54ADFE", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-darwin-aarch64.zip", - "checksum": "EB8C7E09CBEA572414A0A367848E1ACBF05294A946A594405A014B1FB3B3FC76", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-darwin-x64.zip", - "checksum": "A7484721A7EAD45887C812E760B124047E663173CF2A3BA7C5AA1992CB22CD3E", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-linux-aarch64.zip", - "checksum": "A97C687FB5E54DE4E2FB0869A7AC9A2D9C3AF75AC182E2B68138C1DD8F98131B", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-linux-x64.zip", - "checksum": "4C446AF1A01D7B40E1E11BAEBC352F9B2BFD12887E51B97DD3B59879CEE2743A", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-windows-x64.zip", - "checksum": "3A28C685B47A159C5707D150ACCB5B4903C30F1E7B4DD01BB311D4112BDEB452", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-darwin-aarch64.zip", - "checksum": "8803774E4C6C55C8A517464C508F02821E6DB57F94CA1BB5CC2A39F4D2326A51", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-darwin-x64.zip", - "checksum": "3390F9E6A82A9C718E187513B1C56405D0DE9ED3F6B092222B15B4D4BA6E304D", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-linux-aarch64.zip", - "checksum": "C46E841FED85347521915B1B3904D6D175D8E2FD915E18E01C111318219115A4", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-linux-x64.zip", - "checksum": "33C6996049E8D37E8B815959B14B05E5B6F496121352BF11BAE7D047193C28BF", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-windows-x64.zip", - "checksum": "2A9C0A9E6AD77D0C31FD8FB1F596B61AFA7F2F2F580BAAD597F17E7A0DBAD960", - "algorithm": "sha256" - }, - { - "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.418.0/registry.yaml", - "checksum": "A388A9445DBB89721022D840C42A73106650FB0536C4ED6F0C38537C7AC32701", - "algorithm": "sha256" - } - ] + "checksums": [ + { + "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_darwin_all.tar.gz", + "checksum": "E5664B4DE41C6696358BD456F5283E86F0BD30E65AB65FAAD349293D804F088E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_linux_amd64.tar.gz", + "checksum": "89A0BD658256C8561272EF2927EC0240C860779B042598C0456864583C88F702", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_linux_arm64.tar.gz", + "checksum": "59B973E51E65BB153B97A1D4CA1E46347136EEDC0DA60594F6BB5B87C8B1AD45", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_windows_amd64.zip", + "checksum": "74D8F24C312F2FFBD0866A2D38780B076A3D5EDF6690FBB17783358E3E88AFB4", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/caarlos0/svu/v3.2.4/svu_3.2.4_windows_arm64.zip", + "checksum": "2622A9322D6786363E746F6A5CA4884F858C59289571D567B46C18D2C2E160ED", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-aarch64-apple-darwin.tar.gz", + "checksum": "BB0D35F6CA04709B798A19217693C16F4086170C580CC3B5C2531AD2794D2E32", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-aarch64-unknown-linux-musl.tar.gz", + "checksum": "4FCD8310081C32742EB984B1FDD0EEE2E5D4D0F1BE9629318012D42606EC9B3E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-x86_64-apple-darwin.tar.gz", + "checksum": "687F66A6BD4D7D946EF5FF1E3EFEBB3D39DADAD151A8C6B1DE884CC93ADC06A5", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-x86_64-pc-windows-msvc.zip", + "checksum": "04BE7B6D7F8419288CE75532F1962CEE1756992E494E6C8063BB3AB8DB21B52C", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.43.0/just-1.43.0-x86_64-unknown-linux-musl.tar.gz", + "checksum": "A1BC93654F31669FD964EA3011A5E5E9676B9B6F8ADCD762606E5140632EA72D", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Darwin_all.tar.gz", + "checksum": "7AF0CE76660556F8A7CEB6C3D1B438E170EE3F551D5408153830697EB25F5962", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Linux_arm64.tar.gz", + "checksum": "A841DB4911C1DC32FB8EB1D4AFE91AE218C71BBC3B18379F9CDDB2EE899ACA31", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Linux_x86_64.tar.gz", + "checksum": "FD64C4BAEA57043A63CC209E8AD3FA54A7E3EDA88BF0D053D1AF9B577F328EAC", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Windows_arm64.zip", + "checksum": "D2FF86ED139485397DE82274628D6C1F725AD33614F81F043C798FDA46030D23", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.12.3/goreleaser_Windows_x86_64.zip", + "checksum": "2D7E94D17806ED2EB704870716E66218AC8D062BFDB7DC105E575564AA54ADFE", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-darwin-aarch64.zip", + "checksum": "EB8C7E09CBEA572414A0A367848E1ACBF05294A946A594405A014B1FB3B3FC76", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-darwin-x64.zip", + "checksum": "A7484721A7EAD45887C812E760B124047E663173CF2A3BA7C5AA1992CB22CD3E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-linux-aarch64.zip", + "checksum": "A97C687FB5E54DE4E2FB0869A7AC9A2D9C3AF75AC182E2B68138C1DD8F98131B", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-linux-x64.zip", + "checksum": "4C446AF1A01D7B40E1E11BAEBC352F9B2BFD12887E51B97DD3B59879CEE2743A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.2.22/bun-windows-x64.zip", + "checksum": "3A28C685B47A159C5707D150ACCB5B4903C30F1E7B4DD01BB311D4112BDEB452", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-darwin-aarch64.zip", + "checksum": "8803774E4C6C55C8A517464C508F02821E6DB57F94CA1BB5CC2A39F4D2326A51", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-darwin-x64.zip", + "checksum": "3390F9E6A82A9C718E187513B1C56405D0DE9ED3F6B092222B15B4D4BA6E304D", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-linux-aarch64.zip", + "checksum": "C46E841FED85347521915B1B3904D6D175D8E2FD915E18E01C111318219115A4", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-linux-x64.zip", + "checksum": "33C6996049E8D37E8B815959B14B05E5B6F496121352BF11BAE7D047193C28BF", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/oven-sh/bun/bun-v1.3.4/bun-windows-x64.zip", + "checksum": "2A9C0A9E6AD77D0C31FD8FB1F596B61AFA7F2F2F580BAAD597F17E7A0DBAD960", + "algorithm": "sha256" + }, + { + "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.418.0/registry.yaml", + "checksum": "A388A9445DBB89721022D840C42A73106650FB0536C4ED6F0C38537C7AC32701", + "algorithm": "sha256" + } + ] } diff --git a/.github/workflows/source-quality.yaml b/.github/workflows/source-quality.yaml index fd15f18..5e95f71 100644 --- a/.github/workflows/source-quality.yaml +++ b/.github/workflows/source-quality.yaml @@ -18,6 +18,12 @@ jobs: - name: Install dependencies run: bun install + - name: Lint + run: just lint + + - name: Format check + run: bun run oxfmt --check + - name: Typecheck run: just typecheck diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..3db393a --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,5 @@ +{ + "useTabs": true, + "singleQuote": false, + "experimentalSortImports": {} +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..3bfd250 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "categories": { + "correctness": "error" + }, + "rules": { + "unicorn/no-new-array": "off" + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 03221eb..19b8d15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,7 @@ src/ ### Process Management (useProcessManager.tsx:1) The heart of Curse's orchestration system. Handles: + - Process lifecycle (spawn, kill, restart) - Dependency resolution and execution ordering - Readiness probes (HTTP and exec) @@ -70,6 +71,7 @@ The heart of Curse's orchestration system. Handles: ### Log System **LogBuffer** (lib/LogBuffer.ts:1): Fixed-size circular buffer using a dequeue for efficient log storage and retrieval. Supports: + - O(1) append operations - Efficient line-by-line access - Memory-bounded storage (default 5,000 lines) @@ -79,6 +81,7 @@ The heart of Curse's orchestration system. Handles: ### Configuration (parser.ts:1) Uses arktype for runtime validation of TOML configs. Validates: + - Process definitions and dependencies - Readiness probe configurations - Lifecycle hooks @@ -109,6 +112,7 @@ shutdown = { name = "cleanup", command = "docker-compose down" } ## Process Dependencies Three dependency conditions: + - **started**: Dependent runs when dependency is running (any state after pending) - **succeeded**: Dependent waits for dependency to exit successfully (exit code 0) - **ready**: Dependent waits for readiness probe to pass (or running state if no probe) @@ -116,11 +120,13 @@ Three dependency conditions: ## Readiness Probes **HTTP Probe**: Polls an HTTP endpoint until it returns 200 OK + ```toml readiness_probe = { type = "http", host = "127.0.0.1", path = "/health", port = 8080 } ``` **Exec Probe**: Runs a shell command until it exits with code 0 + ```toml readiness_probe = { type = "exec", command = "pg_isready -h localhost" } ``` @@ -138,13 +144,16 @@ readiness_probe = { type = "exec", command = "pg_isready -h localhost" } ## Development ### Building + ```bash bun install bun run src/index.ts -p examples/basic/curse.toml ``` ### Testing Examples + The `examples/` directory contains various test scenarios: + - `basic/`: Simple process configuration - `deps/`: Dependency chain testing - `hooks/`: Lifecycle hook examples diff --git a/README.md b/README.md index b6ef8e3..06f5438 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ cd app/client yarn start -If the above made you cringe, then you're not alone because many others have too. Existing solutions to this problem have come in various forms. +If the above made you cringe, then you're not alone because many others have too. Existing solutions to this problem have come in various forms. - [s(hell) Scripts](https://pythonspeed.com/articles/shell-scripts/): How do you view the ongoing output of each process?. You could hook into TMux or Wezterm panes but that isn't ideal for everyone. @@ -105,7 +105,7 @@ If the above made you cringe, then you're not alone because many others have too - [process-compose](https://github.com/F1bonacc1/process-compose): Has a lot of features I don't use and feels sluggish. -Out of all of the options above, `process-compose` got the closest to the experience I wanted but was still far from it. It felt quite slow, had limited tooling around logging, and resulted in composed configuration files that were unpleasant to maintain. +Out of all of the options above, `process-compose` got the closest to the experience I wanted but was still far from it. It felt quite slow, had limited tooling around logging, and resulted in composed configuration files that were unpleasant to maintain. ## Design Principles @@ -143,15 +143,16 @@ version = 0 # Required: Must be 0 (only supported version) Each process is defined as a `[[process]]` table with the following fields: -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `name` | string | Yes | Unique identifier for the process. Used for dependencies and display. | -| `command` | string | Yes | Shell command to execute. Runs in a shell context. | -| `env` | object | No | Environment variables for the process. Keys are strings, values can be strings or numbers. | -| `deps` | array | No | Array of dependency objects. See [Dependencies](#dependencies) below. | -| `readiness_probe` | object | No | Health check configuration. See [Readiness Probes](#readiness-probes) below. | +| Field | Type | Required | Description | +| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------------ | +| `name` | string | Yes | Unique identifier for the process. Used for dependencies and display. | +| `command` | string | Yes | Shell command to execute. Runs in a shell context. | +| `env` | object | No | Environment variables for the process. Keys are strings, values can be strings or numbers. | +| `deps` | array | No | Array of dependency objects. See [Dependencies](#dependencies) below. | +| `readiness_probe` | object | No | Health check configuration. See [Readiness Probes](#readiness-probes) below. | **Example:** + ```toml [[process]] name = "api-server" @@ -165,10 +166,10 @@ readiness_probe = { type = "http", host = "127.0.0.1", path = "/health", port = Dependencies control the execution order of processes. Each dependency object has two fields: -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `name` | string | Yes | Name of the process this depends on. Must reference an existing process. | -| `condition` | string | Yes | When to consider the dependency satisfied. One of: `"started"`, `"succeeded"`, or `"ready"`. | +| Field | Type | Required | Description | +| ----------- | ------ | -------- | -------------------------------------------------------------------------------------------- | +| `name` | string | Yes | Name of the process this depends on. Must reference an existing process. | +| `condition` | string | Yes | When to consider the dependency satisfied. One of: `"started"`, `"succeeded"`, or `"ready"`. | **Dependency Conditions:** @@ -177,6 +178,7 @@ Dependencies control the execution order of processes. Each dependency object ha - **`"ready"`**: Dependent process waits for the readiness probe to pass (or just running if no probe defined). **Example:** + ```toml [[process]] name = "migrations" @@ -198,14 +200,15 @@ Readiness probes are health checks that determine when a process is ready to acc Polls an HTTP endpoint until it returns a 200 OK response. -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `type` | string | Yes | Must be `"http"`. | -| `host` | string | Yes | Hostname or IP address to connect to. | -| `path` | string | Yes | HTTP path to request (e.g., `"/health"`). | -| `port` | number | Yes | Port number to connect to. | +| Field | Type | Required | Description | +| ------ | ------ | -------- | ----------------------------------------- | +| `type` | string | Yes | Must be `"http"`. | +| `host` | string | Yes | Hostname or IP address to connect to. | +| `path` | string | Yes | HTTP path to request (e.g., `"/health"`). | +| `port` | number | Yes | Port number to connect to. | **Example:** + ```toml [[process]] name = "web-server" @@ -217,12 +220,13 @@ readiness_probe = { type = "http", host = "127.0.0.1", path = "/", port = 8000 } Runs a shell command repeatedly until it exits with code 0. -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `type` | string | Yes | Must be `"exec"`. | -| `command` | string | Yes | Shell command to execute. | +| Field | Type | Required | Description | +| --------- | ------ | -------- | ------------------------- | +| `type` | string | Yes | Must be `"exec"`. | +| `command` | string | Yes | Shell command to execute. | **Example:** + ```toml [[process]] name = "postgres" @@ -244,19 +248,19 @@ shutdown = { name = "cleanup", command = "docker-compose down" } Runs before any processes start. If it fails (non-zero exit), Curse will exit. -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `name` | string | Yes | Display name for the hook. Must be unique across all processes and hooks. | -| `command` | string | Yes | Shell command to execute. | +| Field | Type | Required | Description | +| --------- | ------ | -------- | ------------------------------------------------------------------------- | +| `name` | string | Yes | Display name for the hook. Must be unique across all processes and hooks. | +| `command` | string | Yes | Shell command to execute. | ##### Shutdown Hook Runs after all processes are killed (when user presses `q` or Curse exits). Runs regardless of whether processes succeeded or failed. -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `name` | string | Yes | Display name for the hook. Must be unique across all processes and hooks. | -| `command` | string | Yes | Shell command to execute. | +| Field | Type | Required | Description | +| --------- | ------ | -------- | ------------------------------------------------------------------------- | +| `name` | string | Yes | Display name for the hook. Must be unique across all processes and hooks. | +| `command` | string | Yes | Shell command to execute. | ### Validation Rules diff --git a/biome.json b/biome.json deleted file mode 100644 index de727bc..0000000 --- a/biome.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - } -} diff --git a/bun.lock b/bun.lock index 5534a27..d70fae6 100644 --- a/bun.lock +++ b/bun.lock @@ -14,8 +14,9 @@ "toml": "^3.0.0", }, "devDependencies": { - "@biomejs/biome": "^2.2.3", "@types/bun": "latest", + "oxfmt": "^0.34.0", + "oxlint": "^1.49.0", "react-devtools-core": "^6.1.5", }, "peerDependencies": { @@ -30,23 +31,81 @@ "@ark/util": ["@ark/util@0.49.0", "", {}, "sha512-/BtnX7oCjNkxi2vi6y1399b+9xd1jnCrDYhZ61f0a+3X8x8DxlK52VgEEzyuC2UQMPACIfYrmHkhD3lGt2GaMA=="], - "@biomejs/biome": ["@biomejs/biome@2.2.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.4", "@biomejs/cli-darwin-x64": "2.2.4", "@biomejs/cli-linux-arm64": "2.2.4", "@biomejs/cli-linux-arm64-musl": "2.2.4", "@biomejs/cli-linux-x64": "2.2.4", "@biomejs/cli-linux-x64-musl": "2.2.4", "@biomejs/cli-win32-arm64": "2.2.4", "@biomejs/cli-win32-x64": "2.2.4" }, "bin": { "biome": "bin/biome" } }, "sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.34.0", "", { "os": "android", "cpu": "arm" }, "sha512-sqkqjh/Z38l+duOb1HtVqJTAj1grt2ttkobCopC/72+a4Xxz4xUgZPFyQ4HxrYMvyqO/YA0tvM1QbfOu70Gk1Q=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.34.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1KRCtasHcVcGOMwfOP9d5Bus2NFsN8yAYM5cBwi8LBg5UtXC3C49WHKrlEa8iF1BjOS6CR2qIqiFbGoA0DJQNQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.34.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b+Rmw9Bva6e/7PBES2wLO8sEU7Mi0+/Kv+pXSe/Y8i4fWNftZZlGwp8P01eECaUqpXATfSgNxdEKy7+ssVNz7g=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.34.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGjpevWzf1T9COEokZEWt80kPOtthW1zhRbo7x4Qoz646eTTfi6XsHG2uHeDWJmTbgBoJZPMgj2TAEV/ppEZaA=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.34.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VMSaC02cG75qL59M9M/szEaqq/RsLfgpzQ4nqUu8BUnX1zkiZIW2gTpUv3ZJ6qpWnHxIlAXiRZjQwmcwpvtbcg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Klm367PFJhH6vYK3vdIOxFepSJZHPaBfIuqwxdkOcfSQ4qqc/M8sgK0UTFnJWWTA/IkhMIh1kW6uEqiZ/xtQqg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-nqn0QueVXRfbN9m58/E9Zij0Ap8lzayx591eWBYn0sZrGzY1IRv9RYS7J/1YUXbb0Ugedo0a8qIWzUHU9bWQuA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DDn+dcqW+sMTCEjvLoQvC/VWJjG7h8wcdN/J+g7ZTdf/3/Dx730pQElxPPGsCXPhprb11OsPyMp5FwXjMY3qvA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.34.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.34.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.34.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.34.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6id8kK0t5hKfbV6LHDzRO21wRTA6ctTlKGTZIsG/mcoir0rssvaYsedUymF4HDj7tbCUlnxCX/qOajKlEuqbIw=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.34.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHaz+w673mlYqn9v/+fuiKZpjkmagleXQ+NygShDv8tdHpRYX2oYhTJwwt9j1ZfVhRgza1EIUW3JmzCXmtPdhQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.34.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CXKQM/VaF+yuvGru8ktleHLJoBdjBtTFmAsLGePiESiTN0NjCI/PiaiOCfHMJ1HdP1LykvARUwMvgaN3tDhcrg=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-2WPoh/2oK9r/i2R4o4J18AOrm3HVlWiHZ8TnuCaS4dX8m5ZzRmHW0I3eLxEurQLHWVruhQN7fHgZnah+ag5iQg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YqJAGvNB11EzoKm1euVhZntb79alhMvWW/j12bYqdvVxn6xzEQWrEDCJg9BPo3A3tBCSUBKH7bVkAiCBqK/L1w=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.49.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WFocCRlvVkMhChCJ2qpJfp1Gj/IjvyjuifH9Pex8m8yHonxxQa3d8DZYreuDQU3T4jvSY8rqhoRqnpc61Nlbxw=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.49.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BN0KniwvehbUfYztOMwEDkYoojGm/narf5oJf+/ap+6PnzMeWLezMaVARNIS0j3OdMkjHTEP8s3+GdPJ7WDywQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.49.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SnkAc/DPIY6joMCiP/+53Q+N2UOGMU6ULvbztpmvPJNF/jYPGhNbKtN982uj2Gs6fpbxYkmyj08QnpkD4fbHJA=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-6Z3EzRvpQVIpO7uFhdiGhdE8Mh3S2VWKLL9xuxVqD6fzPhyI3ugthpYXlCChXzO8FzcYIZ3t1+Kau+h2NY1hqA=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wdjXaQYAL/L25732mLlngfst4Jdmi/HLPVHb3yfCoP5mE3lO/pFFrmOJpqWodgv29suWY74Ij+RmJ/YIG5VuzQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oSHpm8zmSvAG1BWUumbDRSg7moJbnwoEXKAkwDf/xTQJOzvbUknq95NVQdw/AduZr5dePftalB8rzJNGBogUMg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.49.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.49.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.49.0", "", { "os": "none", "cpu": "arm64" }, "sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.49.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6rrKe/wL9tn0qnOy76i1/0f4Dc3dtQnibGlU4HqR/brVHlVjzLSoaH0gAFnLnznh9yQ6gcFTBFOPrcN/eKPDGA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.49.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-CXHLWAtLs2xG/aVy1OZiYJzrULlq0QkYpI6cd7VKMrab+qur4fXVE/B1Bp1m0h1qKTj5/FTGg6oU4qaXMjS/ug=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.49.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VteIelt78kwzSglOozaQcs6BCS4Lk0j+QA+hGV0W8UeyaqQ3XpbZRhDU55NW1PPvCy1tg4VXsTlEaPovqto7nQ=="], "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], @@ -108,6 +167,10 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oxfmt": ["oxfmt@0.34.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.34.0", "@oxfmt/binding-android-arm64": "0.34.0", "@oxfmt/binding-darwin-arm64": "0.34.0", "@oxfmt/binding-darwin-x64": "0.34.0", "@oxfmt/binding-freebsd-x64": "0.34.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.34.0", "@oxfmt/binding-linux-arm-musleabihf": "0.34.0", "@oxfmt/binding-linux-arm64-gnu": "0.34.0", "@oxfmt/binding-linux-arm64-musl": "0.34.0", "@oxfmt/binding-linux-ppc64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-musl": "0.34.0", "@oxfmt/binding-linux-s390x-gnu": "0.34.0", "@oxfmt/binding-linux-x64-gnu": "0.34.0", "@oxfmt/binding-linux-x64-musl": "0.34.0", "@oxfmt/binding-openharmony-arm64": "0.34.0", "@oxfmt/binding-win32-arm64-msvc": "0.34.0", "@oxfmt/binding-win32-ia32-msvc": "0.34.0", "@oxfmt/binding-win32-x64-msvc": "0.34.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-t+zTE4XGpzPTK+Zk9gSwcJcFi4pqjl6PwO/ZxPBJiJQ2XCKMucwjPlHxvPHyVKJtkMSyrDGfQ7Ntg/hUr4OgHQ=="], + + "oxlint": ["oxlint@1.49.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.49.0", "@oxlint/binding-android-arm64": "1.49.0", "@oxlint/binding-darwin-arm64": "1.49.0", "@oxlint/binding-darwin-x64": "1.49.0", "@oxlint/binding-freebsd-x64": "1.49.0", "@oxlint/binding-linux-arm-gnueabihf": "1.49.0", "@oxlint/binding-linux-arm-musleabihf": "1.49.0", "@oxlint/binding-linux-arm64-gnu": "1.49.0", "@oxlint/binding-linux-arm64-musl": "1.49.0", "@oxlint/binding-linux-ppc64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-musl": "1.49.0", "@oxlint/binding-linux-s390x-gnu": "1.49.0", "@oxlint/binding-linux-x64-gnu": "1.49.0", "@oxlint/binding-linux-x64-musl": "1.49.0", "@oxlint/binding-openharmony-arm64": "1.49.0", "@oxlint/binding-win32-arm64-msvc": "1.49.0", "@oxlint/binding-win32-ia32-msvc": "1.49.0", "@oxlint/binding-win32-x64-msvc": "1.49.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-YZffp0gM+63CJoRhHjtjRnwKtAgUnXM6j63YQ++aigji2NVvLGsUlrXo9gJUXZOdcbfShLYtA6RuTu8GZ4lzOQ=="], + "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="], @@ -132,6 +195,8 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], diff --git a/examples/exec-probe/curse.toml b/examples/exec-probe/curse.toml index 97eed34..0e8d1f2 100644 --- a/examples/exec-probe/curse.toml +++ b/examples/exec-probe/curse.toml @@ -24,4 +24,4 @@ readiness_probe = { type = "exec", command = "test -f /tmp/worker-ready" } [[process]] name = "api" command = "bun run examples/exec-probe/api.ts" -deps = [{ name = "database", condition = "ready" }, { name = "worker", condition = "ready" }] \ No newline at end of file +deps = [{ name = "database", condition = "ready" }, { name = "worker", condition = "ready" }] diff --git a/examples/exec-probe/server.ts b/examples/exec-probe/server.ts index 0b85e93..b957eee 100644 --- a/examples/exec-probe/server.ts +++ b/examples/exec-probe/server.ts @@ -4,7 +4,7 @@ import { serve } from "bun"; const server = serve({ port: 3000, - fetch(req) { + fetch(_req) { return new Response("Web server is running!"); }, }); diff --git a/examples/flickering-test/README.md b/examples/flickering-test/README.md index 7cd1cd1..bf71221 100644 --- a/examples/flickering-test/README.md +++ b/examples/flickering-test/README.md @@ -5,6 +5,7 @@ This directory contains a test setup to reproduce and verify the fix for the fli ## Problem Description The flickering issue occurred when: + 1. Multiple processes were generating logs at different rates 2. The LogPreview component was re-rendering due to forced updates every second 3. Each re-render would fetch fresh log data, causing visual flickering when other processes were actively logging @@ -22,11 +23,13 @@ This example includes several processes that generate logs at different rates: ## How to Test 1. Navigate to this directory: + ```bash cd examples/flickering-test ``` 2. Run the test with curse: + ```bash ../../dist/curse ``` @@ -45,11 +48,13 @@ This example includes several processes that generate logs at different rates: ## What to Look For ### Before the Fix (Flickering Behavior) + - Log preview would flicker/flash when viewing static or slow processes - Text would appear to "jump" or "refresh" visually even when the selected process wasn't generating new logs - Most noticeable when the fast-logger or burst-logger were active ### After the Fix (Stable Behavior) + - Log preview remains stable when viewing processes that aren't actively logging - Only updates when the selected process actually generates new log entries - No visual flickering or unnecessary re-renders @@ -57,8 +62,9 @@ This example includes several processes that generate logs at different rates: ## Technical Details The fix uses `useMemo` in the `LogTailPreview` component to cache log data based on: + - The selected process - The total number of lines in that process's log buffer - The component height -This ensures logs are only re-fetched when they actually change for the selected process, preventing flickering caused by updates from other processes. \ No newline at end of file +This ensures logs are only re-fetched when they actually change for the selected process, preventing flickering caused by updates from other processes. diff --git a/examples/large/api-server.ts b/examples/large/api-server.ts index 2dee39b..f83e7db 100644 --- a/examples/large/api-server.ts +++ b/examples/large/api-server.ts @@ -1,15 +1,11 @@ process.on("SIGTERM", () => { - console.log( - `[${process.env.SERVICE_NAME}] Received SIGTERM, shutting down gracefully...`, - ); + console.log(`[${process.env.SERVICE_NAME}] Received SIGTERM, shutting down gracefully...`); server.stop(); process.exit(0); }); process.on("SIGINT", () => { - console.log( - `[${process.env.SERVICE_NAME}] Received SIGINT, shutting down gracefully...`, - ); + console.log(`[${process.env.SERVICE_NAME}] Received SIGINT, shutting down gracefully...`); server.stop(); process.exit(0); }); @@ -39,8 +35,7 @@ const server = Bun.serve({ if (endpoints.includes(url.pathname)) { const method = req.method; const responseTime = Math.random() * 200 + 50; - const statusCode = - Math.random() > 0.95 ? 500 : Math.random() > 0.9 ? 404 : 200; + const statusCode = Math.random() > 0.95 ? 500 : Math.random() > 0.9 ? 404 : 200; setTimeout(() => { console.log( @@ -65,12 +60,8 @@ const server = Bun.serve({ }, }); -console.log( - `[${process.env.SERVICE_NAME}] API server running on http://localhost:${server.port}`, -); -console.log( - `[${process.env.SERVICE_NAME}] Available endpoints: ${endpoints.join(", ")}`, -); +console.log(`[${process.env.SERVICE_NAME}] API server running on http://localhost:${server.port}`); +console.log(`[${process.env.SERVICE_NAME}] Available endpoints: ${endpoints.join(", ")}`); // Simulate periodic activity setInterval( diff --git a/examples/large/db-migrate.ts b/examples/large/db-migrate.ts index 06c636e..daa7d4a 100644 --- a/examples/large/db-migrate.ts +++ b/examples/large/db-migrate.ts @@ -29,9 +29,7 @@ for (const migration of migrations) { const duration = Math.random() * 3000 + 1000; await new Promise((resolve) => setTimeout(resolve, duration)); - console.log( - `[DB-MIGRATE] ✓ ${migration} completed (${(duration / 1000).toFixed(1)}s)`, - ); + console.log(`[DB-MIGRATE] ✓ ${migration} completed (${(duration / 1000).toFixed(1)}s)`); } console.log("[DB-MIGRATE] All migrations completed successfully"); diff --git a/examples/large/mock-analytics.ts b/examples/large/mock-analytics.ts index dde2502..6a29c61 100644 --- a/examples/large/mock-analytics.ts +++ b/examples/large/mock-analytics.ts @@ -32,9 +32,7 @@ function simulateAnalyticsActivity() { const userId = `user_${Math.floor(Math.random() * 1000)}`; const timestamp = new Date().toISOString(); - console.log( - `[ANALYTICS] Event: ${eventType} | User: ${userId} | ${timestamp}`, - ); + console.log(`[ANALYTICS] Event: ${eventType} | User: ${userId} | ${timestamp}`); eventsProcessed++; // Simulate user activity changes @@ -61,11 +59,7 @@ function simulateAnalyticsActivity() { if (Math.random() > 0.9) { const metric = metrics[Math.floor(Math.random() * metrics.length)]; const value = (Math.random() * 100).toFixed(2); - const unit = metric.includes("rate") - ? "%" - : metric.includes("time") - ? "ms" - : ""; + const unit = metric.includes("rate") ? "%" : metric.includes("time") ? "ms" : ""; console.log(`[ANALYTICS] Metric update: ${metric} = ${value}${unit}`); } diff --git a/examples/large/mock-backup.ts b/examples/large/mock-backup.ts index 7eeadff..e653eaf 100644 --- a/examples/large/mock-backup.ts +++ b/examples/large/mock-backup.ts @@ -15,15 +15,11 @@ const storageProviders = ["AWS S3", "Google Cloud", "Azure Blob", "Local NAS"]; function simulateBackupActivity() { if (Math.random() > 0.85) { - const target = - backupTargets[Math.floor(Math.random() * backupTargets.length)]; - const provider = - storageProviders[Math.floor(Math.random() * storageProviders.length)]; + const target = backupTargets[Math.floor(Math.random() * backupTargets.length)]; + const provider = storageProviders[Math.floor(Math.random() * storageProviders.length)]; const backupId = `backup_${Date.now()}_${Math.random().toString(36).substring(2, 6)}`; - console.log( - `[BACKUP] Starting backup: ${target.name} (${target.size}) to ${provider}`, - ); + console.log(`[BACKUP] Starting backup: ${target.name} (${target.size}) to ${provider}`); console.log(`[BACKUP] Backup ID: ${backupId}`); // Simulate backup progress @@ -33,10 +29,7 @@ function simulateBackupActivity() { const progressInterval = setInterval( () => { currentStep++; - const percentage = Math.min( - 100, - (currentStep / totalSteps) * 100, - ).toFixed(1); + const percentage = Math.min(100, (currentStep / totalSteps) * 100).toFixed(1); if (currentStep <= totalSteps) { console.log(`[BACKUP] ${backupId}: ${percentage}% complete...`); @@ -48,19 +41,11 @@ function simulateBackupActivity() { if (Math.random() > 0.1) { backupsCompleted++; const duration = (Math.random() * 300 + 60).toFixed(1); - console.log( - `[BACKUP] ${backupId}: Backup completed successfully in ${duration}s`, - ); - console.log( - `[BACKUP] ${backupId}: Verification passed, stored at ${provider}`, - ); + console.log(`[BACKUP] ${backupId}: Backup completed successfully in ${duration}s`); + console.log(`[BACKUP] ${backupId}: Verification passed, stored at ${provider}`); } else { - console.log( - `[BACKUP] ${backupId}: Backup failed - Network timeout`, - ); - console.log( - `[BACKUP] ${backupId}: Scheduled for retry in 30 minutes`, - ); + console.log(`[BACKUP] ${backupId}: Backup failed - Network timeout`); + console.log(`[BACKUP] ${backupId}: Scheduled for retry in 30 minutes`); } } }, @@ -84,14 +69,8 @@ function simulateBackupActivity() { } console.log("[BACKUP] Backup service initialized"); -console.log( - "[BACKUP] Checking backup targets:", - backupTargets.map((t) => t.name).join(", "), -); -console.log( - "[BACKUP] Storage providers configured:", - storageProviders.join(", "), -); +console.log("[BACKUP] Checking backup targets:", backupTargets.map((t) => t.name).join(", ")); +console.log("[BACKUP] Storage providers configured:", storageProviders.join(", ")); console.log("[BACKUP] Backup service ready"); setInterval(simulateBackupActivity, 5000 + Math.random() * 10000); diff --git a/examples/large/mock-cache.ts b/examples/large/mock-cache.ts index 6370311..02e7b27 100644 --- a/examples/large/mock-cache.ts +++ b/examples/large/mock-cache.ts @@ -15,23 +15,15 @@ const cacheOperations = [ "analytics:daily:stats", ]; -const evictionReasons = [ - "TTL expired", - "Memory pressure", - "Manual delete", - "Key overwritten", -]; +const evictionReasons = ["TTL expired", "Memory pressure", "Manual delete", "Key overwritten"]; function simulateCacheActivity() { if (Math.random() > 0.6) { - const key = - cacheOperations[Math.floor(Math.random() * cacheOperations.length)]; + const key = cacheOperations[Math.floor(Math.random() * cacheOperations.length)]; if (Math.random() > 0.3) { cacheHits++; - console.log( - `[CACHE] HIT ${key} (${(Math.random() * 2 + 0.1).toFixed(2)}ms)`, - ); + console.log(`[CACHE] HIT ${key} (${(Math.random() * 2 + 0.1).toFixed(2)}ms)`); } else { cacheMisses++; console.log(`[CACHE] MISS ${key}`); @@ -47,10 +39,8 @@ function simulateCacheActivity() { } if (Math.random() > 0.95) { - const key = - cacheOperations[Math.floor(Math.random() * cacheOperations.length)]; - const reason = - evictionReasons[Math.floor(Math.random() * evictionReasons.length)]; + const key = cacheOperations[Math.floor(Math.random() * cacheOperations.length)]; + const reason = evictionReasons[Math.floor(Math.random() * evictionReasons.length)]; console.log(`[CACHE] EVICTED ${key} (${reason})`); } } diff --git a/examples/large/mock-database.ts b/examples/large/mock-database.ts index c3c6387..d5ef312 100644 --- a/examples/large/mock-database.ts +++ b/examples/large/mock-database.ts @@ -23,19 +23,14 @@ const operations = [ function simulateActivity() { if (Math.random() > 0.7) { - connectionCount = Math.max( - 0, - connectionCount + (Math.random() > 0.5 ? 1 : -1), - ); + connectionCount = Math.max(0, connectionCount + (Math.random() > 0.5 ? 1 : -1)); console.log(`[DATABASE] Active connections: ${connectionCount}/20`); } if (Math.random() > 0.8) { const query = queries[Math.floor(Math.random() * queries.length)]; const duration = (Math.random() * 50 + 5).toFixed(2); - console.log( - `[DATABASE] Query executed in ${duration}ms: ${query.substring(0, 50)}...`, - ); + console.log(`[DATABASE] Query executed in ${duration}ms: ${query.substring(0, 50)}...`); } if (Math.random() > 0.9) { diff --git a/examples/large/mock-email.ts b/examples/large/mock-email.ts index d89befb..8568f10 100644 --- a/examples/large/mock-email.ts +++ b/examples/large/mock-email.ts @@ -26,26 +26,19 @@ function simulateEmailActivity() { const recipient = `user${Math.floor(Math.random() * 1000)}@example.com`; const provider = providers[Math.floor(Math.random() * providers.length)]; - console.log( - `[EMAIL] Sending ${emailType} email to ${recipient} (ID: ${emailId})`, - ); + console.log(`[EMAIL] Sending ${emailType} email to ${recipient} (ID: ${emailId})`); console.log(`[EMAIL] Using provider: ${provider}`); emailsQueued--; setTimeout( () => { - const status = - Math.random() > 0.9 - ? statuses[Math.floor(Math.random() * 4)] - : "delivered"; + const status = Math.random() > 0.9 ? statuses[Math.floor(Math.random() * 4)] : "delivered"; if (status === "delivered") { emailsSent++; console.log(`[EMAIL] Email ${emailId} delivered successfully`); } else { - console.log( - `[EMAIL] Email ${emailId} ${status}: ${getStatusReason(status)}`, - ); + console.log(`[EMAIL] Email ${emailId} ${status}: ${getStatusReason(status)}`); } }, Math.random() * 3000 + 1000, diff --git a/examples/large/mock-file-processor.ts b/examples/large/mock-file-processor.ts index f9c3c36..a9fd437 100644 --- a/examples/large/mock-file-processor.ts +++ b/examples/large/mock-file-processor.ts @@ -15,30 +15,23 @@ const fileTypes = [ { ext: "docx", action: "text extraction" }, ]; -const statuses = ["processing", "completed", "failed", "queued"]; +const _statuses = ["processing", "completed", "failed", "queued"]; function simulateFileProcessing() { if (Math.random() > 0.6) { const fileType = fileTypes[Math.floor(Math.random() * fileTypes.length)]; const fileName = `file_${Math.random().toString(36).substring(2, 8)}.${fileType.ext}`; - const directory = - watchedDirectories[Math.floor(Math.random() * watchedDirectories.length)]; + const directory = watchedDirectories[Math.floor(Math.random() * watchedDirectories.length)]; const fileSize = (Math.random() * 50 + 1).toFixed(2); - console.log( - `[FILE-PROCESSOR] New file detected: ${directory}/${fileName} (${fileSize}MB)`, - ); - console.log( - `[FILE-PROCESSOR] Starting ${fileType.action} for ${fileName}...`, - ); + console.log(`[FILE-PROCESSOR] New file detected: ${directory}/${fileName} (${fileSize}MB)`); + console.log(`[FILE-PROCESSOR] Starting ${fileType.action} for ${fileName}...`); const processingTime = Math.random() * 5000 + 2000; setTimeout(() => { if (Math.random() > 0.15) { processedFiles++; - console.log( - `[FILE-PROCESSOR] ${fileType.action} completed for ${fileName}`, - ); + console.log(`[FILE-PROCESSOR] ${fileType.action} completed for ${fileName}`); console.log(`[FILE-PROCESSOR] Output saved to /processed/${fileName}`); } else { console.log( @@ -49,12 +42,9 @@ function simulateFileProcessing() { } if (Math.random() > 0.85) { - const directory = - watchedDirectories[Math.floor(Math.random() * watchedDirectories.length)]; + const directory = watchedDirectories[Math.floor(Math.random() * watchedDirectories.length)]; const fileCount = Math.floor(Math.random() * 5) + 1; - console.log( - `[FILE-PROCESSOR] Scanning ${directory}... found ${fileCount} new files`, - ); + console.log(`[FILE-PROCESSOR] Scanning ${directory}... found ${fileCount} new files`); } if (Math.random() > 0.9) { @@ -64,29 +54,20 @@ function simulateFileProcessing() { } } -console.log( - "[FILE-PROCESSOR] Watching directories:", - watchedDirectories.join(", "), -); -console.log( - "[FILE-PROCESSOR] Supported formats: PDF, JPG, MP4, CSV, XLSX, ZIP, DOCX", -); +console.log("[FILE-PROCESSOR] Watching directories:", watchedDirectories.join(", ")); +console.log("[FILE-PROCESSOR] Supported formats: PDF, JPG, MP4, CSV, XLSX, ZIP, DOCX"); console.log("[FILE-PROCESSOR] File processor ready"); setInterval(simulateFileProcessing, 2000 + Math.random() * 3000); process.on("SIGTERM", () => { - console.log( - "[FILE-PROCESSOR] Received SIGTERM, finishing current processing...", - ); + console.log("[FILE-PROCESSOR] Received SIGTERM, finishing current processing..."); console.log("[FILE-PROCESSOR] File processor stopped"); process.exit(0); }); process.on("SIGINT", () => { - console.log( - "[FILE-PROCESSOR] Received SIGINT, finishing current processing...", - ); + console.log("[FILE-PROCESSOR] Received SIGINT, finishing current processing..."); console.log("[FILE-PROCESSOR] File processor stopped"); process.exit(0); }); diff --git a/examples/large/mock-logs.ts b/examples/large/mock-logs.ts index 9023246..4e5afbb 100644 --- a/examples/large/mock-logs.ts +++ b/examples/large/mock-logs.ts @@ -53,18 +53,14 @@ function simulateLogAggregation() { const timestamp = new Date().toISOString(); const requestId = Math.random().toString(36).substring(2, 8); - console.log( - `[LOGS] ${timestamp} [${level}] ${source}: ${message} (req: ${requestId})`, - ); + console.log(`[LOGS] ${timestamp} [${level}] ${source}: ${message} (req: ${requestId})`); logsProcessed++; } if (Math.random() > 0.8) { const batchSize = Math.floor(Math.random() * 20) + 5; const source = logSources[Math.floor(Math.random() * logSources.length)]; - console.log( - `[LOGS] Processing batch from ${source}: ${batchSize} log entries`, - ); + console.log(`[LOGS] Processing batch from ${source}: ${batchSize} log entries`); logsProcessed += batchSize; setTimeout( @@ -78,9 +74,7 @@ function simulateLogAggregation() { if (Math.random() > 0.85) { const errors = Math.floor(Math.random() * 3); const warnings = Math.floor(Math.random() * 8); - console.log( - `[LOGS] Alert summary: ${errors} errors, ${warnings} warnings in last 5 minutes`, - ); + console.log(`[LOGS] Alert summary: ${errors} errors, ${warnings} warnings in last 5 minutes`); } if (Math.random() > 0.9) { @@ -94,9 +88,7 @@ function simulateLogAggregation() { console.log(`[LOGS] Running log rotation and cleanup...`); setTimeout(() => { const deletedLogs = Math.floor(Math.random() * 1000) + 500; - console.log( - `[LOGS] Cleanup completed: ${deletedLogs} old log entries archived`, - ); + console.log(`[LOGS] Cleanup completed: ${deletedLogs} old log entries archived`); }, 2000); } } diff --git a/examples/large/mock-queue.ts b/examples/large/mock-queue.ts index b9a6c19..b520044 100644 --- a/examples/large/mock-queue.ts +++ b/examples/large/mock-queue.ts @@ -33,26 +33,27 @@ function simulateJobProcessing() { console.log(`[QUEUE] Processing job ${jobId} (${jobType})...`); - setTimeout(() => { - if (Math.random() > 0.85) { - const error = errors[Math.floor(Math.random() * errors.length)]; - failedJobs++; - pendingJobs = Math.max(0, pendingJobs - 1); - console.log(`[QUEUE] Job ${jobId} FAILED: ${error}`); - } else { - processedJobs++; - pendingJobs = Math.max(0, pendingJobs - 1); - console.log(`[QUEUE] Job ${jobId} completed in ${processingTime}s`); - } - }, parseFloat(processingTime) * 1000); + setTimeout( + () => { + if (Math.random() > 0.85) { + const error = errors[Math.floor(Math.random() * errors.length)]; + failedJobs++; + pendingJobs = Math.max(0, pendingJobs - 1); + console.log(`[QUEUE] Job ${jobId} FAILED: ${error}`); + } else { + processedJobs++; + pendingJobs = Math.max(0, pendingJobs - 1); + console.log(`[QUEUE] Job ${jobId} completed in ${processingTime}s`); + } + }, + parseFloat(processingTime) * 1000, + ); } if (Math.random() > 0.7) { const newJobs = Math.floor(Math.random() * 3) + 1; pendingJobs += newJobs; - console.log( - `[QUEUE] ${newJobs} new job(s) queued (${pendingJobs} pending)`, - ); + console.log(`[QUEUE] ${newJobs} new job(s) queued (${pendingJobs} pending)`); } if (Math.random() > 0.9) { diff --git a/examples/large/seed-data.ts b/examples/large/seed-data.ts index 4044455..b2b2c04 100644 --- a/examples/large/seed-data.ts +++ b/examples/large/seed-data.ts @@ -41,9 +41,7 @@ for (const seedSet of seedSets) { await new Promise((resolve) => setTimeout(resolve, duration)); } - console.log( - `[SEED-DATA] ✓ ${seedSet.name} completed (${seedSet.count} records)`, - ); + console.log(`[SEED-DATA] ✓ ${seedSet.name} completed (${seedSet.count} records)`); } console.log("[SEED-DATA] Creating indexes for seeded data..."); diff --git a/examples/large/web-server.ts b/examples/large/web-server.ts index 593e9c5..3bbe1f2 100644 --- a/examples/large/web-server.ts +++ b/examples/large/web-server.ts @@ -1,15 +1,11 @@ process.on("SIGTERM", () => { - console.log( - `[${process.env.SERVICE_NAME}] Received SIGTERM, shutting down gracefully...`, - ); + console.log(`[${process.env.SERVICE_NAME}] Received SIGTERM, shutting down gracefully...`); server.stop(); process.exit(0); }); process.on("SIGINT", () => { - console.log( - `[${process.env.SERVICE_NAME}] Received SIGINT, shutting down gracefully...`, - ); + console.log(`[${process.env.SERVICE_NAME}] Received SIGINT, shutting down gracefully...`); server.stop(); process.exit(0); }); @@ -22,12 +18,7 @@ await new Promise((resolve) => setTimeout(resolve, 4000)); let requestCount = 0; let staticFileRequests = 0; const pages = ["/", "/about", "/contact", "/login", "/dashboard", "/profile"]; -const staticFiles = [ - "/css/style.css", - "/js/app.js", - "/images/logo.png", - "/favicon.ico", -]; +const staticFiles = ["/css/style.css", "/js/app.js", "/images/logo.png", "/favicon.ico"]; const server = Bun.serve({ port: process.env.PORT ?? 8002, @@ -43,9 +34,7 @@ const server = Bun.serve({ } // Simulate static file requests - if ( - staticFiles.some((file) => url.pathname.startsWith(file.split("/")[1])) - ) { + if (staticFiles.some((file) => url.pathname.startsWith(file.split("/")[1]))) { staticFileRequests++; const responseTime = Math.random() * 50 + 10; @@ -86,9 +75,7 @@ const server = Bun.serve({ }, }); -console.log( - `[${process.env.SERVICE_NAME}] Web server running on http://localhost:${server.port}`, -); +console.log(`[${process.env.SERVICE_NAME}] Web server running on http://localhost:${server.port}`); console.log(`[${process.env.SERVICE_NAME}] Serving pages: ${pages.join(", ")}`); // Simulate periodic activity diff --git a/justfile b/justfile index 6bf151f..efbc9dd 100644 --- a/justfile +++ b/justfile @@ -19,7 +19,13 @@ bump-and-commit-version bump_type: git push --follow-tags fmt *args: - bun run biome format --write {{args}} + bun run oxfmt --write {{args}} + +lint: + bun run oxlint + +lint-fix: + bun run oxlint --fix current-version: #!/usr/bin/env bash diff --git a/package.json b/package.json index 6d5a48c..2004886 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,9 @@ { "name": "curse", "version": "0.0.22", - "module": "src/index.ts", - "type": "module", "private": false, - "devDependencies": { - "@biomejs/biome": "^2.2.3", - "@types/bun": "latest", - "react-devtools-core": "^6.1.5" - }, - "peerDependencies": { - "typescript": "^5" - }, + "type": "module", + "module": "src/index.ts", "dependencies": { "arktype": "^2.1.22", "gunshi": "^0.26.3", @@ -20,5 +12,14 @@ "react": "^19.1.1", "strip-ansi": "^7.1.2", "toml": "^3.0.0" + }, + "devDependencies": { + "@types/bun": "latest", + "oxfmt": "^0.34.0", + "oxlint": "^1.49.0", + "react-devtools-core": "^6.1.5" + }, + "peerDependencies": { + "typescript": "^5" } } diff --git a/scripts/build.ts b/scripts/build.ts index b015261..0e3ec61 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,5 +1,5 @@ -import { mkdir, unlink, readdir } from "fs/promises"; import { existsSync } from "fs"; +import { mkdir, unlink, readdir } from "fs/promises"; const targets: Bun.Build.Target[] = [ "bun-linux-x64", diff --git a/src/env.ts b/src/env.ts index 7182d86..b66f625 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,11 +1,7 @@ import { type } from "arktype"; const envSchema = type({ - "LOG_BUFFER_SIZE?": [ - "string", - "=>", - (logBufferSize) => Number(logBufferSize), - ], + "LOG_BUFFER_SIZE?": ["string", "=>", (logBufferSize) => Number(logBufferSize)], }); const parsedEnv = envSchema(process.env); diff --git a/src/hooks/useProcessManager.tsx b/src/hooks/useProcessManager.tsx index b9bd7b3..1ada3d9 100644 --- a/src/hooks/useProcessManager.tsx +++ b/src/hooks/useProcessManager.tsx @@ -1,3 +1,4 @@ +import { spawn, type Subprocess } from "bun"; import { createContext, useContext, @@ -7,12 +8,11 @@ import { useRef, type RefObject, } from "react"; -import { spawn, type Subprocess } from "bun"; -import { LogBuffer, readStreamToBuffer } from "../lib/LogBuffer"; -import { invariant } from "../lib/invariant"; -import type { CurseConfig } from "../parser"; import { ENV } from "../env"; +import { invariant } from "../lib/invariant"; +import { LogBuffer, readStreamToBuffer } from "../lib/LogBuffer"; +import type { CurseConfig } from "../parser"; export enum ProcessStatus { Error = "error", @@ -142,9 +142,7 @@ function clearReadinessTimer(timer?: NodeJS.Timeout) { } } -async function getProcessProfile( - proc: Subprocess, -): Promise { +async function getProcessProfile(proc: Subprocess): Promise { if (!proc.pid) return null; try { @@ -190,10 +188,7 @@ function clearProfileTimer(timer?: NodeJS.Timeout) { } } -function areDependenciesSatisfied( - process: Process, - allProcesses: Process[], -): boolean { +function areDependenciesSatisfied(process: Process, allProcesses: Process[]): boolean { if (!process.deps || process.deps.length === 0) { return true; } @@ -229,14 +224,10 @@ function areDependenciesSatisfied( }); } -function shouldTriggerDependencyCheck( - oldProcess: Process, - newProcess: Process, -): boolean { +function shouldTriggerDependencyCheck(oldProcess: Process, newProcess: Process): boolean { const becameReady = !oldProcess.isReady && newProcess.isReady === true; const becameSuccessful = - oldProcess.status !== ProcessStatus.Success && - newProcess.status === ProcessStatus.Success; + oldProcess.status !== ProcessStatus.Success && newProcess.status === ProcessStatus.Success; const becameRunning = oldProcess.status !== ProcessStatus.Running && newProcess.status === ProcessStatus.Running && @@ -349,10 +340,7 @@ const ProcessManagerCtx = createContext({ runShutdownHook: async () => {}, }); -export function ProcessManagerProvider(props: { - config: CurseConfig; - children: React.ReactNode; -}) { +export function ProcessManagerProvider(props: { config: CurseConfig; children: React.ReactNode }) { const [selectedProcessIdx, setSelectedProcessIdx] = useState(0); const processesRef = useRef([]); const pendingRunRef = useRef(false); @@ -417,15 +405,9 @@ export function ProcessManagerProvider(props: { ); const newProcess = processesRef.current[processIdx]; - invariant( - newProcess, - `Process at index ${processIdx} not found after update`, - ); + invariant(newProcess, `Process at index ${processIdx} not found after update`); - if ( - shouldTriggerDependencyCheck(oldProcess, newProcess) && - !pendingRunRef.current - ) { + if (shouldTriggerDependencyCheck(oldProcess, newProcess) && !pendingRunRef.current) { pendingRunRef.current = true; queueMicrotask(() => { pendingRunRef.current = false; @@ -436,11 +418,8 @@ export function ProcessManagerProvider(props: { const runPendingProcesses = useCallback(() => { // Check if startup hook exists and hasn't completed - const startupHook = processesRef.current.find( - (p) => p.type === "startup_hook", - ); - const isStartupComplete = - !startupHook || startupHook.status === ProcessStatus.Success; + const startupHook = processesRef.current.find((p) => p.type === "startup_hook"); + const isStartupComplete = !startupHook || startupHook.status === ProcessStatus.Success; processesRef.current.map((p, i) => { if ( @@ -451,8 +430,7 @@ export function ProcessManagerProvider(props: { ) { void execProcess({ process: p, - updateProcess: (fields: UpdateProcessFields) => - updateProcess(i, fields), + updateProcess: (fields: UpdateProcessFields) => updateProcess(i, fields), }); } }); @@ -468,8 +446,7 @@ export function ProcessManagerProvider(props: { } await execProcess({ process: process, - updateProcess: (fields: UpdateProcessFields) => - updateProcess(selectedProcessIdx, fields), + updateProcess: (fields: UpdateProcessFields) => updateProcess(selectedProcessIdx, fields), }); }; @@ -477,13 +454,9 @@ export function ProcessManagerProvider(props: { if (!processesRef.current[processIdx]) { return; } - const { proc, status, readinessTimer, profileTimer } = - processesRef.current[processIdx]; + const { proc, status, readinessTimer, profileTimer } = processesRef.current[processIdx]; - if ( - !proc || - (status !== ProcessStatus.Running && status !== ProcessStatus.Starting) - ) { + if (!proc || (status !== ProcessStatus.Running && status !== ProcessStatus.Starting)) { return; } updateProcess(processIdx, { status: ProcessStatus.Killing }); @@ -515,9 +488,7 @@ export function ProcessManagerProvider(props: { }; const runStartupHook = async () => { - const startupHookIndex = processesRef.current.findIndex( - (p) => p.type === "startup_hook", - ); + const startupHookIndex = processesRef.current.findIndex((p) => p.type === "startup_hook"); if (startupHookIndex === -1) return; const startupHook = processesRef.current[startupHookIndex]; @@ -525,15 +496,12 @@ export function ProcessManagerProvider(props: { await execProcess({ process: startupHook, - updateProcess: (fields: UpdateProcessFields) => - updateProcess(startupHookIndex, fields), + updateProcess: (fields: UpdateProcessFields) => updateProcess(startupHookIndex, fields), }); }; const runShutdownHook = async () => { - const shutdownHookIndex = processesRef.current.findIndex( - (p) => p.type === "shutdown_hook", - ); + const shutdownHookIndex = processesRef.current.findIndex((p) => p.type === "shutdown_hook"); if (shutdownHookIndex === -1) return; const shutdownHook = processesRef.current[shutdownHookIndex]; @@ -545,8 +513,7 @@ export function ProcessManagerProvider(props: { await new Promise((resolve) => setTimeout(resolve, 50)); await execProcess({ process: { ...shutdownHook, status: ProcessStatus.Pending }, - updateProcess: (fields: UpdateProcessFields) => - updateProcess(shutdownHookIndex, fields), + updateProcess: (fields: UpdateProcessFields) => updateProcess(shutdownHookIndex, fields), }); }; @@ -579,9 +546,7 @@ export function ProcessManagerProvider(props: { } }); - const startupHookIndex = processesRef.current.findIndex( - (p) => p.type === "startup_hook", - ); + const startupHookIndex = processesRef.current.findIndex((p) => p.type === "startup_hook"); if (startupHookIndex !== -1) { const startupHook = processesRef.current[startupHookIndex]; if (startupHook) { @@ -589,8 +554,7 @@ export function ProcessManagerProvider(props: { await new Promise((resolve) => setTimeout(resolve, 50)); await execProcess({ process: { ...startupHook, status: ProcessStatus.Pending }, - updateProcess: (fields: UpdateProcessFields) => - updateProcess(startupHookIndex, fields), + updateProcess: (fields: UpdateProcessFields) => updateProcess(startupHookIndex, fields), }); } } diff --git a/src/index.ts b/src/index.ts index 176be53..2f8a815 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ -import { cli, define, type CommandContext } from "gunshi"; import { resolve } from "path"; +import { cli, define, type CommandContext } from "gunshi"; + +import { version } from "./generated/version"; import { parseCurseConfig } from "./parser"; import { renderView } from "./ui/View.tsx"; -import { version } from "./generated/version"; function makeCursePath(rawPath: string): string { if (rawPath.startsWith("/")) { @@ -45,9 +46,7 @@ const command = define({ if (ctx.values.path) { console.error(`Path does not point to .toml file: ${ctx.values.path}`); } else { - console.error( - "No config file found. Looking for curse.toml or curse.local.toml", - ); + console.error("No config file found. Looking for curse.toml or curse.local.toml"); } process.exit(1); } diff --git a/src/lib/Dequeue.test.ts b/src/lib/Dequeue.test.ts index 716cf65..503b129 100644 --- a/src/lib/Dequeue.test.ts +++ b/src/lib/Dequeue.test.ts @@ -1,6 +1,7 @@ // Vendored and adapted from: https://github.com/petkaantonov/deque import { describe, it, expect } from "bun:test"; + import { Deque } from "./Dequeue"; describe("Deque.prototype.constructor", () => { @@ -61,24 +62,18 @@ describe("Deque.prototype.push", () => { expect(ret).toBe(before + 1); expect(a.length).toBe(ret); expect(ret).toBe(16); - expect(a.toArray()).toEqual([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1, - ]); + expect(a.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1]); }); it("Should add single argument - over capacity", () => { - const a = new Deque([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - ]); + const a = new Deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); expect(a._capacity - a.length).toBe(0); const before = a.length; const ret = a.push(1); expect(ret).toBe(before + 1); expect(a.length).toBe(ret); expect(ret).toBe(17); - expect(a.toArray()).toEqual([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1, - ]); + expect(a.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1]); }); it("Should add multiple arguments - plenty of capacity", () => { @@ -100,9 +95,7 @@ describe("Deque.prototype.push", () => { expect(ret).toBe(before + 2); expect(a.length).toBe(ret); expect(ret).toBe(16); - expect(a.toArray()).toEqual([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1, 2, - ]); + expect(a.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 1, 2]); }); it("Should add multiple arguments - over capacity", () => { @@ -113,9 +106,7 @@ describe("Deque.prototype.push", () => { expect(ret).toBe(before + 2); expect(a.length).toBe(ret); expect(ret).toBe(17); - expect(a.toArray()).toEqual([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1, 2, - ]); + expect(a.toArray()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1, 2]); }); }); @@ -148,24 +139,18 @@ describe("Deque.prototype.unshift", () => { expect(ret).toBe(before + 1); expect(a.length).toBe(ret); expect(ret).toBe(16); - expect(a.toArray()).toEqual([ - 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ]); + expect(a.toArray()).toEqual([1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); }); it("Should add single argument - over capacity", () => { - const a = new Deque([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - ]); + const a = new Deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); expect(a._capacity - a.length).toBe(0); const before = a.length; const ret = a.unshift(1); expect(ret).toBe(before + 1); expect(a.length).toBe(ret); expect(ret).toBe(17); - expect(a.toArray()).toEqual([ - 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - ]); + expect(a.toArray()).toEqual([1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); }); it("Should add multiple arguments - plenty of capacity", () => { @@ -187,9 +172,7 @@ describe("Deque.prototype.unshift", () => { expect(ret).toBe(before + 2); expect(a.length).toBe(ret); expect(ret).toBe(16); - expect(a.toArray()).toEqual([ - 1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - ]); + expect(a.toArray()).toEqual([1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]); }); it("Should add multiple arguments - over capacity", () => { @@ -200,9 +183,7 @@ describe("Deque.prototype.unshift", () => { expect(ret).toBe(before + 2); expect(a.length).toBe(ret); expect(ret).toBe(17); - expect(a.toArray()).toEqual([ - 1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, - ]); + expect(a.toArray()).toEqual([1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); }); }); @@ -333,8 +314,8 @@ describe("Deque.prototype.peekBack", () => { ); expect(a.toArray()).toEqual([ - 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, - 12901, 10121, 0, 12, 3, 4, + 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, + 3, 4, ]); expect(a.peekBack()).toEqual([4]); @@ -348,8 +329,8 @@ describe("Deque.prototype.peekBack", () => { expect(a.peekBack()).toEqual([3]); expect(a.toArray()).toEqual([ - 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, - 12901, 10121, 0, 12, 3, 4, 1, 3, + 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 3, + 4, 1, 3, ]); }); @@ -492,8 +473,8 @@ describe("Deque.prototype.peekFront", () => { ); expect(a.toArray()).toEqual([ - 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, - 12901, 10121, 0, 12, 3, 4, + 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, + 3, 4, ]); expect(a.peekFront()).toEqual([1]); @@ -507,8 +488,8 @@ describe("Deque.prototype.peekFront", () => { expect(a.peekFront()).toEqual([2]); expect(a.toArray()).toEqual([ - 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, - 12901, 10121, 0, 12, 3, 4, 1, 3, + 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 1, 2, 3, 4, 5, 6, 78, 89, 12901, 10121, 0, 12, 3, + 4, 1, 3, ]); }); @@ -573,10 +554,7 @@ describe("Deque.prototype.clear", () => { }); describe("Deque resizing", () => { - function times( - x: number, - value: number | "index" | undefined, - ): (number | undefined)[] { + function times(x: number, value: number | "index" | undefined): (number | undefined)[] { const a: (number | undefined)[] = []; for (let i = 0; i < x; ++i) { a.push(value === "index" ? i : value); @@ -595,15 +573,11 @@ describe("Deque resizing", () => { expect(a.peekFront()).toEqual([2]); expect(a.get(3)).toBe(5); expect(a._capacity).toBe(16); - expect(a.toArray()).toEqual( - [2, 3, 4, 5].concat(times(12, undefined) as any), - ); + expect(a.toArray()).toEqual([2, 3, 4, 5].concat(times(12, undefined) as any)); a.push(6); expect(a._capacity).not.toBe(16); a.unshift(1); - expect(a.toArray()).toEqual( - [1, 2, 3, 4, 5].concat(times(12, undefined) as any, 6), - ); + expect(a.toArray()).toEqual([1, 2, 3, 4, 5].concat(times(12, undefined) as any, 6)); }); it("Resize doesn't require movement", () => { diff --git a/src/lib/Dequeue.ts b/src/lib/Dequeue.ts index 72c9bf1..790b248 100644 --- a/src/lib/Dequeue.ts +++ b/src/lib/Dequeue.ts @@ -37,9 +37,7 @@ function getCapacity(capacity: number | T[] | undefined): number { return DEQUE_MIN_CAPACITY; } } - return pow2AtLeast( - Math.min(Math.max(DEQUE_MIN_CAPACITY, capacity), DEQUE_MAX_CAPACITY), - ); + return pow2AtLeast(Math.min(Math.max(DEQUE_MIN_CAPACITY, capacity), DEQUE_MAX_CAPACITY)); } export class Deque { @@ -141,8 +139,7 @@ export class Deque { for (let i = argsLength - 1; i >= 0; i--) { this._checkCapacity(length + 1); capacity = this._capacity; - const j: number = - (((this._front - 1) & (capacity - 1)) ^ capacity) - capacity; + const j: number = (((this._front - 1) & (capacity - 1)) ^ capacity) - capacity; this[j] = arguments[i]; length++; this._length = length; @@ -152,8 +149,7 @@ export class Deque { } else { let front = this._front; for (let i = argsLength - 1; i >= 0; i--) { - const j: number = - (((front - 1) & (capacity - 1)) ^ capacity) - capacity; + const j: number = (((front - 1) & (capacity - 1)) ^ capacity) - capacity; this[j] = arguments[i]; front = j; } diff --git a/src/lib/LogBuffer.ts b/src/lib/LogBuffer.ts index 55f8237..a3f4350 100644 --- a/src/lib/LogBuffer.ts +++ b/src/lib/LogBuffer.ts @@ -1,4 +1,5 @@ import MiniSearch from "minisearch"; + import { Deque } from "./Dequeue"; export class LogBuffer { @@ -54,20 +55,14 @@ export class LogBuffer { return this.lines.peekBackReversed(count); } - getLinesByAbsolutePosition( - absoluteStartLine: number, - count: number, - ): string[] { + getLinesByAbsolutePosition(absoluteStartLine: number, count: number): string[] { if (count <= 0 || this.lines.length === 0) return []; const oldestLine = this.getOldestAvailableLineNumber(); const newestLine = this.totalLinesAdded - 1; // Check if the requested range is available - if ( - absoluteStartLine > newestLine || - absoluteStartLine + count <= oldestLine - ) { + if (absoluteStartLine > newestLine || absoluteStartLine + count <= oldestLine) { return []; } @@ -93,9 +88,7 @@ export class LogBuffer { isPositionValid(absolutePosition: number): boolean { const oldestLine = this.getOldestAvailableLineNumber(); - return ( - absolutePosition >= oldestLine && absolutePosition < this.totalLinesAdded - ); + return absolutePosition >= oldestLine && absolutePosition < this.totalLinesAdded; } getTotalLines(): number { @@ -110,9 +103,7 @@ export class LogBuffer { this.discardLineIdQueue.clear(); } - search( - query: string, - ): Array<{ text: string; lineNumber: number; score: number }> { + search(query: string): Array<{ text: string; lineNumber: number; score: number }> { if (!query.trim()) { return []; } diff --git a/src/lib/invariant.ts b/src/lib/invariant.ts index 59c9647..b4a5433 100644 --- a/src/lib/invariant.ts +++ b/src/lib/invariant.ts @@ -1,7 +1,4 @@ -export function invariant( - condition: unknown, - message: string, -): asserts condition { +export function invariant(condition: unknown, message: string): asserts condition { if (!condition) { throw new Error(`Invariant violation: ${message}`); } diff --git a/src/parser.ts b/src/parser.ts index cad3952..501ba05 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -62,12 +62,8 @@ export async function parseCurseConfig(path: string): Promise { const uniqueNames = new Set(allNames); if (allNames.length !== uniqueNames.size) { - const duplicates = allNames.filter( - (name, index) => allNames.indexOf(name) !== index, - ); - console.error( - `Duplicate names found: ${[...new Set(duplicates)].join(", ")}`, - ); + const duplicates = allNames.filter((name, index) => allNames.indexOf(name) !== index); + console.error(`Duplicate names found: ${[...new Set(duplicates)].join(", ")}`); process.exit(1); } diff --git a/src/ui/View.tsx b/src/ui/View.tsx index 4f9989a..143b36b 100644 --- a/src/ui/View.tsx +++ b/src/ui/View.tsx @@ -1,25 +1,20 @@ import { Box, render, Text, useInput } from "ink"; import { useEffect } from "react"; -import type { CurseConfig } from "../parser"; -import { Colors } from "../lib/Colors"; -import { LogPage } from "./views/LogPage"; -import { MainPage } from "./views/MainPage"; -import { ProcessManagerProvider } from "../hooks/useProcessManager"; +import { version } from "../generated/version"; import { useAltScreen } from "../hooks/useAltScreen"; import { usePage, PageProvider, ViewPage } from "../hooks/usePage"; +import { ProcessManagerProvider } from "../hooks/useProcessManager"; import { useProcessManager } from "../hooks/useProcessManager"; -import { - ProgramStateProvider, - useProgramState, - ProgramStatus, -} from "../hooks/useProgramState"; -import { version } from "../generated/version"; +import { ProgramStateProvider, useProgramState, ProgramStatus } from "../hooks/useProgramState"; +import { Colors } from "../lib/Colors"; +import type { CurseConfig } from "../parser"; +import { LogPage } from "./views/LogPage"; +import { MainPage } from "./views/MainPage"; function View(props: { config: CurseConfig }) { const { isReady } = useAltScreen(); - const { runPendingProcesses, killAllProcesses, runStartupHook } = - useProcessManager(); + const { runPendingProcesses, killAllProcesses, runStartupHook } = useProcessManager(); const { page } = usePage(); const { status } = useProgramState(); @@ -52,9 +47,7 @@ function View(props: { config: CurseConfig }) { v{version} - {status === ProgramStatus.Quitting && ( - Quitting... - )} + {status === ProgramStatus.Quitting && Quitting...} Config: diff --git a/src/ui/components/LogTailPreview.tsx b/src/ui/components/LogTailPreview.tsx index ba28dd2..cf7667a 100644 --- a/src/ui/components/LogTailPreview.tsx +++ b/src/ui/components/LogTailPreview.tsx @@ -14,11 +14,7 @@ export function LogTailPreview(props: { height: number }) { const linesPerPage = props.height - 2; // Account for borders only return selectedProcess.logBuffer.getRecentLines(linesPerPage); - }, [ - selectedProcess, - selectedProcess?.logBuffer.getTotalLines(), - props.height, - ]); + }, [selectedProcess, selectedProcess?.logBuffer.getTotalLines(), props.height]); if (!selectedProcess) { return ( @@ -50,10 +46,7 @@ export function LogTailPreview(props: { height: number }) { ) : ( logs.map((log: string, index: number) => ( - + {preprocessLog(log)} diff --git a/src/ui/components/ShortcutFooter.tsx b/src/ui/components/ShortcutFooter.tsx index adaeb3f..a5a1102 100644 --- a/src/ui/components/ShortcutFooter.tsx +++ b/src/ui/components/ShortcutFooter.tsx @@ -1,4 +1,5 @@ import { Box, Text, useStdout } from "ink"; + import { Colors } from "../../lib/Colors"; interface ShortcutFooterProps { @@ -6,10 +7,7 @@ interface ShortcutFooterProps { showShortcuts: boolean; } -export function ShortcutFooter({ - shortcuts, - showShortcuts, -}: ShortcutFooterProps) { +export function ShortcutFooter({ shortcuts, showShortcuts }: ShortcutFooterProps) { const { stdout } = useStdout(); const terminalWidth = stdout.columns; @@ -29,10 +27,7 @@ export function ShortcutFooter({ const columns = []; for (let col = 0; col < numColumns; col++) { const startIdx = col * itemsPerColumn; - const endIdx = Math.min( - startIdx + itemsPerColumn, - shortcuts.length, - ); + const endIdx = Math.min(startIdx + itemsPerColumn, shortcuts.length); const columnShortcuts = shortcuts.slice(startIdx, endIdx); columns.push( diff --git a/src/ui/views/LogPage.tsx b/src/ui/views/LogPage.tsx index b7f7985..7051947 100644 --- a/src/ui/views/LogPage.tsx +++ b/src/ui/views/LogPage.tsx @@ -1,18 +1,15 @@ +import { $ } from "bun"; import { Box, Text, useInput, useStdout } from "ink"; import { useEffect, useState, useMemo, useCallback } from "react"; -import { $ } from "bun"; import stripAnsi from "strip-ansi"; import { usePage, ViewPage } from "../../hooks/usePage"; import { useProcessManager } from "../../hooks/useProcessManager"; -import { useRenderTick } from "../../hooks/useRenderTick"; import { useProgramState, ProgramStatus } from "../../hooks/useProgramState"; +import { useRenderTick } from "../../hooks/useRenderTick"; import { Colors } from "../../lib/Colors"; import { preprocessLog } from "../../lib/LogProcessing"; -import { - ShortcutFooter, - getShortcutFooterHeight, -} from "../components/ShortcutFooter"; +import { ShortcutFooter, getShortcutFooterHeight } from "../components/ShortcutFooter"; function LogTable(props: { height: number; @@ -39,8 +36,7 @@ function LogTable(props: { const [numberPrefix, setNumberPrefix] = useState(""); const [waitingForSecondG, setWaitingForSecondG] = useState(false); const [isSelectMode, setIsSelectMode] = useState(false); - const [selectionStartAbsoluteLine, setSelectionStartAbsoluteLine] = - useState(0); + const [selectionStartAbsoluteLine, setSelectionStartAbsoluteLine] = useState(0); const [selectionStartViewIndex, setSelectionStartViewIndex] = useState(0); const [searchViewStartIndex, setSearchViewStartIndex] = useState(0); const [showCopyIndicator, setShowCopyIndicator] = useState(false); @@ -65,10 +61,7 @@ function LogTable(props: { } const parts = []; - const regex = new RegExp( - `(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, - "gi", - ); + const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi"); const matches = text.split(regex); for (let i = 0; i < matches.length; i++) { @@ -110,12 +103,7 @@ function LogTable(props: { // Notify parent that we've handled the request onViewInContextHandled(); } - }, [ - viewInContextRequested, - selectedProcess, - props.height, - onViewInContextHandled, - ]); + }, [viewInContextRequested, selectedProcess, props.height, onViewInContextHandled]); // Notify parent about current search cursor position useEffect(() => { @@ -125,20 +113,15 @@ function LogTable(props: { appliedSearchQuery.trim() && selectedProcess ) { - const searchResults = - selectedProcess.logBuffer.search(appliedSearchQuery); + const searchResults = selectedProcess.logBuffer.search(appliedSearchQuery); const sortedResults = searchResults.sort( - (a: { lineNumber: number }, b: { lineNumber: number }) => - a.lineNumber - b.lineNumber, + (a: { lineNumber: number }, b: { lineNumber: number }) => a.lineNumber - b.lineNumber, ); // Calculate the actual cursor position within search results let actualCursorIndex = 0; if (autoScroll) { - actualCursorIndex = Math.max( - 0, - sortedResults.length - (props.height - 3) + cursorIndex, - ); + actualCursorIndex = Math.max(0, sortedResults.length - (props.height - 3) + cursorIndex); } else { actualCursorIndex = searchViewStartIndex + cursorIndex; } @@ -158,8 +141,7 @@ function LogTable(props: { if (!selectedProcess) return 0; if (autoScroll) { - const oldestLine = - selectedProcess.logBuffer.getOldestAvailableLineNumber(); + const oldestLine = selectedProcess.logBuffer.getOldestAvailableLineNumber(); const totalLines = selectedProcess.logBuffer.getTotalLines(); const viewStart = oldestLine + Math.max(0, totalLines - linesPerPage); return viewStart + cursorIndex; @@ -173,8 +155,7 @@ function LogTable(props: { if (!selectedProcess) return 0; if (autoScroll) { - const oldestLine = - selectedProcess.logBuffer.getOldestAvailableLineNumber(); + const oldestLine = selectedProcess.logBuffer.getOldestAvailableLineNumber(); const totalLines = selectedProcess.logBuffer.getTotalLines(); const viewStart = oldestLine + Math.max(0, totalLines - linesPerPage); return viewStart + index; @@ -197,19 +178,10 @@ function LogTable(props: { const currentAbsoluteLine = getCurrentAbsoluteLine(); const lineAbsolutePosition = getAbsoluteLineForIndex(index); - const selectionStart = Math.min( - selectionStartAbsoluteLine, - currentAbsoluteLine, - ); - const selectionEnd = Math.max( - selectionStartAbsoluteLine, - currentAbsoluteLine, - ); + const selectionStart = Math.min(selectionStartAbsoluteLine, currentAbsoluteLine); + const selectionEnd = Math.max(selectionStartAbsoluteLine, currentAbsoluteLine); - return ( - lineAbsolutePosition >= selectionStart && - lineAbsolutePosition <= selectionEnd - ); + return lineAbsolutePosition >= selectionStart && lineAbsolutePosition <= selectionEnd; } }; @@ -219,14 +191,8 @@ function LogTable(props: { // When showing search results, work with the filtered logs array if (currentSearchQuery && currentSearchQuery.trim()) { const currentIndex = cursorIndex; - const selectionStartIndex = Math.min( - selectionStartViewIndex || 0, - currentIndex, - ); - const selectionEndIndex = Math.max( - selectionStartViewIndex || 0, - currentIndex, - ); + const selectionStartIndex = Math.min(selectionStartViewIndex || 0, currentIndex); + const selectionEndIndex = Math.max(selectionStartViewIndex || 0, currentIndex); const selectedLines = []; for (let i = selectionStartIndex; i <= selectionEndIndex; i++) { @@ -238,21 +204,14 @@ function LogTable(props: { } else { // Original behavior for non-search mode const currentAbsoluteLine = getCurrentAbsoluteLine(); - const selectionStart = Math.min( - selectionStartAbsoluteLine, - currentAbsoluteLine, - ); - const selectionEnd = Math.max( - selectionStartAbsoluteLine, - currentAbsoluteLine, - ); + const selectionStart = Math.min(selectionStartAbsoluteLine, currentAbsoluteLine); + const selectionEnd = Math.max(selectionStartAbsoluteLine, currentAbsoluteLine); const selectionCount = selectionEnd - selectionStart + 1; - const selectedLines = - selectedProcess.logBuffer.getLinesByAbsolutePosition( - selectionStart, - selectionCount, - ); + const selectedLines = selectedProcess.logBuffer.getLinesByAbsolutePosition( + selectionStart, + selectionCount, + ); return selectedLines.join("\n"); } @@ -263,7 +222,7 @@ function LogTable(props: { try { await $`echo ${cleanText} | pbcopy`; showCopyFeedback(indicatorText); - } catch (error) { + } catch { try { // Fallback for non-macOS systems await $`echo ${cleanText} | xclip -selection clipboard`; @@ -290,8 +249,7 @@ function LogTable(props: { if (!currentSearchQuery || !currentSearchQuery.trim()) return []; const searchResults = selectedProcess.logBuffer.search(currentSearchQuery); return searchResults.sort( - (a: { lineNumber: number }, b: { lineNumber: number }) => - a.lineNumber - b.lineNumber, + (a: { lineNumber: number }, b: { lineNumber: number }) => a.lineNumber - b.lineNumber, ); }, [currentSearchQuery, selectedProcess.logBuffer.getTotalLines()]); @@ -299,19 +257,14 @@ function LogTable(props: { if (autoScroll) { // In autoscroll mode, show the most recent search results const startIndex = Math.max(0, sortedSearchResults.length - linesPerPage); - logs = sortedSearchResults - .slice(startIndex) - .map((result: { text: string }) => result.text); + logs = sortedSearchResults.slice(startIndex).map((result: { text: string }) => result.text); } else { // In manual scroll mode, show results from current view position const startIndex = Math.min( searchViewStartIndex, Math.max(0, sortedSearchResults.length - linesPerPage), ); - const endIndex = Math.min( - startIndex + linesPerPage, - sortedSearchResults.length, - ); + const endIndex = Math.min(startIndex + linesPerPage, sortedSearchResults.length); logs = sortedSearchResults .slice(startIndex, endIndex) .map((result: { text: string }) => result.text); @@ -327,25 +280,17 @@ function LogTable(props: { } else { if (!selectedProcess.logBuffer.isPositionValid(viewStartLine)) { // Cap viewStartLine to valid bounds instead of falling back to autoscroll - const oldestLine = - selectedProcess.logBuffer.getOldestAvailableLineNumber(); - const newestLine = - selectedProcess.logBuffer.getTotalLines() + oldestLine; + const oldestLine = selectedProcess.logBuffer.getOldestAvailableLineNumber(); + const newestLine = selectedProcess.logBuffer.getTotalLines() + oldestLine; const maxStartLine = Math.max(oldestLine, newestLine - linesPerPage); - const cappedViewStartLine = Math.min( - Math.max(viewStartLine, oldestLine), - maxStartLine, - ); + const cappedViewStartLine = Math.min(Math.max(viewStartLine, oldestLine), maxStartLine); setViewStartLine(cappedViewStartLine); logs = selectedProcess.logBuffer.getLinesByAbsolutePosition( cappedViewStartLine, linesPerPage, ); } else { - logs = selectedProcess.logBuffer.getLinesByAbsolutePosition( - viewStartLine, - linesPerPage, - ); + logs = selectedProcess.logBuffer.getLinesByAbsolutePosition(viewStartLine, linesPerPage); } } } @@ -421,9 +366,7 @@ function LogTable(props: { } // Get the repeat count from number prefix (default to 1) - const repeatCount = numberPrefix - ? Math.max(1, parseInt(numberPrefix, 10)) - : 1; + const repeatCount = numberPrefix ? Math.max(1, parseInt(numberPrefix, 10)) : 1; // Handle 'g' sequence for vim-style navigation if (input === "g") { @@ -437,8 +380,7 @@ function LogTable(props: { setAutoScroll(false); } else { // Normal behavior for non-search mode - const oldestLine = - selectedProcess.logBuffer.getOldestAvailableLineNumber(); + const oldestLine = selectedProcess.logBuffer.getOldestAvailableLineNumber(); setViewStartLine(oldestLine); setCursorIndex(0); setAutoScroll(false); @@ -456,16 +398,10 @@ function LogTable(props: { if (selectedProcess) { if (currentSearchQuery && currentSearchQuery.trim()) { // When search is active, jump to last search result - const maxStartIndex = Math.max( - 0, - sortedSearchResults.length - linesPerPage, - ); + const maxStartIndex = Math.max(0, sortedSearchResults.length - linesPerPage); setSearchViewStartIndex(maxStartIndex); setCursorIndex( - Math.min( - linesPerPage - 1, - sortedSearchResults.length - maxStartIndex - 1, - ), + Math.min(linesPerPage - 1, sortedSearchResults.length - maxStartIndex - 1), ); setAutoScroll(true); // Jump to end enables autoscroll } else { @@ -475,12 +411,7 @@ function LogTable(props: { selectedProcess.logBuffer.getTotalLines(); const maxStartLine = Math.max(0, newestLine - linesPerPage); setViewStartLine(maxStartLine); - setCursorIndex( - Math.min( - linesPerPage - 1, - selectedProcess.logBuffer.getTotalLines() - 1, - ), - ); + setCursorIndex(Math.min(linesPerPage - 1, selectedProcess.logBuffer.getTotalLines() - 1)); setAutoScroll(true); // Jump to end enables autoscroll } } @@ -496,11 +427,9 @@ function LogTable(props: { if (input === "s") { if (autoScroll && selectedProcess) { - const oldestLine = - selectedProcess.logBuffer.getOldestAvailableLineNumber(); + const oldestLine = selectedProcess.logBuffer.getOldestAvailableLineNumber(); const currentBufferSize = selectedProcess.logBuffer.getTotalLines(); - const absoluteStartLine = - oldestLine + Math.max(0, currentBufferSize - linesPerPage); + const absoluteStartLine = oldestLine + Math.max(0, currentBufferSize - linesPerPage); setViewStartLine(absoluteStartLine); } setAutoScroll(!autoScroll); @@ -554,8 +483,7 @@ function LogTable(props: { remainingMoves--; } else if (selectedProcess) { // Cursor is at top, try to scroll up - const oldestLine = - selectedProcess.logBuffer.getOldestAvailableLineNumber(); + const oldestLine = selectedProcess.logBuffer.getOldestAvailableLineNumber(); const possibleNewViewStart = Math.max(oldestLine, newViewStart - 1); if (possibleNewViewStart !== newViewStart) { newViewStart = possibleNewViewStart; @@ -595,10 +523,7 @@ function LogTable(props: { remainingMoves--; } else { // Cursor is at bottom, try to scroll down through search results - const maxStartIndex = Math.max( - 0, - sortedSearchResults.length - linesPerPage, - ); + const maxStartIndex = Math.max(0, sortedSearchResults.length - linesPerPage); if (newSearchViewStart < maxStartIndex) { newSearchViewStart++; remainingMoves--; @@ -635,10 +560,7 @@ function LogTable(props: { selectedProcess.logBuffer.getOldestAvailableLineNumber() + selectedProcess.logBuffer.getTotalLines(); const maxStartLine = newestLine - linesPerPage; - const possibleNewViewStart = Math.min( - maxStartLine, - newViewStart + 1, - ); + const possibleNewViewStart = Math.min(maxStartLine, newViewStart + 1); if (possibleNewViewStart !== newViewStart) { newViewStart = possibleNewViewStart; remainingMoves--; @@ -664,11 +586,7 @@ function LogTable(props: { }); // Helper functions for text colors - const getTextColor = ( - isCursor: boolean, - isSelected: boolean, - log: string, - ) => { + const getTextColor = (isCursor: boolean, isSelected: boolean, log: string) => { if (isCursor) return "white"; if (isSelected) return Colors.purple; if (log.includes("stderr")) return "red"; @@ -700,28 +618,16 @@ function LogTable(props: { {autoScroll ? "on" : "off"} {positionLost && ( - - {" "} - (position lost, returned to tail) - - )} - {numberPrefix && ( - [{numberPrefix}] + (position lost, returned to tail) )} + {numberPrefix && [{numberPrefix}]} {waitingForSecondG && [g]} {isSelectMode && [SELECT]} - {isSearchMode && ( - </{searchQuery}> - )} + {isSearchMode && </{searchQuery}>} {!isSearchMode && appliedSearchQuery && ( - - {" "} - </{appliedSearchQuery}> - - )} - {showCopyIndicator && ( - ✓ {copyIndicatorText} + </{appliedSearchQuery}> )} + {showCopyIndicator && ✓ {copyIndicatorText}} {logs.map((log, index) => { @@ -743,11 +649,7 @@ function LogTable(props: { return ( - + {textParts.map((part, partIndex) => { const processedText = isCursor || isSelected @@ -759,12 +661,7 @@ function LogTable(props: { return ( {displayText} @@ -797,10 +694,7 @@ export function LogPage() { const [isSelectMode, setIsSelectMode] = useState(false); const handleSearchCursorChange = useCallback( - ( - cursorIndex: number, - searchResults: Array<{ lineNumber: number; text: string }>, - ) => { + (cursorIndex: number, searchResults: Array<{ lineNumber: number; text: string }>) => { setCurrentSearchCursor(cursorIndex); setCurrentSearchResults(searchResults); }, @@ -877,10 +771,7 @@ export function LogPage() { // Handle "view in context" with 'o' key when search is applied if (input === "o" && appliedSearchQuery && !key.shift) { - if ( - currentSearchResults.length > 0 && - currentSearchCursor < currentSearchResults.length - ) { + if (currentSearchResults.length > 0 && currentSearchCursor < currentSearchResults.length) { const currentResult = currentSearchResults[currentSearchCursor]; if (currentResult) { // Clear the search and navigate to that line @@ -934,11 +825,7 @@ export function LogPage() { terminalHeight - 6 - (isSearchMode ? 1 : 0) - - getShortcutFooterHeight( - shortcuts.length, - terminalWidth, - showShortcuts, - ) + getShortcutFooterHeight(shortcuts.length, terminalWidth, showShortcuts) } isSearchMode={isSearchMode} searchQuery={searchQuery} diff --git a/src/ui/views/MainPage.tsx b/src/ui/views/MainPage.tsx index 58f6561..69e671d 100644 --- a/src/ui/views/MainPage.tsx +++ b/src/ui/views/MainPage.tsx @@ -3,14 +3,11 @@ import { useState } from "react"; import { usePage, ViewPage } from "../../hooks/usePage"; import { useProcessManager, type Process } from "../../hooks/useProcessManager"; -import { useRenderTick } from "../../hooks/useRenderTick"; import { useProgramState, ProgramStatus } from "../../hooks/useProgramState"; +import { useRenderTick } from "../../hooks/useRenderTick"; import { Colors } from "../../lib/Colors"; -import { - ShortcutFooter, - getShortcutFooterHeight, -} from "../components/ShortcutFooter"; import { LogTailPreview } from "../components/LogTailPreview"; +import { ShortcutFooter, getShortcutFooterHeight } from "../components/ShortcutFooter"; function getReadinessDisplay(process: Process): { char: string; @@ -43,10 +40,7 @@ function getReadinessDisplay(process: Process): { return { char, color }; } -function ProcessTable(props: { - numberPrefix: string; - waitingForSecondG: boolean; -}) { +function ProcessTable(props: { numberPrefix: string; waitingForSecondG: boolean }) { const { processesRef, selectedProcessIdx } = useProcessManager(); const processes = processesRef.current; const { stdout } = useStdout(); @@ -55,23 +49,11 @@ function ProcessTable(props: { const terminalWidth = stdout?.columns ?? 80; const fixedColumnsWidth = 10 + 2 + 8 + 2 + 8 + 2 + 8 + 2 + 8; // STATUS + margin + READY + margin + AGE + margin + MEM + margin + CPU const borderAndPadding = 4; // border + padding - const nameColumnWidth = Math.max( - 20, - terminalWidth - fixedColumnsWidth - borderAndPadding, - ); + const nameColumnWidth = Math.max(20, terminalWidth - fixedColumnsWidth - borderAndPadding); return ( - - + + NAME @@ -89,12 +71,8 @@ function ProcessTable(props: { CPU - {props.numberPrefix && ( - [{props.numberPrefix}] - )} - {props.waitingForSecondG && ( - [g] - )} + {props.numberPrefix && [{props.numberPrefix}]} + {props.waitingForSecondG && [g]} {processes.map((process: Process, index: number) => { @@ -135,9 +113,7 @@ function ProcessTable(props: { const { char, color } = getReadinessDisplay(process); return ( {char} @@ -276,14 +252,10 @@ export function MainPage() { } // Get the repeat count from number prefix (default to 1) - const repeatCount = numberPrefix - ? Math.max(1, parseInt(numberPrefix, 10)) - : 1; + const repeatCount = numberPrefix ? Math.max(1, parseInt(numberPrefix, 10)) : 1; if (key.downArrow || input === "j") { - setSelectedProcessIdx((prev: number) => - Math.min(prev + repeatCount, processes.length - 1), - ); + setSelectedProcessIdx((prev: number) => Math.min(prev + repeatCount, processes.length - 1)); setNumberPrefix(""); } else if (key.upArrow || input === "k") { setSelectedProcessIdx((prev: number) => Math.max(prev - repeatCount, 0)); @@ -331,10 +303,7 @@ export function MainPage() { return ( - +