diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf4f669..21096bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,9 @@ jobs: run: rake - name: Build gem - run: gem build try-cli.gemspec + run: | + make dist + gem build try-cli.gemspec - name: Configure trusted publishing credentials uses: rubygems/configure-rubygems-credentials@main diff --git a/AGENTS.md b/AGENTS.md index 3ac917a..39d6d6d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,12 @@ # Repository Guidelines ## Project Structure & Module Organization -- `try.rb`: Single-file Ruby CLI and TUI (no gems). -- `flake.nix`/`flake.lock`: Nix packaging and Home Manager module. +- `try.rb`, `lib/tui.rb`, `lib/fuzzy.rb`: Source of truth (edit these). Do not turn the repo into only the concat. +- `dist/try.rb`: Generated single-file Ruby (`make dist` concatenates lib/tui.rb + lib/fuzzy.rb + try.rb). Gitignored. +- `dist/try`: Optional Spinel native binary (`make native` compiles `dist/try.rb`). Gitignored. +- `bin/try`: Tiny router — execs `dist/try` when that executable exists, otherwise loads `dist/try.rb`. +- Gem (`try-cli`): packages `bin/try` + `dist/try.rb` (and LICENSE). Not the split sources, not `dist/try.c`. A local `make native && gem build` may also include `dist/try`; release CI must not run `make native`. +- `flake.nix`/`flake.lock`: Nix packaging and Home Manager module (`make dist` + bin/try + dist/try.rb). - `README.md`: Usage, installation, and philosophy. - Tries live outside this repo (default `~/src/tries`, configurable via `TRY_PATH`). @@ -23,25 +27,26 @@ - Emitted commands use absolute, quoted paths and are shell-neutral (bash/zsh/fish). Only the wrapper function generated by `init` differs per shell. ## Build, Test, and Development Commands -- `rake` / `rake test`: lint, unit, MRI spec; if Spinel is present, also emit `dist/try.c`, compile `dist/try`, native spec, and `runner_and_compare.sh`. -- `rake lint`: `ruby -c` always; `spinel -c` on `try.rb` and `lib/**/*.rb` when Spinel is available. -- `rake spec_spinel`: `make native` (writes `dist/try.c` then compiles), spec the binary, compare outputs with MRI. +- `make dist` / `rake dist`: concatenate sources into `dist/try.rb`. +- `rake` / `rake test`: lint, unit, MRI spec on `./try.rb` and `dist/try.rb`; if Spinel is present, also emit `dist/try.c` from the concat, compile `dist/try`, native spec, and `runner_and_compare.sh`. +- `rake lint`: `ruby -c` on source files and generated `dist/try.rb`; `spinel -c` on those same files when Spinel is available. +- `rake spec_spinel`: `make native` (compiles `dist/try.rb`), spec the binary, compare outputs with `dist/try.rb`. - `nix run`: Run the packaged CLI (e.g., `nix run . -- --help`). -- `nix build`: Build the binary derivation; output at `./result/bin/try`. -- `./try.rb init ~/src/tries`: Emit shell function for your shell config. +- `nix build`: Build the derivation; output at `./result/bin/try` (router + `dist/try.rb`). +- `./try.rb init ~/src/tries`: Emit shell function for your shell config (dev; split sources). - `./try.rb cd`: Launch interactive selector; prints `cd` script to stdout. - `./try.rb clone [name]`: Clone into date-prefixed directory. -- `make native SPINEL=/path/to/spinel`: AOT a native `dist/try`. +- `make native SPINEL=/path/to/spinel`: AOT a native `dist/try` from `dist/try.rb`. ## Dual implementation (MRI + Spinel) `try` must keep working on **both**: -- MRI: `ruby try.rb` / the `try-cli` gem -- Spinel AOT: `spinel try.rb -o dist/try` (see `make native`) +- MRI: `ruby try.rb` (dev, split sources) / `dist/try.rb` / the `try-cli` gem (`bin/try` → `dist/try.rb`) +- Spinel AOT: compiles **`dist/try.rb`** (one compilation unit) → `dist/try` (see `make native`) -A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` on those same files when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues. +A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` on the source files plus `dist/try.rb` when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues. ## Coding Style & Naming Conventions -- Ruby, 2-space indent, standard library only; keep it single-file unless necessary. +- Ruby, 2-space indent, standard library only; edit the split sources, generate `dist/try.rb` for ship/native. - Prefer small, pure functions; no global state beyond `ENV` reads. - UI tokens live in `UI::TOKEN_MAP`; add tokens with clear names. UI printing runs through `UI.expand_tokens`. - Shell emission goes through `UI.emit_tasks_script` (don’t handcraft `$dir` scripts). @@ -58,9 +63,9 @@ A construct that only one of them can run is a bug. Prefer stdlib and Spinel-fri **Important**: Specs must reflect the full feature set of `try`. They serve as the canonical reference for behavior, enabling new implementations (in any language) to be validated against the same test suite. When adding or changing features, update both the relevant spec markdown and add corresponding tests. ## Testing Guidelines -- `rake` runs lint + unit + spec. Lint is `ruby -c` on `try.rb` and `lib/**/*.rb`, plus `spinel -c` on each of `try.rb` and `lib/**/*.rb` when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail. -- Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI). -- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel). `make native-compare` / `rake spec_spinel` diffs MRI vs native output (init exec prefix is normalized). +- `rake` runs lint + unit + spec on `./try.rb` and `dist/try.rb`. Lint is `ruby -c` on `try.rb`, `lib/**/*.rb`, and generated `dist/try.rb`, plus `spinel -c` on those files when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail. +- Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI source). Also `make dist-test` / `bash spec/tests/runner.sh dist/try.rb` and `bash spec/tests/runner.sh bin/try` (after `make dist`). +- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel, compiled from `dist/try.rb`). `make native-compare` / `rake spec_spinel` diffs `dist/try.rb` vs native output (init exec prefix is normalized). - Manual flows for exploratory testing: - `TRY_PATH=$(mktemp -d) ./try.rb cd` then create/select directories. - Validate delete confirmation and scoring by changing `mtime`/`ctime`. diff --git a/Formula/try.rb b/Formula/try.rb index e147885..b93d066 100644 --- a/Formula/try.rb +++ b/Formula/try.rb @@ -9,11 +9,9 @@ class Try < Formula depends_on "ruby" def install - bin.install "try.rb" => "try" - if build.head? - (bin/"lib").install "lib/tui.rb" - (bin/"lib").install "lib/fuzzy.rb" - end + system "make", "dist" + bin.install "bin/try" + (prefix/"dist").install "dist/try.rb" end def caveats diff --git a/Makefile b/Makefile index a207a13..9fc35ea 100644 --- a/Makefile +++ b/Makefile @@ -32,20 +32,54 @@ lint: ## Check Ruby syntax $(RUBY) -c "$$file"; \ done +.PHONY: dist +dist: dist/try.rb ## Concatenate lib/ + try.rb into dist/try.rb + +# One working file: lib/tui.rb, lib/fuzzy.rb, then try.rb (no require_relative). +dist/try.rb: lib/tui.rb lib/fuzzy.rb try.rb + @mkdir -p dist + @{ \ + printf '%s\n' '#!/usr/bin/env ruby'; \ + printf '%s\n' '# Generated by make dist. Do not edit.'; \ + printf '%s\n' '# Source: lib/tui.rb + lib/fuzzy.rb + try.rb'; \ + printf '\n'; \ + cat lib/tui.rb; \ + printf '\n'; \ + cat lib/fuzzy.rb; \ + printf '\n'; \ + sed -e '/^#!/d' \ + -e '/require_relative .lib\/tui./d' \ + -e '/require_relative .lib\/fuzzy./d' \ + try.rb; \ + } > dist/try.rb + @chmod +x dist/try.rb + @echo "Wrote dist/try.rb" + +.PHONY: dist-test +dist-test: dist/try.rb ## Run specs against dist/try.rb + bash spec/tests/runner.sh dist/try.rb + .PHONY: install -install: ## Install try.rb to ~/.local/ - @echo "Installing $(SCRIPT) to ~/.local/..." - @mkdir -p ~/.local - @cp $(SCRIPT) ~/.local/ - @chmod +x ~/.local/$(SCRIPT) - @echo "Installed! Add to your shell:" - @echo " eval \"\$$(~/.local/$(SCRIPT) init ~/src/tries)\"" +install: dist/try.rb ## Install bin/try + dist/ to ~/.local/ + @echo "Installing try to ~/.local/..." + @mkdir -p ~/.local/bin ~/.local/dist + @cp bin/try ~/.local/bin/try + @chmod +x ~/.local/bin/try + @cp dist/try.rb ~/.local/dist/try.rb + @chmod +x ~/.local/dist/try.rb + @if [ -f dist/try ] && [ -x dist/try ]; then cp dist/try ~/.local/dist/try && chmod +x ~/.local/dist/try; fi + @echo "Installed! Add ~/.local/bin to PATH, then:" + @echo " eval \"\$$(try init ~/src/tries)\"" .PHONY: install-global -install-global: ## Install try.rb to /usr/local/bin/ - @echo "Installing $(SCRIPT) to /usr/local/bin/..." - @sudo cp $(SCRIPT) /usr/local/bin/try +install-global: dist/try.rb ## Install bin/try + dist/ to /usr/local/ + @echo "Installing try to /usr/local/..." + @sudo mkdir -p /usr/local/bin /usr/local/dist + @sudo cp bin/try /usr/local/bin/try @sudo chmod +x /usr/local/bin/try + @sudo cp dist/try.rb /usr/local/dist/try.rb + @sudo chmod +x /usr/local/dist/try.rb + @if [ -f dist/try ] && [ -x dist/try ]; then sudo cp dist/try /usr/local/dist/try && sudo chmod +x /usr/local/dist/try; fi @echo "Installed globally! Add to your shell:" @echo " eval \"\$$(try init ~/src/tries)\"" @@ -81,6 +115,7 @@ clean: ## Clean up temporary files @echo "Cleaning up..." @find . -name "*.tmp" -delete @find . -name "*~" -delete + @rm -rf dist @echo "Clean complete" .PHONY: check-deps @@ -117,7 +152,7 @@ l: lint ## Shortcut for lint .PHONY: i i: install ## Shortcut for install -# Native binary via Spinel (optional) +# Native binary via Spinel (optional) — compiles the concat, not the split sources SPINEL ?= spinel SPINEL_FLAGS ?= -O s CC ?= cc @@ -129,9 +164,9 @@ SPINEL_LIB ?= $(dir $(SPINEL_BIN))../lib .PHONY: native native-test native-compare native: $(NATIVE) -$(NATIVE_C): try.rb lib/tui.rb lib/fuzzy.rb +$(NATIVE_C): dist/try.rb mkdir -p dist - $(SPINEL) $(SPINEL_FLAGS) -c try.rb -o $(NATIVE_C) + $(SPINEL) $(SPINEL_FLAGS) -c dist/try.rb -o $(NATIVE_C) $(NATIVE): $(NATIVE_C) $(CC) -Os -Wno-all -ffunction-sections -fdata-sections \ @@ -144,4 +179,4 @@ native-test: $(NATIVE) bash spec/tests/runner.sh $(NATIVE) native-compare: $(NATIVE) - bash spec/tests/runner_and_compare.sh ./try.rb $(NATIVE) + bash spec/tests/runner_and_compare.sh dist/try.rb $(NATIVE) diff --git a/README.md b/README.md index 4eb0e66..b45b53d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Instantly navigate through all your experiment directories with: - **Fuzzy search** that just works - **Smart sorting** - recently used stuff bubbles to the top - **Auto-dating** - creates directories like `2025-08-17-redis-experiment` -- **Zero config** - just one Ruby file, no dependencies +- **Zero config** - no runtime gem dependencies ## Installation @@ -30,6 +30,8 @@ Instantly navigate through all your experiment directories with: gem install try-cli ``` +The gem ships `bin/try` plus a single generated `dist/try.rb` (`make dist` concatenates `lib/tui.rb`, `lib/fuzzy.rb`, and `try.rb`). `bin/try` loads that script, or execs `dist/try` when a Spinel native build is present. + Then add to your shell: ```bash @@ -42,32 +44,39 @@ try init | source ### Quick Start (Manual) -```bash -curl -sL https://raw.githubusercontent.com/tobi/try/refs/heads/main/try.rb > ~/.local/try.rb +Edit the split sources (`try.rb`, `lib/tui.rb`, `lib/fuzzy.rb`), then build the single-file script: -# Make "try" executable so it can be run directly -chmod +x ~/.local/try.rb +```bash +git clone https://github.com/tobi/try.git +cd try +make dist # writes dist/try.rb +# optional: make native # writes dist/try (Spinel) +# bin/try routes to dist/try when present, otherwise dist/try.rb +chmod +x bin/try # Add to your shell (bash/zsh) -echo 'eval "$(ruby ~/.local/try.rb init ~/src/tries)"' >> ~/.zshrc +echo 'eval "$(./bin/try init ~/src/tries)"' >> ~/.zshrc # for fish shell users -echo '~/.local/try.rb init ~/src/tries | source' >> ~/.config/fish/config.fish +echo './bin/try init ~/src/tries | source' >> ~/.config/fish/config.fish ``` +Or run `dist/try.rb` directly after `make dist`. + ### Native binary (optional) -Compile a native `try` with [Spinel](https://github.com/matz/spinel). Build Spinel from source; [PR 3906](https://github.com/matz/spinel/pull/3906) is required so `IO#tty?` / `#winsize` work on handles that are not statically typed IO. +Compile a native `try` with [Spinel](https://github.com/matz/spinel). `make native` compiles the concatenated `dist/try.rb` (one compilation unit), not the split sources. Build Spinel from source; [PR 3906](https://github.com/matz/spinel/pull/3906) is required so `IO#tty?` / `#winsize` work on handles that are not statically typed IO. ```bash -make native SPINEL=/path/to/spinel # -O s, then strip +make dist +make native SPINEL=/path/to/spinel # compiles dist/try.rb, then strip ./dist/try --help -eval "$(./dist/try init)" # wires the shell function to the binary, not MRI +eval "$(./bin/try init)" # bin/try execs dist/try when present make native-test SPINEL=/path/to/spinel ``` -MRI `ruby try.rb` and the gem keep working. `dist/try init` emits the binary path; `ruby try.rb init` still emits `/usr/bin/env ruby '…/try.rb'`. +MRI `ruby try.rb` (dev), `dist/try.rb`, and the gem keep working. `bin/try` picks `dist/try` when that executable exists, otherwise `dist/try.rb`. The published gem does not include a Linux ELF `dist/try`. ## The Problem @@ -263,10 +272,10 @@ After installation, add to your shell: ## Why Ruby? -- One file, no dependencies +- Small codebase, no runtime gem dependencies - Works on any system with Ruby (macOS has it built-in) - Fast enough for thousands of directories -- Easy to hack on +- Easy to hack on — edit `try.rb` / `lib/`, then `make dist` ## The Philosophy @@ -290,7 +299,7 @@ A: First, welcome to the club. Second, it handles it fine - the scoring algorith ## Contributing -It's one file. If you want to change something, just edit it. Send a PR if you think others would like it too. +Edit `try.rb`, `lib/tui.rb`, and `lib/fuzzy.rb` (those stay the source of truth). `make dist` concatenates them into `dist/try.rb`. Send a PR if you think others would like it too. ## License diff --git a/Rakefile b/Rakefile index 6568ffe..14a00a8 100644 --- a/Rakefile +++ b/Rakefile @@ -16,12 +16,20 @@ Rake::TestTask.new(:unit) do |t| t.pattern = 'test/**/*_test.rb' end +desc "Build concatenated dist/try.rb" +task :dist do + sh 'make', 'dist' +end + desc "Check syntax with MRI and Spinel (warns if Spinel is missing)" task :lint do RUBY_SOURCES.each do |file| sh 'ruby', '-c', file end + sh 'make', 'dist/try.rb' + sh 'ruby', '-c', 'dist/try.rb' + unless spinel_available? warn "warning: spinel not found (#{spinel_cmd}); skipping Spinel syntax check" warn "warning: try must parse and run on both MRI Ruby and Spinel" @@ -33,14 +41,24 @@ task :lint do sh spinel_cmd, '-c', file, '-o', tmp.path end end + + Tempfile.create(['try-spinel-dist', '.c']) do |tmp| + sh spinel_cmd, '-c', 'dist/try.rb', '-o', tmp.path + end end -desc "Run shell spec compliance tests (MRI)" +desc "Run shell spec compliance tests (MRI source)" task :spec do sh 'bash', 'spec/tests/runner.sh', './try.rb' end -desc "Emit dist/try.c, compile dist/try, spec it, and compare with MRI" +desc "Run shell spec compliance tests against dist/try.rb" +task :spec_dist do + sh 'make', 'dist' + sh 'bash', 'spec/tests/runner.sh', 'dist/try.rb' +end + +desc "Emit dist/try.c from concat, compile dist/try, spec it, and compare with MRI" task :spec_spinel do unless spinel_available? warn "warning: spinel not found (#{spinel_cmd}); skipping native spec + compare" @@ -49,10 +67,10 @@ task :spec_spinel do sh 'make', 'native', "SPINEL=#{spinel_cmd}" sh 'bash', 'spec/tests/runner.sh', 'dist/try' - sh 'bash', 'spec/tests/runner_and_compare.sh', './try.rb', 'dist/try' + sh 'bash', 'spec/tests/runner_and_compare.sh', 'dist/try.rb', 'dist/try' end -desc "Run all tests (lint + unit + spec; native spec+compare if Spinel is present)" -task test: [:lint, :unit, :spec, :spec_spinel] +desc "Run all tests (lint + unit + spec + dist spec; native spec+compare if Spinel is present)" +task test: [:lint, :unit, :spec, :spec_dist, :spec_spinel] task default: :test diff --git a/bin/try b/bin/try index 4e8ecb4..dbb589b 100755 --- a/bin/try +++ b/bin/try @@ -1,5 +1,9 @@ #!/usr/bin/env ruby -# frozen_string_literal: true - -$0 = File.expand_path('../try.rb', __dir__) -load $0 +root = File.expand_path('..', __dir__) +native = File.join(root, 'dist', 'try') +script = File.join(root, 'dist', 'try.rb') +if File.file?(native) && File.executable?(native) + exec native, *ARGV +end +abort "try: missing #{script} (run: make dist)" unless File.file?(script) +load script diff --git a/flake.nix b/flake.nix index b3ff505..8d1aec7 100644 --- a/flake.nix +++ b/flake.nix @@ -72,10 +72,11 @@ nativeBuildInputs = [ pkgs.makeBinaryWrapper ]; installPhase = '' - mkdir -p $out/bin - cp try.rb $out/bin/try - cp -r lib $out/bin/ - chmod +x $out/bin/try + make SHELL=bash dist + mkdir -p $out/bin $out/dist + cp bin/try $out/bin/try + cp dist/try.rb $out/dist/try.rb + chmod +x $out/bin/try $out/dist/try.rb wrapProgram $out/bin/try \ --prefix PATH : ${ruby}/bin diff --git a/try-cli.gemspec b/try-cli.gemspec index 3f743af..4d5580e 100644 --- a/try-cli.gemspec +++ b/try-cli.gemspec @@ -17,15 +17,21 @@ Gem::Specification.new do |spec| spec.metadata["documentation_uri"] = "https://pages.tobi.lutke.com/try/" spec.metadata["changelog_uri"] = "https://github.com/tobi/try/releases" - spec.files = Dir[ - "lib/**/*", - "bin/*", - "try.rb", - "VERSION", - "LICENSE*", - "README.md" - ] + # Generate the single-file script at build time. VERSION is read above from + # the repo; it is not shipped inside the installed gem. + Dir.chdir(__dir__) do + system("make", "dist/try.rb") or raise "failed to generate dist/try.rb (make dist)" + end + + files = ["bin/try", "dist/try.rb"] + files << "LICENSE" if File.file?(File.expand_path("LICENSE", __dir__)) + # Local `make native && gem build` may include the AOT binary. Release CI + # must not run `make native`, so the published gem stays portable Ruby. + native = File.expand_path("dist/try", __dir__) + files << "dist/try" if File.file?(native) && File.executable?(native) + + spec.files = files spec.bindir = "bin" spec.executables = ["try"] - spec.require_paths = ["lib", "."] + spec.require_paths = ["dist"] end