all_in_ruby 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +52 -0
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +1 -0
- data/README.md +95 -5
- data/exe/all-in-ruby +1 -0
- data/lib/all_in_ruby/cli.rb +99 -14
- data/lib/all_in_ruby/diff.rb +66 -0
- data/lib/all_in_ruby/installer.rb +61 -21
- data/lib/all_in_ruby/instruction.rb +9 -3
- data/lib/all_in_ruby/version.rb +3 -1
- data/lib/all_in_ruby.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6930c270ee263f4f0479be9854cf03b7af4db01e88e6ffa3ee585a2dad0f152b
|
|
4
|
+
data.tar.gz: f0491de4a159d7e327d6e6f270146ec5546213b82d6ed68dc5cbd1e3f1cd610a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d771a3ce9f1669f73d1bb441dfaaae80b180570ddcce09d6ea38f1c709fee3d9a8df778b6a8e72aa7b213591777066e51f06f5f016c892a1f3a4ab8b46993311
|
|
7
|
+
data.tar.gz: 66e7bffbd34e3f45f1bd8366e8cbd12f068f93de6f89e3ea6cf5424772e8cdd0224b360174d8b617cd4e09a48334fe1b123b6f84f051432b32d5a1179463de73
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`all_in_ruby` is a zero-dependency CLI gem that adds (or removes) a single "write throwaway scripts in Ruby" instruction block to AI-agent instruction files — `CLAUDE.md` for Claude Code, `AGENTS.md` for Codex/OpenCode/Cursor — either in the current repo (`local`) or in home-directory config (`global`). The core contract is: **additive and idempotent**. Existing file content is preserved verbatim; the block lives inside marker comments so it can be found and removed cleanly.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
The project uses `.ruby-version` (3.4.7) with **chruby**. Prefix Ruby commands with the activation, e.g.:
|
|
12
|
+
`source /opt/homebrew/share/chruby/chruby.sh && chruby ruby-3.4.7 && <cmd>`
|
|
13
|
+
|
|
14
|
+
- Install deps: `bundle install`
|
|
15
|
+
- Run all tests: `bundle exec rake test` (Minitest; `rake` default task is `test`)
|
|
16
|
+
- Run one test file: `bundle exec rake test TEST=test/installer_test.rb`
|
|
17
|
+
- Run one test by name: `bundle exec ruby -Itest test/installer_test.rb -n test_install_is_idempotent`
|
|
18
|
+
- Run the CLI from source (bypasses installed gem): `bundle exec exe/all-in-ruby install --dry-run`
|
|
19
|
+
- Build gem: `bundle exec rake build` — Release: `bundle exec rake release` (both from `bundler/gem_tasks`)
|
|
20
|
+
|
|
21
|
+
There is no linter configured. `--dry-run` (`-n`) is the safest way to exercise real behavior without touching disk.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
Four collaborating layers under `lib/all_in_ruby/`, loaded by `lib/all_in_ruby.rb`:
|
|
26
|
+
|
|
27
|
+
- **`CLI`** (`cli.rb`) — parses argv with `OptionParser`, resolves defaults, calls the installer, formats output. It owns two label maps, `STATUS_LABELS` and `DRY_RUN_LABELS`, that translate the installer's status *symbols* into user-facing strings. The CLI never touches the filesystem itself.
|
|
28
|
+
- **`Installer`** (`installer.rb`) — the orchestrator. `path_for(agent, scope)` is the single source of truth mapping an `(agent, scope)` pair to a file path (or `nil`). `install`/`uninstall`/`status` each iterate targets via `each_target` and return an array of `Result` structs `(agent, path, status, diff, backup)`.
|
|
29
|
+
- **`Instruction`** (`instruction.rb`) — owns the block text and the marker comments (`<!-- BEGIN all_in_ruby -->` / `<!-- END all_in_ruby -->`). `installed?` detects the block; `remove` strips it with a regex. Change the wording here in exactly one place.
|
|
30
|
+
- **`Diff`** (`diff.rb`) — pure-function unified-diff generator for dry-run previews. It assumes a single contiguous edit per file (that assumption is what makes the common-prefix/suffix approach valid), and handles the "no newline at end of file" case.
|
|
31
|
+
|
|
32
|
+
Data flows one direction: `CLI → Installer → (Instruction, Diff)`. `Result` is the boundary object between the installer and the CLI.
|
|
33
|
+
|
|
34
|
+
### Non-obvious behaviors to preserve
|
|
35
|
+
|
|
36
|
+
These are the invariants that most of the code exists to protect — read `installer.rb` and the shared-file tests before changing any of them:
|
|
37
|
+
|
|
38
|
+
- **Multiple agents share one file.** Locally, `codex`, `opencode`, and `cursor` all map to `AGENTS.md`. So installing several agents in one run produces e.g. `[:created, :already_installed, :already_installed]` — not three creates. Tests assert this exact sequence.
|
|
39
|
+
- **Dry run simulates writes in memory.** The `planned` hash (`installer.rb:82-97`) holds what each file *would* contain, so later same-file agents in a dry run report the same statuses (and the diff is emitted only once) a real run would. Don't "simplify" this away — it's load-bearing.
|
|
40
|
+
- **`Cursor` + `global` is `:manual`** — `path_for` returns `nil` because Cursor has no global rules file. The installer yields `:manual` and writes nothing; the CLI prints the instruction body to paste into Cursor Settings > Rules. This only happens for `install`, not `status`.
|
|
41
|
+
- **Backups before overwriting.** `install`/`uninstall` copy an existing file to a timestamped `<path>.YYYYMMDD-HHMMSS.bak` before writing — but never on `:created`, since a brand-new file has nothing to preserve. The clock is injected (`Installer.new(clock:)`) so tests can assert the exact name. `apply` returns `[status, diff, backup]` and the CLI lists the backups at the end of the run. `--no-backup` skips it; a dry run names the backup it *would* make without creating it.
|
|
42
|
+
- **`CODEX_HOME` overrides `~/.codex`** for the codex global path (empty string is ignored), matching Codex's own resolution.
|
|
43
|
+
- **Scope defaults differ by command** (`cli.rb:scopes_for`): `status` with no scope flag reports *both* local and global (it's read-only); `install`/`uninstall` stay `local` unless `--global` is given. Scope and agent flags both *combine* rather than last-wins.
|
|
44
|
+
- **Separator logic** (`separator_for`): when appending to an existing file, the installer inserts the right number of newlines so the block is separated by exactly one blank line, whether or not the file ended with a trailing newline.
|
|
45
|
+
|
|
46
|
+
## Testing conventions
|
|
47
|
+
|
|
48
|
+
Everything is dependency-injected for isolation — no mocking, no global state:
|
|
49
|
+
- `Installer.new(root:, home:, env:, clock:)` runs against `Dir.mktmpdir` temp roots; tests assert on real files. Inject `clock:` (a `-> { Time }` lambda) to make timestamped backup names deterministic.
|
|
50
|
+
- `CLI.new(stdout:, stderr:, installer:)` captures output in `StringIO` and receives an installer wired to temp dirs.
|
|
51
|
+
|
|
52
|
+
When adding a feature, cover both the installer level (file effects, status symbols) and the CLI level (exit code, printed labels, diffs). The shared-`AGENTS.md` and dry-run-simulation cases are the ones most likely to break — mirror the existing tests for them.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-08-03)
|
|
4
|
+
|
|
5
|
+
- `install` and `uninstall` now back up any existing file before modifying it, writing a timestamped copy (`CLAUDE.md.20260802-153000.bak`) next to the original and listing the backups created at the end of the run. New files are not backed up. Pass `--no-backup` to skip, or `--dry-run` to preview which backups would be made
|
|
6
|
+
- Add `--dry-run` (`-n`) to `install` and `uninstall`: prints a unified diff per file showing the exact lines that would be added or removed, without writing anything. Dry runs simulate writes in memory, so agents sharing `AGENTS.md` report the same statuses a real run would
|
|
7
|
+
- `status` with no scope flag now reports both local and global scopes (previously it silently checked only local)
|
|
8
|
+
- Add `--local` flag; scope flags combine, so `install --local --global` targets both scopes in one run
|
|
9
|
+
|
|
10
|
+
## 0.1.1 (2026-07-22)
|
|
11
|
+
|
|
12
|
+
- Support OpenCode: local `AGENTS.md` (shared with Codex) and global `~/.config/opencode/AGENTS.md`
|
|
13
|
+
- Support Cursor: local `AGENTS.md` (shared with Codex); `--global` prints the instruction to paste into Cursor Settings > Rules, since Cursor has no global rules file
|
|
14
|
+
- Agent flags now combine (`--claude --codex` targets both) instead of last-flag-wins
|
|
15
|
+
|
|
3
16
|
## 0.1.0 (2026-07-20)
|
|
4
17
|
|
|
5
18
|
- First release
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Tell your AI coding agents to write scripts in Ruby.
|
|
4
4
|
|
|
5
|
-
Adds a "write throwaway and utility scripts in Ruby" instruction to agent instruction files - `CLAUDE.md` for Claude Code, `AGENTS.md` for Codex. Additive and idempotent: your existing instructions are never modified.
|
|
5
|
+
Adds a "write throwaway and utility scripts in Ruby" instruction to agent instruction files - `CLAUDE.md` for Claude Code, `AGENTS.md` for Codex, OpenCode, and Cursor. Additive and idempotent: your existing instructions are never modified.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -24,29 +24,119 @@ Or add it globally, for every project:
|
|
|
24
24
|
all-in-ruby install --global
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
This writes to `~/.claude/CLAUDE.md
|
|
27
|
+
This writes to `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, and `~/.config/opencode/AGENTS.md`. If `CODEX_HOME` is set, it is used instead of `~/.codex`, matching Codex's own resolution.
|
|
28
|
+
|
|
29
|
+
Cursor has no global rules file — its global "User Rules" live only in the app settings. For `--global`, the instruction text is printed so you can paste it into Cursor Settings > Rules.
|
|
28
30
|
|
|
29
31
|
## Usage
|
|
30
32
|
|
|
31
|
-
Target
|
|
33
|
+
Target one or more agents:
|
|
32
34
|
|
|
33
35
|
```sh
|
|
34
36
|
all-in-ruby install --claude
|
|
35
|
-
all-in-ruby install --codex
|
|
37
|
+
all-in-ruby install --codex --opencode
|
|
38
|
+
all-in-ruby install --cursor
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
Codex, OpenCode, and Cursor all read `AGENTS.md` from the project root, so locally they share one file: installing for any of them covers all three, and uninstalling removes the block for all three.
|
|
42
|
+
|
|
43
|
+
Check where the instruction is installed. With no scope flag, `status` reports both the current repo and the global files:
|
|
39
44
|
|
|
40
45
|
```sh
|
|
41
46
|
all-in-ruby status
|
|
42
47
|
```
|
|
43
48
|
|
|
49
|
+
```text
|
|
50
|
+
local:
|
|
51
|
+
claude /path/to/repo/CLAUDE.md: installed
|
|
52
|
+
codex /path/to/repo/AGENTS.md: not installed
|
|
53
|
+
...
|
|
54
|
+
global:
|
|
55
|
+
claude ~/.claude/CLAUDE.md: not installed
|
|
56
|
+
codex ~/.codex/AGENTS.md: installed
|
|
57
|
+
...
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pass `--local` or `--global` to check a single scope.
|
|
61
|
+
|
|
44
62
|
Remove it:
|
|
45
63
|
|
|
46
64
|
```sh
|
|
47
65
|
all-in-ruby uninstall
|
|
48
66
|
```
|
|
49
67
|
|
|
68
|
+
### Backups
|
|
69
|
+
|
|
70
|
+
Before `install` or `uninstall` modifies a file that already exists, it writes a timestamped copy next to the original and lists the backups at the end of the run:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
claude /path/to/repo/CLAUDE.md: instructions added
|
|
74
|
+
|
|
75
|
+
Backups created:
|
|
76
|
+
/path/to/repo/CLAUDE.md.20260802-153000.bak
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Only existing files are backed up — creating a brand-new `CLAUDE.md` or `AGENTS.md` makes no backup, since there is nothing to preserve. Each run uses a fresh timestamp, so an earlier backup is never overwritten. Pass `--no-backup` to skip them, or `--dry-run` to see which backups would be made without writing anything.
|
|
80
|
+
|
|
81
|
+
### Dry run
|
|
82
|
+
|
|
83
|
+
Preview what `install` or `uninstall` would change without writing anything:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
all-in-ruby install --dry-run
|
|
87
|
+
all-in-ruby uninstall -n
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For each file, dry run prints the status a real run would produce, followed by a unified diff showing the exact location of the change:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
claude /path/to/repo/CLAUDE.md: instructions would be added
|
|
94
|
+
codex /path/to/repo/AGENTS.md: would be created
|
|
95
|
+
opencode /path/to/repo/AGENTS.md: would already be covered, no change
|
|
96
|
+
cursor /path/to/repo/AGENTS.md: would already be covered, no change
|
|
97
|
+
|
|
98
|
+
--- /path/to/repo/CLAUDE.md
|
|
99
|
+
+++ /path/to/repo/CLAUDE.md
|
|
100
|
+
@@ -1,3 +1,19 @@
|
|
101
|
+
# My project
|
|
102
|
+
|
|
103
|
+
Always use tabs.
|
|
104
|
+
+
|
|
105
|
+
+<!-- BEGIN all_in_ruby -->
|
|
106
|
+
+## Scripts
|
|
107
|
+
+
|
|
108
|
+
+Write throwaway and utility scripts (data munging, one-off migrations,
|
|
109
|
+
...
|
|
110
|
+
+<!-- END all_in_ruby -->
|
|
111
|
+
|
|
112
|
+
--- /dev/null
|
|
113
|
+
+++ /path/to/repo/AGENTS.md
|
|
114
|
+
@@ -0,0 +1,15 @@
|
|
115
|
+
+<!-- BEGIN all_in_ruby -->
|
|
116
|
+
+## Scripts
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A dry run simulates its own writes, so the statuses match what a real run would report - above, creating `AGENTS.md` for Codex also covers OpenCode and Cursor, so only one diff is shown for it. New files appear as `--- /dev/null`.
|
|
121
|
+
|
|
122
|
+
The diffs are standard unified format: pipe the output to `delta` or `bat -l diff` for coloring, or apply it with `patch -p0`.
|
|
123
|
+
|
|
124
|
+
### Options
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
--local Target the current repo (default for install/uninstall)
|
|
128
|
+
--global Target global files instead of the current repo
|
|
129
|
+
--claude Claude Code (CLAUDE.md)
|
|
130
|
+
--codex Codex (AGENTS.md)
|
|
131
|
+
--opencode OpenCode (AGENTS.md)
|
|
132
|
+
--cursor Cursor (AGENTS.md; no global file)
|
|
133
|
+
-n, --dry-run Preview changes as diffs without writing any file
|
|
134
|
+
--no-backup Skip backing up existing files before modifying them
|
|
135
|
+
-v, --version Print version
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
With no agent flags, all agents are targeted. Scope flags combine too: `install --local --global` installs in both places. Without a scope flag, `install` and `uninstall` stay local, while `status` reports both scopes.
|
|
139
|
+
|
|
50
140
|
## How It Works
|
|
51
141
|
|
|
52
142
|
The instruction is appended inside marker comments:
|
data/exe/all-in-ruby
CHANGED
data/lib/all_in_ruby/cli.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "optparse"
|
|
2
4
|
|
|
3
5
|
module AllInRuby
|
|
4
6
|
class CLI
|
|
5
|
-
COMMANDS = [
|
|
7
|
+
COMMANDS = %w[install uninstall status].freeze
|
|
6
8
|
|
|
7
9
|
STATUS_LABELS = {
|
|
8
10
|
created: "created",
|
|
@@ -10,9 +12,18 @@ module AllInRuby
|
|
|
10
12
|
already_installed: "already installed, left untouched",
|
|
11
13
|
removed: "instructions removed",
|
|
12
14
|
not_installed: "not installed",
|
|
13
|
-
installed: "installed"
|
|
15
|
+
installed: "installed",
|
|
16
|
+
manual: "no global rules file; use Cursor Settings > Rules"
|
|
14
17
|
}.freeze
|
|
15
18
|
|
|
19
|
+
DRY_RUN_LABELS = STATUS_LABELS.merge(
|
|
20
|
+
created: "would be created",
|
|
21
|
+
updated: "instructions would be added",
|
|
22
|
+
already_installed: "would already be covered, no change",
|
|
23
|
+
removed: "instructions would be removed",
|
|
24
|
+
not_installed: "not installed, nothing to remove"
|
|
25
|
+
).freeze
|
|
26
|
+
|
|
16
27
|
def initialize(stdout: $stdout, stderr: $stderr, installer: Installer.new)
|
|
17
28
|
@stdout = stdout
|
|
18
29
|
@stderr = stderr
|
|
@@ -20,7 +31,7 @@ module AllInRuby
|
|
|
20
31
|
end
|
|
21
32
|
|
|
22
33
|
def run(argv)
|
|
23
|
-
options = {agents:
|
|
34
|
+
options = { agents: [], scopes: [], dry_run: false, backup: true }
|
|
24
35
|
parser = build_parser(options)
|
|
25
36
|
parser.parse!(argv)
|
|
26
37
|
|
|
@@ -30,9 +41,38 @@ module AllInRuby
|
|
|
30
41
|
return 1
|
|
31
42
|
end
|
|
32
43
|
|
|
33
|
-
|
|
44
|
+
agents = options[:agents].empty? ? Installer::AGENTS.dup : options[:agents]
|
|
45
|
+
scopes = scopes_for(command, options[:scopes])
|
|
46
|
+
dry_run = options[:dry_run] && command != "status"
|
|
47
|
+
|
|
48
|
+
scope_results = scopes.map do |scope|
|
|
49
|
+
results =
|
|
50
|
+
if command == "status"
|
|
51
|
+
@installer.status(agents: agents, scope: scope)
|
|
52
|
+
else
|
|
53
|
+
@installer.public_send(command, agents: agents, scope: scope, dry_run: dry_run, backup: options[:backup])
|
|
54
|
+
end
|
|
55
|
+
[scope, results]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
print_statuses(scope_results, dry_run ? DRY_RUN_LABELS : STATUS_LABELS)
|
|
59
|
+
|
|
60
|
+
results = scope_results.flat_map { |_scope, scope_result| scope_result }
|
|
34
61
|
results.each do |result|
|
|
35
|
-
|
|
62
|
+
next unless result.diff
|
|
63
|
+
|
|
64
|
+
@stdout.puts
|
|
65
|
+
@stdout.puts result.diff
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
print_backups(results, dry_run)
|
|
69
|
+
|
|
70
|
+
if command == "install" && results.any? { |result| result.status == :manual }
|
|
71
|
+
@stdout.puts
|
|
72
|
+
@stdout.puts "Cursor has no global rules file. To apply the instruction globally,"
|
|
73
|
+
@stdout.puts "paste this into Cursor Settings > Rules:"
|
|
74
|
+
@stdout.puts
|
|
75
|
+
@stdout.puts Instruction.body
|
|
36
76
|
end
|
|
37
77
|
0
|
|
38
78
|
rescue OptionParser::ParseError => e
|
|
@@ -42,31 +82,76 @@ module AllInRuby
|
|
|
42
82
|
|
|
43
83
|
private
|
|
44
84
|
|
|
85
|
+
# status is read-only, so with no scope flag it reports both scopes;
|
|
86
|
+
# install/uninstall write files, so they stay local unless asked.
|
|
87
|
+
def scopes_for(command, requested)
|
|
88
|
+
return requested.uniq unless requested.empty?
|
|
89
|
+
|
|
90
|
+
command == "status" ? Installer::SCOPES.dup : ["local"]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def print_backups(results, dry_run)
|
|
94
|
+
backups = results.filter_map(&:backup)
|
|
95
|
+
return if backups.empty?
|
|
96
|
+
|
|
97
|
+
@stdout.puts
|
|
98
|
+
@stdout.puts(dry_run ? "Backups that would be created:" : "Backups created:")
|
|
99
|
+
backups.each { |backup| @stdout.puts " #{backup}" }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def print_statuses(scope_results, labels)
|
|
103
|
+
show_headers = scope_results.length > 1
|
|
104
|
+
indent = show_headers ? " " : ""
|
|
105
|
+
scope_results.each do |scope, results|
|
|
106
|
+
@stdout.puts "#{scope}:" if show_headers
|
|
107
|
+
results.each do |result|
|
|
108
|
+
location = result.path || "(none)"
|
|
109
|
+
@stdout.puts "#{indent}#{result.agent.ljust(8)} #{location}: #{labels.fetch(result.status)}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
45
114
|
def build_parser(options)
|
|
46
115
|
OptionParser.new do |opts|
|
|
47
116
|
opts.banner = <<~BANNER
|
|
48
117
|
Usage: all-in-ruby COMMAND [options]
|
|
49
118
|
|
|
50
119
|
Adds a "write scripts in Ruby" instruction to AI agent instruction
|
|
51
|
-
files
|
|
52
|
-
idempotent: existing content is never modified.
|
|
120
|
+
files: CLAUDE.md for Claude Code, AGENTS.md for Codex, OpenCode, and
|
|
121
|
+
Cursor. Additive and idempotent: existing content is never modified.
|
|
53
122
|
|
|
54
123
|
Commands:
|
|
55
124
|
install add the instruction block
|
|
56
125
|
uninstall remove the instruction block
|
|
57
|
-
status show where the block is installed
|
|
126
|
+
status show where the block is installed (both scopes unless one is given)
|
|
58
127
|
|
|
59
128
|
Options:
|
|
60
129
|
BANNER
|
|
61
130
|
|
|
62
|
-
opts.on("--
|
|
63
|
-
options[:
|
|
131
|
+
opts.on("--local", "Target the current repo (default for install/uninstall)") do
|
|
132
|
+
options[:scopes] << "local"
|
|
133
|
+
end
|
|
134
|
+
opts.on("--global",
|
|
135
|
+
"Target global files (~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, ~/.config/opencode/AGENTS.md)") do
|
|
136
|
+
options[:scopes] << "global"
|
|
137
|
+
end
|
|
138
|
+
opts.on("--claude", "Claude Code (CLAUDE.md)") do
|
|
139
|
+
options[:agents] << "claude"
|
|
140
|
+
end
|
|
141
|
+
opts.on("--codex", "Codex (AGENTS.md)") do
|
|
142
|
+
options[:agents] << "codex"
|
|
143
|
+
end
|
|
144
|
+
opts.on("--opencode", "OpenCode (AGENTS.md)") do
|
|
145
|
+
options[:agents] << "opencode"
|
|
146
|
+
end
|
|
147
|
+
opts.on("--cursor", "Cursor (AGENTS.md; no global file)") do
|
|
148
|
+
options[:agents] << "cursor"
|
|
64
149
|
end
|
|
65
|
-
opts.on("--
|
|
66
|
-
options[:
|
|
150
|
+
opts.on("-n", "--dry-run", "Preview changes as diffs without writing any file") do
|
|
151
|
+
options[:dry_run] = true
|
|
67
152
|
end
|
|
68
|
-
opts.on("--
|
|
69
|
-
options[:
|
|
153
|
+
opts.on("--no-backup", "Skip backing up existing files before modifying them") do
|
|
154
|
+
options[:backup] = false
|
|
70
155
|
end
|
|
71
156
|
opts.on("-v", "--version", "Print version") do
|
|
72
157
|
@stdout.puts VERSION
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllInRuby
|
|
4
|
+
# Builds unified diffs for dry-run previews. The installer only ever makes
|
|
5
|
+
# one contiguous edit per file (appending or removing a single block), so a
|
|
6
|
+
# common prefix/suffix comparison finds the exact changed region.
|
|
7
|
+
module Diff
|
|
8
|
+
CONTEXT_LINES = 3
|
|
9
|
+
NO_NEWLINE_MARKER = "\"
|
|
10
|
+
|
|
11
|
+
def self.unified(old_content, new_content, path)
|
|
12
|
+
old_lines = (old_content || "").lines
|
|
13
|
+
new_lines = new_content.lines
|
|
14
|
+
|
|
15
|
+
prefix = common_prefix_length(old_lines, new_lines)
|
|
16
|
+
suffix = common_suffix_length(old_lines, new_lines, prefix)
|
|
17
|
+
|
|
18
|
+
removed = old_lines[prefix...(old_lines.length - suffix)]
|
|
19
|
+
added = new_lines[prefix...(new_lines.length - suffix)]
|
|
20
|
+
lead = [prefix, CONTEXT_LINES].min
|
|
21
|
+
trail = [suffix, CONTEXT_LINES].min
|
|
22
|
+
|
|
23
|
+
output = []
|
|
24
|
+
output << "--- #{old_content ? path : File::NULL}"
|
|
25
|
+
output << "+++ #{path}"
|
|
26
|
+
output << hunk_header(prefix, lead, trail, removed.length, added.length)
|
|
27
|
+
old_lines[prefix - lead, lead].each { |line| append(output, " ", line) }
|
|
28
|
+
removed.each { |line| append(output, "-", line) }
|
|
29
|
+
added.each { |line| append(output, "+", line) }
|
|
30
|
+
old_lines[old_lines.length - suffix, trail].each { |line| append(output, " ", line) }
|
|
31
|
+
"#{output.join("\n")}\n"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.common_prefix_length(old_lines, new_lines)
|
|
35
|
+
limit = [old_lines.length, new_lines.length].min
|
|
36
|
+
count = 0
|
|
37
|
+
count += 1 while count < limit && old_lines[count] == new_lines[count]
|
|
38
|
+
count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.common_suffix_length(old_lines, new_lines, prefix)
|
|
42
|
+
limit = [old_lines.length, new_lines.length].min - prefix
|
|
43
|
+
count = 0
|
|
44
|
+
count += 1 while count < limit && old_lines[-1 - count] == new_lines[-1 - count]
|
|
45
|
+
count
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.hunk_header(prefix, lead, trail, removed_count, added_count)
|
|
49
|
+
old_range = range(prefix - lead, lead + removed_count + trail)
|
|
50
|
+
new_range = range(prefix - lead, lead + added_count + trail)
|
|
51
|
+
"@@ -#{old_range} +#{new_range} @@"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.range(start_index, count)
|
|
55
|
+
count.zero? ? "#{start_index},0" : "#{start_index + 1},#{count}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.append(output, sign, line)
|
|
59
|
+
output << "#{sign}#{line.chomp}"
|
|
60
|
+
output << NO_NEWLINE_MARKER unless line.end_with?("\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method :common_prefix_length, :common_suffix_length,
|
|
64
|
+
:hunk_header, :range, :append
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,54 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "fileutils"
|
|
2
4
|
|
|
3
5
|
module AllInRuby
|
|
4
6
|
class Installer
|
|
5
|
-
AGENTS = [
|
|
6
|
-
SCOPES = [
|
|
7
|
+
AGENTS = %w[claude codex opencode cursor].freeze
|
|
8
|
+
SCOPES = %w[local global].freeze
|
|
7
9
|
|
|
8
|
-
Result = Struct.new(:agent, :path, :status)
|
|
10
|
+
Result = Struct.new(:agent, :path, :status, :diff, :backup)
|
|
9
11
|
|
|
10
|
-
def initialize(root: Dir.pwd, home: Dir.home, env: ENV)
|
|
12
|
+
def initialize(root: Dir.pwd, home: Dir.home, env: ENV, clock: -> { Time.now })
|
|
11
13
|
@root = root
|
|
12
14
|
@home = home
|
|
13
15
|
@env = env
|
|
16
|
+
@clock = clock
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def path_for(agent, scope)
|
|
17
20
|
case [agent, scope]
|
|
18
|
-
when [
|
|
19
|
-
when [
|
|
20
|
-
when [
|
|
21
|
-
when [
|
|
21
|
+
when %w[claude local] then File.join(@root, "CLAUDE.md")
|
|
22
|
+
when %w[claude global] then File.join(@home, ".claude", "CLAUDE.md")
|
|
23
|
+
when %w[codex local] then File.join(@root, "AGENTS.md")
|
|
24
|
+
when %w[codex global] then File.join(codex_home, "AGENTS.md")
|
|
25
|
+
when %w[opencode local] then File.join(@root, "AGENTS.md")
|
|
26
|
+
when %w[opencode global] then File.join(@home, ".config", "opencode", "AGENTS.md")
|
|
27
|
+
when %w[cursor local] then File.join(@root, "AGENTS.md")
|
|
28
|
+
# Cursor has no documented global rules file; User Rules live in the app settings
|
|
29
|
+
when %w[cursor global] then nil
|
|
22
30
|
else
|
|
23
31
|
raise ArgumentError, "unknown agent or scope: #{agent}, #{scope}"
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
def install(agents:, scope:)
|
|
35
|
+
def install(agents:, scope:, dry_run: false, backup: true)
|
|
36
|
+
planned = {}
|
|
28
37
|
each_target(agents, scope) do |path|
|
|
29
|
-
content =
|
|
38
|
+
content = current_content(path, planned)
|
|
30
39
|
|
|
31
40
|
if content && Instruction.installed?(content)
|
|
32
41
|
:already_installed
|
|
33
42
|
elsif content
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
:updated
|
|
43
|
+
new_content = content + separator_for(content) + Instruction.block
|
|
44
|
+
apply(path, content, new_content, :updated, dry_run, backup, planned)
|
|
37
45
|
else
|
|
38
|
-
|
|
39
|
-
File.write(path, Instruction.block)
|
|
40
|
-
:created
|
|
46
|
+
apply(path, nil, Instruction.block, :created, dry_run, backup, planned)
|
|
41
47
|
end
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
|
|
45
|
-
def uninstall(agents:, scope:)
|
|
51
|
+
def uninstall(agents:, scope:, dry_run: false, backup: true)
|
|
52
|
+
planned = {}
|
|
46
53
|
each_target(agents, scope) do |path|
|
|
47
|
-
content =
|
|
54
|
+
content = current_content(path, planned)
|
|
48
55
|
|
|
49
56
|
if content && Instruction.installed?(content)
|
|
50
|
-
|
|
51
|
-
:removed
|
|
57
|
+
apply(path, content, Instruction.remove(content), :removed, dry_run, backup, planned)
|
|
52
58
|
else
|
|
53
59
|
:not_installed
|
|
54
60
|
end
|
|
@@ -70,10 +76,44 @@ module AllInRuby
|
|
|
70
76
|
codex_home && !codex_home.empty? ? codex_home : File.join(@home, ".codex")
|
|
71
77
|
end
|
|
72
78
|
|
|
79
|
+
def separator_for(content)
|
|
80
|
+
return "" if content.empty? || content.end_with?("\n\n")
|
|
81
|
+
|
|
82
|
+
content.end_with?("\n") ? "\n" : "\n\n"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Planned holds the content a dry run would have written, so later agents
|
|
86
|
+
# sharing the same file (codex/opencode/cursor all use AGENTS.md locally)
|
|
87
|
+
# report the same statuses a real run would produce.
|
|
88
|
+
def current_content(path, planned)
|
|
89
|
+
planned.fetch(path) { File.exist?(path) ? File.read(path) : nil }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns [status, diff, backup]. A backup is made only when an existing
|
|
93
|
+
# file is modified (never on create); a dry run computes the name it would
|
|
94
|
+
# use so the preview can report it, but writes nothing.
|
|
95
|
+
def apply(path, old_content, new_content, status, dry_run, backup, planned)
|
|
96
|
+
backup_path = backup && old_content ? backup_path_for(path) : nil
|
|
97
|
+
if dry_run
|
|
98
|
+
planned[path] = new_content
|
|
99
|
+
[status, Diff.unified(old_content, new_content, path), backup_path]
|
|
100
|
+
else
|
|
101
|
+
FileUtils.cp(path, backup_path) if backup_path
|
|
102
|
+
FileUtils.mkdir_p(File.dirname(path)) unless old_content
|
|
103
|
+
File.write(path, new_content)
|
|
104
|
+
[status, nil, backup_path]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def backup_path_for(path)
|
|
109
|
+
"#{path}.#{@clock.call.strftime("%Y%m%d-%H%M%S")}.bak"
|
|
110
|
+
end
|
|
111
|
+
|
|
73
112
|
def each_target(agents, scope)
|
|
74
113
|
agents.map do |agent|
|
|
75
114
|
path = path_for(agent, scope)
|
|
76
|
-
|
|
115
|
+
status, diff, backup = path ? yield(path) : :manual
|
|
116
|
+
Result.new(agent, path, status, diff, backup)
|
|
77
117
|
end
|
|
78
118
|
end
|
|
79
119
|
end
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AllInRuby
|
|
2
4
|
module Instruction
|
|
3
|
-
BEGIN_MARKER = "<!-- BEGIN all_in_ruby -->"
|
|
4
|
-
END_MARKER = "<!-- END all_in_ruby -->"
|
|
5
|
+
BEGIN_MARKER = "<!-- BEGIN all_in_ruby -->"
|
|
6
|
+
END_MARKER = "<!-- END all_in_ruby -->"
|
|
5
7
|
|
|
6
|
-
BODY = <<~MARKDOWN
|
|
8
|
+
BODY = <<~MARKDOWN
|
|
7
9
|
## Scripts
|
|
8
10
|
|
|
9
11
|
Write throwaway and utility scripts (data munging, one-off migrations,
|
|
@@ -25,6 +27,10 @@ module AllInRuby
|
|
|
25
27
|
BLOCK
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
def self.body
|
|
31
|
+
BODY
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
def self.installed?(content)
|
|
29
35
|
content.include?(BEGIN_MARKER)
|
|
30
36
|
end
|
data/lib/all_in_ruby/version.rb
CHANGED
data/lib/all_in_ruby.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: all_in_ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -18,12 +18,15 @@ executables:
|
|
|
18
18
|
extensions: []
|
|
19
19
|
extra_rdoc_files: []
|
|
20
20
|
files:
|
|
21
|
+
- AGENTS.md
|
|
21
22
|
- CHANGELOG.md
|
|
23
|
+
- CLAUDE.md
|
|
22
24
|
- LICENSE.txt
|
|
23
25
|
- README.md
|
|
24
26
|
- exe/all-in-ruby
|
|
25
27
|
- lib/all_in_ruby.rb
|
|
26
28
|
- lib/all_in_ruby/cli.rb
|
|
29
|
+
- lib/all_in_ruby/diff.rb
|
|
27
30
|
- lib/all_in_ruby/installer.rb
|
|
28
31
|
- lib/all_in_ruby/instruction.rb
|
|
29
32
|
- lib/all_in_ruby/version.rb
|