ruby_workspace_manager 0.5.0 → 0.6.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.
data/README.md CHANGED
@@ -1,35 +1,747 @@
1
1
  [![CI](https://github.com/sidbhatt11/ruby-workspace-manager/actions/workflows/ci.yml/badge.svg)](https://github.com/sidbhatt11/ruby-workspace-manager/actions/workflows/ci.yml)
2
+ [![Gem Version](https://img.shields.io/gem/v/ruby_workspace_manager)](https://rubygems.org/gems/ruby_workspace_manager)
2
3
  ![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.4-red)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
3
5
 
4
6
  # RWM — Ruby Workspace Manager
5
7
 
6
- An Nx-like monorepo tool for Ruby. Convention-over-configuration, zero runtime dependencies, delegates to Rake.
8
+ A monorepo tool for Ruby, inspired by [Nx](https://nx.dev). Convention-over-configuration, zero runtime dependencies, delegates to Rake.
7
9
 
8
- ## What it does
10
+ RWM discovers packages in your repository, builds a dependency graph from Gemfiles, runs tasks in parallel respecting dependency order, detects which packages are affected by a change, and caches results so unchanged work is never repeated.
9
11
 
10
- RWM manages Ruby monorepos with multiple apps and libraries. It builds a dependency graph from your Gemfiles, enforces structural conventions, runs tasks in parallel respecting dependency order, detects affected packages from git changes, and caches results so unchanged work is never repeated.
12
+ ## Table of Contents
11
13
 
12
- ## Commands
14
+ - [Getting started](#getting-started)
15
+ - [Core concepts](#core-concepts)
16
+ - [Workspace layout](#workspace-layout)
17
+ - [Managing packages](#managing-packages)
18
+ - [Dependencies between packages](#dependencies-between-packages)
19
+ - [The dependency graph](#the-dependency-graph)
20
+ - [Running tasks](#running-tasks)
21
+ - [Task caching](#task-caching)
22
+ - [Affected detection](#affected-detection)
23
+ - [Bootstrap and daily workflow](#bootstrap-and-daily-workflow)
24
+ - [Git hooks](#git-hooks)
25
+ - [Convention enforcement](#convention-enforcement)
26
+ - [Rails and Zeitwerk](#rails-and-zeitwerk)
27
+ - [VSCode integration](#vscode-integration)
28
+ - [Shell completions](#shell-completions)
29
+ - [Command reference](#command-reference)
30
+ - [Design philosophy](#design-philosophy)
31
+ - [Resources](#resources)
32
+
33
+ ---
34
+
35
+ ## Getting started
36
+
37
+ ### New workspace
38
+
39
+ ```sh
40
+ gem install ruby_workspace_manager
41
+
42
+ mkdir my-project && cd my-project
43
+ git init
44
+ rwm init
45
+ ```
46
+
47
+ `rwm init` creates the full workspace structure — `libs/`, `apps/`, a root Gemfile (with `rake` and `ruby_workspace_manager`), a root Rakefile, and adds `.rwm/` to `.gitignore`. It then runs `rwm bootstrap` automatically. The command is idempotent.
48
+
49
+ ### Existing project
50
+
51
+ If you already have a git repo with a Gemfile, add RWM to it and initialize:
52
+
53
+ ```sh
54
+ bundle add ruby_workspace_manager
55
+ rwm init
56
+ ```
57
+
58
+ `rwm init` won't overwrite your existing Gemfile or Rakefile — it only creates files that are missing.
59
+
60
+ ### Creating packages
61
+
62
+ ```sh
63
+ rwm new lib auth
64
+ rwm new lib billing
65
+ rwm new app api
66
+ ```
67
+
68
+ Each command scaffolds a complete gem structure: Gemfile, gemspec, Rakefile, module stub, and test helper (unless `--test=none`).
69
+
70
+ ### Declaring dependencies
71
+
72
+ Edit the consuming package's Gemfile:
73
+
74
+ ```ruby
75
+ # apps/api/Gemfile
76
+ require "rwm/gemfile"
77
+
78
+ rwm_lib "auth"
79
+ rwm_lib "billing"
80
+ ```
81
+
82
+ Then bootstrap to install deps and rebuild the graph:
83
+
84
+ ```sh
85
+ rwm bootstrap
86
+ ```
87
+
88
+ ### Running tasks
89
+
90
+ ```sh
91
+ rwm spec # runs `rake spec` in every package that defines it
92
+ rwm lint # any unrecognized command is a task shortcut
93
+ rwm lint auth # run a task in a single package
94
+ ```
95
+
96
+ Tasks run in parallel, respecting dependency order. Packages that don't define the requested task are silently skipped.
97
+
98
+ ```
99
+ $ rwm spec
100
+ Running `rake spec` across 4 package(s)...
101
+
102
+ [auth] 12 examples, 0 failures
103
+ [billing] 8 examples, 0 failures
104
+ [notifications] 5 examples, 0 failures
105
+ [api] 21 examples, 0 failures
106
+
107
+ 4 package(s): 4 passed.
108
+ ```
109
+
110
+ ## Core concepts
111
+
112
+ **Workspace** — A git repository containing multiple packages. The git root is the workspace root. No configuration file is needed; RWM finds the root via `git rev-parse --show-toplevel`.
113
+
114
+ **Package** — A directory inside `libs/` or `apps/` that contains a `Gemfile`. Each package is a self-contained Ruby gem with its own Gemfile, gemspec, Rakefile, and source code.
115
+
116
+ **Libraries** (`libs/`) — Shared code. Libraries can depend on other libraries but never on applications.
117
+
118
+ **Applications** (`apps/`) — Deployable units. Applications can depend on libraries but never on other applications.
119
+
120
+ **Dependency graph** — A directed acyclic graph (DAG) built by parsing each package's Gemfile for `path:` dependencies. This graph drives task ordering, affected detection, and convention checks. It is cached at `.rwm/graph.json` and auto-rebuilt when any Gemfile changes.
121
+
122
+ ## Workspace layout
123
+
124
+ ```
125
+ my-project/ # git root = workspace root
126
+ ├── libs/
127
+ │ ├── auth/ # a library package
128
+ │ │ ├── Gemfile
129
+ │ │ ├── auth.gemspec
130
+ │ │ ├── Rakefile
131
+ │ │ └── lib/auth.rb
132
+ │ └── billing/
133
+ │ └── ...
134
+ ├── apps/
135
+ │ ├── api/ # an application package
136
+ │ │ ├── Gemfile
137
+ │ │ ├── api.gemspec
138
+ │ │ ├── Rakefile
139
+ │ │ └── app/api.rb
140
+ │ └── web/
141
+ │ └── ...
142
+ ├── Gemfile # root Gemfile (rake, ruby_workspace_manager)
143
+ ├── Rakefile # root Rakefile (bootstrap task, etc.)
144
+ └── .rwm/ # generated state (gitignored)
145
+ ├── graph.json # serialized dependency graph
146
+ └── cache/ # task cache hashes
147
+ ```
148
+
149
+ A directory is recognized as a package if it lives directly inside `libs/` or `apps/` and contains a `Gemfile`. Nested directories or directories without a Gemfile are ignored.
150
+
151
+ The `.rwm/` directory is created automatically and gitignored by `rwm init`. It stores the dependency graph cache and task cache state.
152
+
153
+ ## Managing packages
154
+
155
+ ### Scaffolding
156
+
157
+ ```sh
158
+ rwm new lib <name>
159
+ rwm new app <name>
160
+ rwm new lib <name> --test=minitest
161
+ rwm new app <name> --test=none
162
+ ```
163
+
164
+ Package names must match `/\A[a-z][a-z0-9_]*\z/` (lowercase, letters/digits/underscores, starts with a letter).
165
+
166
+ The `--test` flag controls which test framework is scaffolded. Values: `rspec` (default), `minitest`, `none`.
167
+
168
+ The scaffold includes:
169
+
170
+ - **Gemfile** — Sources rubygems.org, loads the gemspec, includes development dependencies (`rake`, the chosen test gem, `ruby_workspace_manager`), and requires `rwm/gemfile` for the `rwm_lib` helper.
171
+ - **Gemspec** — Minimal spec. Libraries use `require_paths = ["lib"]` and declare `spec.files`; applications use `require_paths = ["app"]` and omit `spec.files`.
172
+ - **Rakefile** — A `cacheable_task` for the test framework (`:spec` for rspec, `:test` for minitest) plus an empty `:bootstrap` task. With `--test=none`, only the bootstrap task is generated.
173
+ - **Source file** — `lib/<name>.rb` for libraries, `app/<name>.rb` for applications. Module stub.
174
+ - **Test helper** — `spec/spec_helper.rb` for rspec, `test/test_helper.rb` for minitest. Omitted with `--test=none`.
175
+
176
+ ### Inspecting and listing
177
+
178
+ ```sh
179
+ rwm info auth # type, path, dependencies, direct/transitive dependents
180
+ rwm list # formatted table of all packages
181
+ ```
182
+
183
+ ## Dependencies between packages
184
+
185
+ ### How dependency detection works
186
+
187
+ RWM reads each package's Gemfile using Bundler's DSL parser and extracts gems declared with a `path:` option pointing into the workspace. It does not scan source code for `require` statements. This means Bundler's Gemfile is the single source of truth for both runtime resolution and RWM's dependency graph.
188
+
189
+ ### The `rwm_lib` helper
190
+
191
+ Scaffolded packages include `require "rwm/gemfile"` in their Gemfile, which adds the `rwm_lib` method to Bundler's DSL:
192
+
193
+ ```ruby
194
+ # libs/billing/Gemfile
195
+ require "rwm/gemfile"
196
+
197
+ rwm_lib "auth"
198
+ ```
199
+
200
+ This expands to:
201
+
202
+ ```ruby
203
+ gem "auth", path: "/absolute/path/to/libs/auth"
204
+ ```
205
+
206
+ The workspace root is resolved via `git rev-parse --show-toplevel`, so it works regardless of where you run the command. You can pass any extra options that `gem` accepts:
207
+
208
+ ```ruby
209
+ rwm_lib "auth", require: false
210
+ ```
211
+
212
+ There is no `rwm_app` helper. Applications are leaf nodes — nothing should depend on them.
213
+
214
+ You can also use raw `gem ... path:` syntax directly. Both work identically for dependency detection.
215
+
216
+ ### Transitive resolution
217
+
218
+ When you call `rwm_lib "auth"`, RWM automatically resolves auth's own workspace dependencies. If auth's Gemfile declares `rwm_lib "core"`, then `core` is added to your bundle automatically.
219
+
220
+ This works recursively. Diamond dependencies and cycles are handled safely (each lib is resolved at most once).
221
+
222
+ ```ruby
223
+ # apps/web/Gemfile — only the direct dep is needed
224
+ require "rwm/gemfile"
225
+
226
+ rwm_lib "auth" # core (auth's dep) is added automatically
227
+ ```
228
+
229
+ Transitive resolution uses `Bundler::Dsl.eval_gemfile` — the same mechanism Bundler uses internally. Options passed to the direct `rwm_lib` call (like `group:` or `require:`) are not forwarded to transitive deps.
230
+
231
+ ## The dependency graph
232
+
233
+ ### Building
234
+
235
+ ```sh
236
+ rwm graph
237
+ ```
238
+
239
+ Parses every package's Gemfile, constructs a DAG using Ruby's `TSort` module (Tarjan's algorithm), writes it to `.rwm/graph.json`, and prints a summary.
240
+
241
+ ### Caching and staleness
242
+
243
+ Most commands (`run`, `list`, `check`, `affected`, `info`) load the graph from `.rwm/graph.json` rather than re-parsing Gemfiles. If any package's Gemfile has a modification time newer than the cache file, the graph is silently rebuilt. You rarely need to run `rwm graph` manually.
244
+
245
+ Concurrent `rwm` processes are safe — graph reads use shared file locks and writes use exclusive file locks.
246
+
247
+ ### Visualization
248
+
249
+ ```sh
250
+ rwm graph --dot # Graphviz DOT format
251
+ rwm graph --mermaid # Mermaid flowchart format
252
+ ```
253
+
254
+ Pipe DOT output to Graphviz to render an image:
255
+
256
+ ```sh
257
+ rwm graph --dot | dot -Tpng -o graph.png
258
+ ```
259
+
260
+ Or paste Mermaid output into any Mermaid-compatible renderer (GitHub markdown, Mermaid Live Editor, etc.).
261
+
262
+ ## Running tasks
263
+
264
+ ### Basic usage
265
+
266
+ ```sh
267
+ rwm run <task> # run in all packages
268
+ rwm run <task> <package> # run in one package
269
+ rwm spec # shortcut for `rwm run spec`
270
+ rwm lint auth # shortcut for `rwm run lint auth`
271
+ ```
272
+
273
+ Any command that isn't a built-in subcommand is treated as a task name and forwarded to `rwm run`.
274
+
275
+ RWM runs `bundle exec rake <task>` in each package directory that has a Rakefile. Packages that don't define the requested task are automatically detected and silently skipped.
276
+
277
+ ### Parallel execution
278
+
279
+ RWM uses a DAG scheduler with a thread pool. Each package starts executing the instant all of its dependencies have completed. If A and B are independent, they run simultaneously. If C depends on A, C starts as soon as A finishes — it does not wait for B.
280
+
281
+ The default concurrency is `Etc.nprocessors` (number of CPU cores). Override with:
282
+
283
+ ```sh
284
+ rwm run spec --concurrency 4
285
+ ```
286
+
287
+ ### Output modes
288
+
289
+ **Streaming (default)** — Output is printed as it happens, prefixed with the package name:
290
+
291
+ ```
292
+ [auth] 5 examples, 0 failures
293
+ [billing] 3 examples, 0 failures
294
+ ```
295
+
296
+ **Buffered** — Each package's output is collected and printed as a complete block when it finishes. Failed packages have their output sent to stderr:
297
+
298
+ ```sh
299
+ rwm run spec --buffered
300
+ ```
301
+
302
+ ### Failure handling
303
+
304
+ When a package fails, its transitive dependents are immediately skipped. Unrelated packages continue running. The exit code is 0 if all packages pass, 1 if any fail.
305
+
306
+ ## Task caching
307
+
308
+ ### Why caching matters
309
+
310
+ In a monorepo with many packages, most runs touch only a few. Without caching, `rwm spec` re-runs everything even if nothing changed. Task caching skips packages whose inputs are unchanged.
311
+
312
+ ### Content-hash caching
313
+
314
+ RWM's cache is inspired by [DJB's redo](https://cr.yp.to/redo.html). The core insight: **use content hashes, not timestamps, to decide what needs rebuilding.** Timestamps are fragile — `git checkout` changes them, rebasing rewrites them. Content hashes are deterministic: if the bytes haven't changed, the result is still valid.
315
+
316
+ For each (package, task) pair, RWM:
317
+
318
+ 1. **Computes a content hash** — SHA256 of all source files in the package (sorted by path), plus the content hashes of all dependency packages.
319
+ 2. **Compares with stored hash** — If the hash matches the last successful run and declared outputs exist, the task is skipped.
320
+ 3. **Stores on success** — After a successful run, the hash is saved to `.rwm/cache/<package>-<task>`.
321
+
322
+ Source files are discovered via `git ls-files` (tracked + untracked-but-not-ignored), so anything in `.gitignore` is excluded from the hash.
323
+
324
+ ### Transitive invalidation
325
+
326
+ A package's content hash includes the content hashes of its dependencies, recursively:
327
+
328
+ ```
329
+ hash(auth) = SHA256(auth's files)
330
+ hash(billing) = SHA256(billing's files + hash(auth))
331
+ hash(api) = SHA256(api's files + hash(billing) + hash(auth))
332
+ ```
333
+
334
+ Change a single file in `auth` and the hashes of `billing` and `api` change automatically. No explicit invalidation logic needed.
335
+
336
+ ### Where the cache is coarser than redo
337
+
338
+ True redo tracks exactly which files a build step read during execution. RWM hashes every git-tracked file in the package directory. This means editing a README invalidates the spec cache even though RSpec never reads it.
339
+
340
+ This is a deliberate tradeoff. File-level read tracking would require filesystem interception (`strace`, `dtrace`, FUSE), which contradicts the zero-dependency philosophy. Package-level hashing may give false invalidations (unnecessary re-runs) but never false cache hits (skipping when it shouldn't).
341
+
342
+ ### Declaring cacheable tasks
343
+
344
+ Tasks are only cached if declared with `cacheable_task` in the Rakefile:
345
+
346
+ ```ruby
347
+ # libs/auth/Rakefile
348
+ require "rwm/rake"
349
+
350
+ cacheable_task :spec do
351
+ sh "bundle exec rspec"
352
+ end
353
+
354
+ cacheable_task :build, output: "pkg/*.gem" do
355
+ sh "gem build *.gemspec"
356
+ end
357
+ ```
358
+
359
+ `cacheable_task` creates a normal Rake task — it works like `task` when run directly. The caching metadata is only used when RWM orchestrates the run.
360
+
361
+ The optional `output:` parameter declares a glob for expected output files. If declared outputs don't exist, the cache is invalid even if the input hash matches.
362
+
363
+ Tasks declared with plain `task` always run unconditionally.
364
+
365
+ ### Bypassing the cache
366
+
367
+ ```sh
368
+ rwm run spec --no-cache
369
+ ```
370
+
371
+ ### Sharing the cache
372
+
373
+ The `.rwm/` directory is gitignored by design — committing it would create constant merge conflicts as the cache and graph change with every task run. Instead, treat your main branch CI as the single source of truth and distribute the cache from there.
374
+
375
+ Cache entries are content hashes with no absolute paths or machine-specific data. They're fully portable across machines. Restoring a stale cache is always safe — stale entries won't match and the task simply re-runs.
376
+
377
+ **The pattern:**
378
+
379
+ 1. Main branch CI runs the full test suite, producing a complete `.rwm/` cache.
380
+ 2. Feature branch CI restores main's cache, then runs only `--affected` packages.
381
+ 3. Developer machines download the cache during `rwm bootstrap`, so new branches start pre-warmed.
382
+
383
+ The result: feature branch CI and local development only run what actually changed.
384
+
385
+ #### GitHub Actions
386
+
387
+ ```yaml
388
+ name: CI
389
+
390
+ on:
391
+ push:
392
+ branches: [main]
393
+ pull_request:
394
+
395
+ jobs:
396
+ test:
397
+ runs-on: ubuntu-latest
398
+ steps:
399
+ - uses: actions/checkout@v4
400
+
401
+ - name: Fetch base branch for affected detection
402
+ if: github.ref != 'refs/heads/main'
403
+ run: git fetch origin main --depth=1
404
+
405
+ - name: Set up Ruby
406
+ uses: ruby/setup-ruby@v1
407
+ with:
408
+ ruby-version: "3.4"
409
+ bundler-cache: true
410
+
411
+ - name: Restore RWM cache
412
+ uses: actions/cache@v4
413
+ with:
414
+ path: .rwm
415
+ key: rwm-${{ runner.os }}-${{ github.sha }}
416
+ restore-keys: rwm-${{ runner.os }}-
417
+
418
+ - name: Bootstrap
419
+ run: bundle exec rwm bootstrap
420
+
421
+ - name: Run specs
422
+ run: |
423
+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
424
+ bundle exec rwm run spec
425
+ else
426
+ bundle exec rwm run spec --affected
427
+ fi
428
+
429
+ # Make cache available for local dev bootstrap
430
+ - name: Upload RWM cache
431
+ if: github.ref == 'refs/heads/main'
432
+ uses: actions/upload-artifact@v4
433
+ with:
434
+ name: rwm-cache
435
+ path: .rwm/
436
+ retention-days: 30
437
+ ```
438
+
439
+ Key points:
440
+
441
+ - **Fetch base branch** — Affected detection runs `git diff main...HEAD`, which needs the base branch ref. A shallow fetch of `main` is enough — no need for `fetch-depth: 0` or a full clone.
442
+ - **`actions/cache`** — Caches created on the default branch are accessible to all feature branches. The `restore-keys` prefix picks up the most recent main cache automatically.
443
+ - **Main runs everything**, populating a complete cache. Feature branches run only `--affected` and skip anything already cached from main.
444
+ - **`upload-artifact`** on main makes the cache downloadable for local dev bootstrap (see below).
445
+
446
+ #### Local developer cache (optional)
447
+
448
+ Add a cache download step to your root Rakefile so `rwm bootstrap` warms the local cache automatically:
449
+
450
+ ```ruby
451
+ # Rakefile
452
+ task :bootstrap do
453
+ restore_rwm_cache
454
+ end
455
+
456
+ def restore_rwm_cache
457
+ return if File.directory?(".rwm/cache")
458
+ return unless system("which gh > /dev/null 2>&1")
459
+
460
+ puts "Downloading RWM cache from CI..."
461
+ run_id = `gh run list --branch main --status success --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId'`.strip
462
+ if run_id.empty?
463
+ puts "No CI cache found. Skipping."
464
+ return
465
+ end
466
+
467
+ system("gh", "run", "download", run_id, "--name", "rwm-cache", "--dir", ".rwm")
468
+ puts File.directory?(".rwm/cache") ? "Cache restored." : "Cache download failed. Continuing without cache."
469
+ end
470
+ ```
471
+
472
+ The example above uses the [GitHub CLI](https://cli.github.com/) (`gh`) to download artifacts — your setup may look different depending on your CI provider or storage backend (S3, GCS, etc.). The idea is the same: download the `.rwm/` directory from a known location during bootstrap.
473
+
474
+ After cloning and running `rwm bootstrap`, developers have a warm cache. Creating a feature branch from main and running `rwm run spec --affected` skips unchanged packages immediately.
475
+
476
+ ## Affected detection
477
+
478
+ ### What "affected" means
479
+
480
+ When you change code on a feature branch, the affected packages are those you directly changed plus every package that depends on them, transitively. If you change `libs/auth/` and `libs/billing/` depends on `auth` and `apps/api/` depends on `billing`, all three are affected.
481
+
482
+ ### Viewing affected packages
483
+
484
+ ```sh
485
+ rwm affected
486
+ ```
487
+
488
+ ### Running tasks on affected packages
489
+
490
+ ```sh
491
+ rwm run spec --affected
492
+ ```
493
+
494
+ This is the most useful command for feature branch CI. It combines affected detection with task execution — only affected packages are tested, in correct dependency order with full parallelism.
495
+
496
+ ### How change detection works
497
+
498
+ RWM detects changes from three sources:
499
+
500
+ 1. **Committed changes** — `git diff --name-only <base>...HEAD`
501
+ 2. **Staged changes** — `git diff --name-only --cached`
502
+ 3. **Unstaged changes** — `git diff --name-only`
503
+
504
+ Changed files are mapped to packages by path prefix. Use `--committed` to only consider committed changes (ignoring staged and unstaged):
505
+
506
+ ```sh
507
+ rwm run spec --affected --committed
508
+ ```
509
+
510
+ ### Root-level changes
511
+
512
+ Files outside any package directory (like the root `Gemfile` or `Rakefile`) cause all packages to be marked as affected. This is a conservative default — root-level changes can affect the entire workspace.
513
+
514
+ However, inert files are automatically excluded from triggering a full run. The following patterns are ignored by default:
515
+
516
+ - `*.md`, `LICENSE*`, `CHANGELOG*`
517
+ - `.github/**`, `.vscode/**`, `.idea/**`
518
+ - `docs/**`, `.rwm/**`
519
+
520
+ You can add custom patterns in `.rwm/affected_ignore` (one glob per line, `#` for comments).
521
+
522
+ ### Base branch auto-detection
523
+
524
+ RWM detects the base branch by reading `git symbolic-ref refs/remotes/origin/HEAD`, falling back to checking for `main` or `master` locally. Override with:
525
+
526
+ ```sh
527
+ rwm affected --base develop
528
+ rwm run spec --affected --base develop
529
+ ```
530
+
531
+ ## Bootstrap and daily workflow
532
+
533
+ ### What bootstrap does
534
+
535
+ `rwm bootstrap` gets a workspace into a working state:
536
+
537
+ 1. Runs `bundle install` in the workspace root.
538
+ 2. Runs `rake bootstrap` in the root (if defined — for binstubs, shared tooling, etc.).
539
+ 3. Installs git hooks (pre-push runs `rwm check`, post-commit rebuilds the graph on Gemfile changes).
540
+ 4. Runs `bundle install` in every package (in parallel).
541
+ 5. Runs `rake bootstrap` in packages that define it (in parallel).
542
+ 6. Builds and validates the dependency graph.
543
+ 7. Updates the `.code-workspace` file (if it exists).
544
+
545
+ Both `rwm init` and `rwm bootstrap` are idempotent.
546
+
547
+ ### The bootstrap rake task
548
+
549
+ Every scaffolded package includes an empty `bootstrap` task. This is where package-specific setup belongs:
550
+
551
+ ```ruby
552
+ # libs/auth/Rakefile
553
+ task :bootstrap do
554
+ sh "bin/rails db:setup" if File.exist?("bin/rails")
555
+ sh "cp config/credentials.example.yml config/credentials.yml" unless File.exist?("config/credentials.yml")
556
+ end
557
+ ```
558
+
559
+ Common uses: database setup, copying example config files, generating local certificates, compiling native extensions.
560
+
561
+ The key property: `rwm bootstrap` runs every package's bootstrap task automatically. Developers don't need to know which packages have special setup — they run one command and everything is handled.
562
+
563
+ ### After cloning
564
+
565
+ ```sh
566
+ git clone <repo>
567
+ cd <repo>
568
+ rwm bootstrap
569
+ ```
570
+
571
+ Every package is installed, the graph is built, hooks are active, and the workspace is ready.
572
+
573
+ ### Daily workflow
574
+
575
+ ```sh
576
+ git pull --rebase
577
+ rwm bootstrap # picks up any new packages or dependency changes
578
+ git checkout -b my-feature
579
+ # ... make changes ...
580
+ rwm spec # run all specs
581
+ rwm spec --affected # or just the affected ones
582
+ ```
583
+
584
+ The pre-push hook runs `rwm check` automatically. The post-commit hook rebuilds the graph when Gemfiles change.
585
+
586
+ ## Git hooks
587
+
588
+ RWM installs two hooks during `rwm bootstrap`:
589
+
590
+ - **pre-push** — Runs `rwm check` to validate conventions before pushing. Blocks the push on failure.
591
+ - **post-commit** — Runs `rwm graph` if any Gemfile was changed in the commit. Keeps the cached graph in sync.
592
+
593
+ ### Overcommit integration
594
+
595
+ If `.overcommit.yml` exists, RWM integrates with [Overcommit](https://github.com/sds/overcommit) — it merges hook configuration into the YAML file and creates executable hook scripts. Without Overcommit, RWM writes directly to `.git/hooks/`, appending to existing hooks rather than overwriting.
596
+
597
+ ## Convention enforcement
598
+
599
+ ```sh
600
+ rwm check
601
+ ```
602
+
603
+ Three rules:
604
+
605
+ 1. **No library depending on an application.** Libraries are shared building blocks and must not be coupled to deployment targets.
606
+ 2. **No application depending on another application.** Applications are independent deployment units. Shared code should be extracted into a library.
607
+ 3. **No circular dependencies.** Cycles make build ordering impossible and indicate tangled responsibilities.
608
+
609
+ Exits 0 on pass, 1 on violation. The pre-push hook runs this automatically.
610
+
611
+ ## Rails and Zeitwerk
612
+
613
+ Rails uses [Zeitwerk](https://github.com/fxn/zeitwerk) for autoloading. Zeitwerk overrides `Kernel#require`, so `require "auth"` in a controller will fail with a `LoadError` even though the gem is installed. The fix: require workspace libs before Zeitwerk starts.
614
+
615
+ ### Setup
616
+
617
+ **1. Gemfile** — declare workspace deps with `rwm_lib`:
618
+
619
+ ```ruby
620
+ # apps/web/Gemfile
621
+ source "https://rubygems.org"
622
+ gemspec
623
+
624
+ require "rwm/gemfile"
625
+
626
+ rwm_lib "auth" # transitive deps resolved automatically
627
+ ```
628
+
629
+ `ruby_workspace_manager` must be a runtime dependency (not in `:development` group) for Rails apps:
630
+
631
+ ```ruby
632
+ # apps/web/web.gemspec
633
+ spec.add_dependency "ruby_workspace_manager"
634
+ ```
635
+
636
+ **2. application.rb** — require workspace libs before Rails loads:
637
+
638
+ ```ruby
639
+ # apps/web/config/application.rb
640
+ require_relative "boot"
641
+
642
+ require "rwm/rails"
643
+ Rwm.require_libs
644
+
645
+ require "rails"
646
+ require "action_controller/railtie"
647
+
648
+ module Web
649
+ class Application < Rails::Application
650
+ config.load_defaults 8.0
651
+ end
652
+ end
653
+ ```
654
+
655
+ `Rwm.require_libs` requires exactly the libs that `rwm_lib` resolved in the Gemfile — direct and transitive. After this line, workspace libraries are loaded as plain Ruby modules. Zeitwerk takes over for the app's own code and never touches them.
656
+
657
+ ### Why this ordering matters
658
+
659
+ The Rails boot sequence: `config/boot.rb` runs `Bundler.setup` (adds gems to load path) → `config/application.rb` (your code, then Rails) → `config/environment.rb` (Zeitwerk activates). `Rwm.require_libs` must run after Bundler.setup and before `require "rails"`.
660
+
661
+ ### What doesn't work
662
+
663
+ - `require "auth"` inside a controller or model — Zeitwerk is already active.
664
+ - Adding workspace libs to `config.autoload_paths` — they have their own structure.
665
+
666
+ Non-Rails apps don't have this problem and can `require` workspace libs defined in their Gemfile anywhere in their code.
667
+
668
+ ## VSCode integration
669
+
670
+ ```sh
671
+ rwm init --vscode
672
+ ```
673
+
674
+ Generates a `.code-workspace` file that configures VSCode's multi-root workspace feature. Each package becomes a separate root folder in the sidebar. After initial creation, `rwm bootstrap` and `rwm new` keep the folder list updated automatically. Existing `settings`, `extensions`, `launch`, and `tasks` keys are preserved.
675
+
676
+ ## Shell completions
677
+
678
+ RWM ships with completion scripts for Bash and Zsh that provide command, flag, and package name completion.
679
+
680
+ ### Bash
681
+
682
+ Add to `.bashrc` or `.bash_profile`:
683
+
684
+ ```bash
685
+ source "$(gem contents ruby_workspace_manager | grep rwm.bash)"
686
+ ```
687
+
688
+ ### Zsh
689
+
690
+ Add to `.zshrc` (before `compinit`):
691
+
692
+ ```zsh
693
+ fpath=($(gem contents ruby_workspace_manager | grep completions/rwm.zsh | xargs dirname) $fpath)
694
+ autoload -Uz compinit && compinit
695
+ ```
696
+
697
+ Both scripts dynamically discover package names by scanning `libs/` and `apps/`, so tab completion always reflects your current workspace.
698
+
699
+ ## Command reference
13
700
 
14
701
  | Command | Description |
15
702
  |---------|-------------|
16
- | `rwm init` | Initialize a workspace. |
17
- | `rwm bootstrap` | Install deps, build graph, install hooks, run bootstrap tasks. |
18
- | `rwm new <app\|lib> <name>` | Scaffold a new package. |
19
- | `rwm graph` | Rebuild the dependency graph. `--dot` / `--mermaid` for visualization. |
20
- | `rwm run <task> [pkg]` | Run a Rake task across packages. Packages without the task are skipped. |
21
- | `rwm <task> [pkg]` | Any unknown command is a task shortcut: `rwm test` = `rwm run test`. |
22
- | `rwm run <task> --affected` | Run only on packages affected by current changes. |
23
- | `rwm check` | Validate conventions. |
703
+ | `rwm init [--vscode]` | Initialize a workspace. Creates dirs, Gemfile, Rakefile, .gitignore. Runs bootstrap. Idempotent. |
704
+ | `rwm bootstrap` | Install deps, build graph, install hooks, run bootstrap tasks. Idempotent. |
705
+ | `rwm new <app\|lib> <name> [--test=FW]` | Scaffold a new package. `--test`: `rspec` (default), `minitest`, `none`. |
706
+ | `rwm info <name>` | Show package details: type, path, deps, dependents. |
707
+ | `rwm graph [--dot\|--mermaid]` | Rebuild dependency graph. Optionally output DOT or Mermaid. |
708
+ | `rwm check` | Validate conventions. Exit 0 on pass, 1 on failure. |
24
709
  | `rwm list` | List all packages. |
25
- | `rwm info <name>` | Show package details. |
26
- | `rwm affected` | Show affected packages. |
710
+ | `rwm run <task> [pkg]` | Run a Rake task across packages. |
711
+ | `rwm <task> [pkg]` | Task shortcut: `rwm spec` = `rwm run spec`. |
712
+ | `rwm affected [--committed] [--base REF]` | Show affected packages. |
27
713
  | `rwm cache clean [pkg]` | Clear cached task results. |
714
+ | `rwm help` | Show available commands. |
715
+ | `rwm version` | Show version. |
716
+
717
+ ### `rwm run` flags
718
+
719
+ | Flag | Description |
720
+ |------|-------------|
721
+ | `--affected` | Only run on packages affected by current changes. |
722
+ | `--committed` | With `--affected`, only consider committed changes. |
723
+ | `--base REF` | With `--affected`, compare against REF instead of auto-detected base. |
724
+ | `--dry-run` | Show what would run without executing. |
725
+ | `--no-cache` | Bypass task caching. Force all tasks to run. |
726
+ | `--buffered` | Buffer output per-package and print on completion. |
727
+ | `--concurrency N` | Limit parallel workers. Default: number of CPU cores. |
728
+
729
+ ## Design philosophy
730
+
731
+ **Zero runtime dependencies.** RWM depends only on Ruby's standard library and Bundler (which ships with Ruby). No Thor, no custom graph library — `TSort` from stdlib handles topological sorting. Installing RWM adds nothing to your dependency tree.
732
+
733
+ **No configuration file.** The git root is the workspace root. Libraries go in `libs/`, applications go in `apps/`. The dependency graph comes from Gemfiles. The conventions are the configuration.
28
734
 
29
- Shell completions for Bash and Zsh are included see [GUIDE.md](GUIDE.md) for setup instructions.
735
+ **Delegation to Rake.** RWM does not invent a task system. It runs `bundle exec rake <task>` in each package. The Rakefile has full control over execution; RWM handles orchestration.
30
736
 
31
- See [GUIDE.md](GUIDE.md) for full usage documentation dependencies, caching, affected detection, git hooks, design decisions, and more.
737
+ **Content-hash caching over timestamps.** The cache uses SHA256 content hashes rather than file timestamps. Timestamps change on branch switches and rebases. Content hashes are deterministic. This is the same model that [redo](https://cr.yp.to/redo.html) and [Bazel](https://bazel.build/) use.
32
738
 
33
- ## License
739
+ ## Resources
34
740
 
35
- MIT
741
+ - **[Nx](https://nx.dev)** — The JavaScript monorepo tool that inspired RWM's workspace model, affected detection, and task caching.
742
+ - **[DJB's redo](https://cr.yp.to/redo.html)** — Build system that pioneered content-hash-based caching. RWM's task cache uses the same principle.
743
+ - **[Bazel](https://bazel.build/)** — Google's build tool. RWM borrows content-addressable caching but trades Bazel's complexity for convention-over-configuration.
744
+ - **[TSort](https://ruby-doc.org/stdlib/libdoc/tsort/rdoc/TSort.html)** — Ruby stdlib module implementing Tarjan's algorithm. Used for topological sorting and cycle detection.
745
+ - **[Bundler](https://bundler.io/)** — RWM reads Gemfiles using Bundler's DSL parser and relies on Bundler's dependency resolution at runtime.
746
+ - **[Overcommit](https://github.com/sds/overcommit)** — Git hook manager that RWM integrates with when present.
747
+ - **[Lerna](https://lerna.js.org/)** — The original JavaScript monorepo tool. RWM's `bootstrap` command is inspired by `lerna bootstrap`.