boxwerk 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/CHANGELOG.md +26 -3
- data/README.md +70 -226
- data/exe/boxwerk +0 -8
- data/lib/boxwerk/cli.rb +12 -44
- data/lib/boxwerk/graph.rb +8 -66
- data/lib/boxwerk/loader.rb +33 -161
- data/lib/boxwerk/package.rb +5 -14
- data/lib/boxwerk/registry.rb +1 -12
- data/lib/boxwerk/setup.rb +5 -26
- data/lib/boxwerk/version.rb +1 -1
- data/lib/boxwerk.rb +0 -5
- metadata +4 -17
- data/example/Gemfile +0 -6
- data/example/Gemfile.lock +0 -66
- data/example/README.md +0 -130
- data/example/app.rb +0 -101
- data/example/package.yml +0 -6
- data/example/packages/finance/lib/invoice.rb +0 -51
- data/example/packages/finance/lib/tax_calculator.rb +0 -26
- data/example/packages/finance/package.yml +0 -10
- data/example/packages/util/lib/calculator.rb +0 -21
- data/example/packages/util/lib/geometry.rb +0 -26
- data/example/packages/util/package.yml +0 -5
- data/sig/boxwerk.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fbe46b469b3c4c08b56abcf734d26d26edf383161e3c2bc7e345c930ec4db13
|
|
4
|
+
data.tar.gz: 69b8f826fa921007beae2b32b4fa2e053f2c9109a0095a5813431cb4cc6447e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48649f22d3db03be484e78dbf42234b9a1bc13b6616d20a3dae31d0ea8e7542c559d315aa572e5f1f9ffca2cbc8639cf944e95569dc4e122028ccf3a87772702
|
|
7
|
+
data.tar.gz: 1a211414af0a486e1df289b1b12f84cc51eada906bad50fa047e3d9a38ef93c51a19871593844268a8ef14ab9fd36540698cb8ca7dafd57f42e6f6dc0ab844f4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [v0.2.0] - 2026-01-06
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Simplified implementation (~370 lines removed)
|
|
9
|
+
- Consolidated cycle detection in Graph (removed redundant methods)
|
|
10
|
+
- Removed unused Loader methods
|
|
11
|
+
- Streamlined all module implementations
|
|
12
|
+
- Added class-level documentation to all modules
|
|
13
|
+
- Simplified example application
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
- Removed `Gemfile.lock` from git (library best practice)
|
|
17
|
+
- Removed `sig/boxwerk.rbs`
|
|
18
|
+
- Excluded `example/` from gem package
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
- Reduced test suite from 62 to 46 tests
|
|
22
|
+
- Removed redundant unit tests (275 lines)
|
|
23
|
+
- Maintained full integration test coverage
|
|
24
|
+
|
|
25
|
+
## [v0.1.0] - 2026-01-05
|
|
4
26
|
|
|
5
27
|
Initial release.
|
|
6
28
|
|
|
7
|
-
[
|
|
8
|
-
[
|
|
29
|
+
[Unreleased]: https://github.com/dtcristo/boxwerk/compare/v0.2.0...HEAD
|
|
30
|
+
[v0.2.0]: https://github.com/dtcristo/boxwerk/releases/tag/v0.2.0
|
|
31
|
+
[v0.1.0]: https://github.com/dtcristo/boxwerk/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>
|
|
3
|
+
π¦ Boxwerk
|
|
4
|
+
</h1>
|
|
5
|
+
</div>
|
|
2
6
|
|
|
3
|
-
Boxwerk is
|
|
7
|
+
Boxwerk is an **experimental** Ruby package system with Box-powered constant isolation. It is used at runtime to organize code into packages with explicit dependencies and strict constant access using [`Ruby::Box`](https://docs.ruby-lang.org/en/master/Ruby/Box.html). Inspired by [Packwerk](https://github.com/Shopify/packwerk).
|
|
4
8
|
|
|
5
9
|
## Features
|
|
6
10
|
|
|
7
|
-
- **Strict Isolation**: Each package runs in its own `Ruby::Box`, preventing
|
|
8
|
-
- **Explicit Dependencies**: Dependencies
|
|
9
|
-
- **
|
|
11
|
+
- **Strict Isolation**: Each package runs in its own `Ruby::Box`, preventing constant leakage
|
|
12
|
+
- **Explicit Dependencies**: Dependencies declared in `package.yml`, validated as a DAG
|
|
13
|
+
- **Controlled Exports**: Only declared constants are accessible to importers
|
|
14
|
+
- **Flexible Imports**: Multiple strategies (namespaced, aliased, selective, renamed)
|
|
15
|
+
- **Lazy Loading**: Exports loaded on-demand when imported
|
|
10
16
|
|
|
11
|
-
##
|
|
17
|
+
## Current Limitations
|
|
12
18
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
19
|
+
- No gem isolationβall gems are global across packages
|
|
20
|
+
- No constant reloading support
|
|
21
|
+
- Exported constants must follow [Zeitwerk naming conventions](https://github.com/fxn/zeitwerk#file-structure)
|
|
22
|
+
- Console runs in root box, not root package box (due to IRB loading issues)
|
|
23
|
+
- `Ruby::Box` itself is experimental in Ruby 4.0
|
|
17
24
|
|
|
18
25
|
## Requirements
|
|
19
26
|
|
|
20
|
-
- Ruby 4.0+ with
|
|
21
|
-
- `RUBY_BOX=1` environment variable must be set at process startup
|
|
27
|
+
- Ruby 4.0+ with `RUBY_BOX=1` environment variable set.
|
|
22
28
|
|
|
23
29
|
## Quick Start
|
|
24
30
|
|
|
@@ -26,14 +32,15 @@ Boxwerk is a runtime package system for Ruby with strict isolation of constants
|
|
|
26
32
|
|
|
27
33
|
```
|
|
28
34
|
my_app/
|
|
29
|
-
βββ Gemfile
|
|
30
|
-
βββ package.yml # Root package
|
|
35
|
+
βββ Gemfile
|
|
36
|
+
βββ package.yml # Root package manifest
|
|
31
37
|
βββ app.rb # Your application entrypoint
|
|
32
38
|
βββ packages/
|
|
33
|
-
βββ
|
|
39
|
+
βββ finance/
|
|
34
40
|
βββ package.yml # Package manifest
|
|
35
41
|
βββ lib/
|
|
36
|
-
|
|
42
|
+
βββ invoice.rb # Defines Invoice
|
|
43
|
+
βββ tax_calculator.rb # Defines TaxCalculator
|
|
37
44
|
```
|
|
38
45
|
|
|
39
46
|
### 2. Define Your Gemfile
|
|
@@ -61,27 +68,6 @@ exports:
|
|
|
61
68
|
- TaxCalculator
|
|
62
69
|
```
|
|
63
70
|
|
|
64
|
-
**`packages/finance/lib/invoice.rb`:**
|
|
65
|
-
```ruby
|
|
66
|
-
class Invoice
|
|
67
|
-
def initialize(amount_cents)
|
|
68
|
-
# Money gem is accessible because it's in the Gemfile
|
|
69
|
-
@amount = Money.new(amount_cents, 'USD')
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def total
|
|
73
|
-
@amount
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**`packages/finance/lib/tax_calculator.rb`:**
|
|
79
|
-
```ruby
|
|
80
|
-
class TaxCalculator
|
|
81
|
-
# ...
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
71
|
### 4. Use in Your Application
|
|
86
72
|
|
|
87
73
|
**`app.rb`:**
|
|
@@ -97,40 +83,30 @@ puts invoice.total # => #<Money fractional:10000 currency:USD>
|
|
|
97
83
|
RUBY_BOX=1 boxwerk run app.rb
|
|
98
84
|
```
|
|
99
85
|
|
|
100
|
-
Boxwerk automatically
|
|
101
|
-
1. Sets up Bundler
|
|
102
|
-
2. Requires all gems from your Gemfile in the root box
|
|
103
|
-
3. Loads and wires all packages
|
|
104
|
-
4. Executes your script in the root package context
|
|
86
|
+
Boxwerk handles Bundler setup, gem loading, package wiring, and script execution automatically.
|
|
105
87
|
|
|
106
|
-
##
|
|
107
|
-
|
|
108
|
-
### Running Scripts
|
|
88
|
+
## Example
|
|
109
89
|
|
|
110
|
-
|
|
90
|
+
See the [example/](example/) directory for a working multi-package application:
|
|
111
91
|
|
|
112
92
|
```bash
|
|
113
|
-
|
|
93
|
+
cd example
|
|
94
|
+
RUBY_BOX=1 boxwerk run app.rb
|
|
114
95
|
```
|
|
115
96
|
|
|
116
|
-
|
|
117
|
-
- All gems from your Gemfile (automatically required)
|
|
118
|
-
- All imports defined in the root `package.yml`
|
|
119
|
-
|
|
120
|
-
### Interactive Console
|
|
97
|
+
## CLI Usage
|
|
121
98
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
99
|
+
**Run a script:**
|
|
100
|
+
```bash
|
|
101
|
+
boxwerk run script.rb [args...]
|
|
102
|
+
```
|
|
125
103
|
|
|
104
|
+
**Interactive console** (currently runs in root box, not root package):
|
|
126
105
|
```bash
|
|
127
106
|
boxwerk console [irb-args...]
|
|
128
107
|
```
|
|
129
108
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
### Help
|
|
133
|
-
|
|
109
|
+
**Help:**
|
|
134
110
|
```bash
|
|
135
111
|
boxwerk help
|
|
136
112
|
```
|
|
@@ -151,211 +127,79 @@ imports:
|
|
|
151
127
|
|
|
152
128
|
### Exports
|
|
153
129
|
|
|
154
|
-
Constants that should be visible to packages that import this one.
|
|
130
|
+
Constants that should be visible to packages that import this one. Exports are lazily loaded during boot; only those actually imported by dependent packages are loaded.
|
|
155
131
|
|
|
156
132
|
### Imports
|
|
157
133
|
|
|
158
|
-
|
|
134
|
+
Package dependencies that are wired as new constants in the importing package's box. Default and aliased namespace imports create a module to hold the exports. **Not transitive**: if A imports B and B imports C, A cannot access C without explicitly importing it.
|
|
159
135
|
|
|
160
136
|
## Import Strategies
|
|
161
137
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
### 1. Default Namespace
|
|
165
|
-
|
|
166
|
-
Import all exports under a module named after the package:
|
|
167
|
-
|
|
138
|
+
**Default namespace** (all exports under package name):
|
|
168
139
|
```yaml
|
|
169
140
|
imports:
|
|
170
141
|
- packages/finance
|
|
142
|
+
# Result: Finance::Invoice, Finance::TaxCalculator
|
|
171
143
|
```
|
|
172
144
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
### 2. Aliased Namespace
|
|
176
|
-
|
|
177
|
-
Import under a custom module name:
|
|
178
|
-
|
|
145
|
+
**Aliased namespace** (custom module name):
|
|
179
146
|
```yaml
|
|
180
147
|
imports:
|
|
181
148
|
- packages/finance: Billing
|
|
149
|
+
# Result: Billing::Invoice, Billing::TaxCalculator
|
|
182
150
|
```
|
|
151
|
+
*Note: Single exports import directly without namespace*
|
|
183
152
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
**Single Export Optimization**: If a package exports only one constant, it's imported directly (not wrapped in a module):
|
|
187
|
-
|
|
188
|
-
```yaml
|
|
189
|
-
# util exports only Calculator
|
|
190
|
-
imports:
|
|
191
|
-
- packages/util: Calc
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Result: `Calc` (not `Calc::Calculator`)
|
|
195
|
-
|
|
196
|
-
### 3. Selective Import
|
|
197
|
-
|
|
198
|
-
Import specific constants directly:
|
|
199
|
-
|
|
153
|
+
**Selective import** (specific constants):
|
|
200
154
|
```yaml
|
|
201
155
|
imports:
|
|
202
156
|
- packages/finance:
|
|
203
157
|
- Invoice
|
|
204
158
|
- TaxCalculator
|
|
159
|
+
# Result: Invoice, TaxCalculator (no namespace)
|
|
205
160
|
```
|
|
206
161
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
### 4. Selective Rename
|
|
210
|
-
|
|
211
|
-
Import specific constants with custom names:
|
|
212
|
-
|
|
162
|
+
**Selective rename** (custom names):
|
|
213
163
|
```yaml
|
|
214
164
|
imports:
|
|
215
165
|
- packages/finance:
|
|
216
166
|
Invoice: Bill
|
|
217
167
|
TaxCalculator: Calculator
|
|
168
|
+
# Result: Bill, Calculator
|
|
218
169
|
```
|
|
219
170
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
## Gems and Packages
|
|
223
|
-
|
|
224
|
-
### How Gems Work in Boxwerk
|
|
225
|
-
|
|
226
|
-
When you run `boxwerk`, all gems in your `Gemfile` are:
|
|
227
|
-
1. Automatically loaded via Bundler in the root box
|
|
228
|
-
2. Accessible globally in all package boxes (gems are not isolated)
|
|
171
|
+
## Gem Handling
|
|
229
172
|
|
|
230
|
-
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
### Isolation Model
|
|
236
|
-
|
|
237
|
-
- **Root Box**: The box where Ruby bootstraps and all builtin classes/modules are defined. In Boxwerk, the root box performs all setup operations (Bundler setup, gem loading, dependency graph building, package box creation, and import wiring).
|
|
238
|
-
- **Main Box**: The first user box created automatically by Ruby (copied from root box). In Boxwerk, it only runs the `exe/boxwerk` executable file, which then calls into the root box to execute the setup. The main box has no other purpose.
|
|
239
|
-
- **Package Boxes**: Each package (including root package) runs in its own isolated `Ruby::Box` (created by copying from root box after gems are loaded).
|
|
240
|
-
- **Box Inheritance**: All boxes are created via copy-on-write from the root box, inheriting builtin classes and loaded gems.
|
|
241
|
-
- **Gems are Global**: All gems from Gemfile are accessible in all boxes (loaded in root box before package boxes are created).
|
|
242
|
-
- **Package Exports are Isolated**: Only explicit imports from packages are accessible.
|
|
243
|
-
- **No Transitive Access**: Packages can only see their explicit imports.
|
|
244
|
-
|
|
245
|
-
For more details on how Ruby::Box works, see the [official Ruby::Box documentation](https://docs.ruby-lang.org/en/master/Ruby/Box.html).
|
|
173
|
+
All gems in your `Gemfile` are:
|
|
174
|
+
- Automatically loaded in the root box via Bundler
|
|
175
|
+
- Accessible globally in all packages (no gem isolation)
|
|
176
|
+
- No manual `require` or `package.yml` declaration needed
|
|
246
177
|
|
|
247
178
|
## Known Issues
|
|
248
179
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
### Gem Requiring in Boxes
|
|
252
|
-
|
|
253
|
-
Requiring any gem from within a box (after boot) currently crashes the Ruby VM. This is likely an issue with Ruby::Box itself. As a workaround, Boxwerk automatically requires all gems from the Gemfile in the root box before creating package boxes, so gems are already loaded and accessible everywhere.
|
|
254
|
-
|
|
255
|
-
### Console Context
|
|
256
|
-
|
|
257
|
-
The console does not correctly run in the root package boxβit runs in the context of the root box instead. It should run in the root package box. However, if we attempt to `require 'irb'` in the root package box, the Ruby VM crashes due to the gem requiring issue described above.
|
|
258
|
-
|
|
259
|
-
### IRB Autocomplete
|
|
180
|
+
Related to Ruby::Box in Ruby 4.0+. See [Ruby::Box documentation](https://docs.ruby-lang.org/en/master/Ruby/Box.html) for details.
|
|
260
181
|
|
|
261
|
-
|
|
182
|
+
- **Gem requiring**: Crashes VM when requiring gems inside boxes after boot (workaround: gems pre-loaded in root box)
|
|
183
|
+
- **Console context**: Runs in root box instead of root package box due to IRB loading limitation
|
|
184
|
+
- **IRB autocomplete**: Disabled by since it currently crashes VMpe
|
|
262
185
|
|
|
263
186
|
## Architecture
|
|
264
187
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
The command-line interface handler that:
|
|
282
|
-
- Parses commands (`run`, `console`, `help`)
|
|
283
|
-
- Validates the Ruby environment (checks for `RUBY_BOX=1` and Ruby::Box support)
|
|
284
|
-
- Delegates to `Boxwerk::Setup` for the boot process
|
|
285
|
-
- Executes the requested command in the root package's box context
|
|
286
|
-
|
|
287
|
-
#### `Boxwerk::Setup`
|
|
288
|
-
|
|
289
|
-
The setup orchestrator that:
|
|
290
|
-
- Searches up the directory tree to find the root `package.yml`
|
|
291
|
-
- Creates a `Boxwerk::Graph` instance to build and validate the dependency graph
|
|
292
|
-
- Creates a `Boxwerk::Registry` instance to track booted packages
|
|
293
|
-
- Calls `Boxwerk::Loader.boot_all` to boot all packages in topological order
|
|
294
|
-
- Returns the loaded graph for introspection
|
|
295
|
-
|
|
296
|
-
#### `Boxwerk::Graph`
|
|
297
|
-
|
|
298
|
-
The dependency graph builder that:
|
|
299
|
-
- Parses the root `package.yml` and recursively discovers all package dependencies
|
|
300
|
-
- Builds a directed acyclic graph (DAG) of package relationships
|
|
301
|
-
- Validates that there are no circular dependencies
|
|
302
|
-
- Performs topological sorting to determine boot order (dependencies before consumers)
|
|
303
|
-
- Provides access to all packages in the graph
|
|
304
|
-
|
|
305
|
-
#### `Boxwerk::Package`
|
|
306
|
-
|
|
307
|
-
The package manifest parser that:
|
|
308
|
-
- Represents a single package with its configuration
|
|
309
|
-
- Parses `package.yml` files to extract exports and imports
|
|
310
|
-
- Normalizes the polymorphic import syntax (String, Array, Hash)
|
|
311
|
-
- Stores the package path, name, exports, imports, and box reference
|
|
312
|
-
- Tracks which exports have been loaded via `loaded_exports` hash (export name β file path)
|
|
313
|
-
- Tracks whether the package has been booted
|
|
314
|
-
|
|
315
|
-
#### `Boxwerk::Loader`
|
|
316
|
-
|
|
317
|
-
The package loader that:
|
|
318
|
-
- Creates a new `Ruby::Box` for each package (including the root package)
|
|
319
|
-
- Loads exported constants lazily on-demand when they are imported by other packages
|
|
320
|
-
- Uses Zeitwerk naming conventions to discover file locations for exported constants
|
|
321
|
-
- Caches loaded exports in `package.loaded_exports` to avoid redundant file loading
|
|
322
|
-
- Wires imports by injecting constants from dependency boxes into consumer boxes
|
|
323
|
-
- Implements all four import strategies (default namespace, aliased namespace, selective import, selective rename)
|
|
324
|
-
- Handles the single-export optimization for namespace imports
|
|
325
|
-
- Registers each booted package in the registry
|
|
326
|
-
- Only loads files that define exported constants, never loading non-exported code
|
|
327
|
-
|
|
328
|
-
#### `Boxwerk::Registry`
|
|
329
|
-
|
|
330
|
-
The package registry that:
|
|
331
|
-
- Tracks all booted package instances
|
|
332
|
-
- Allows packages to be retrieved by name during the wiring phase
|
|
333
|
-
- Ensures each package is only booted once
|
|
334
|
-
- Provides a clean interface for package lookup
|
|
335
|
-
|
|
336
|
-
## Example
|
|
337
|
-
|
|
338
|
-
See the [example/](example/) directory for a complete working example with:
|
|
339
|
-
|
|
340
|
-
- Multi-package application
|
|
341
|
-
- Gem usage
|
|
342
|
-
- Transitive dependency demonstration
|
|
343
|
-
- Isolation verification
|
|
344
|
-
- Console usage examples
|
|
345
|
-
|
|
346
|
-
Run it with:
|
|
347
|
-
|
|
348
|
-
```bash
|
|
349
|
-
cd example
|
|
350
|
-
RUBY_BOX=1 boxwerk run app.rb
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
Or explore interactively:
|
|
354
|
-
|
|
355
|
-
```bash
|
|
356
|
-
cd example
|
|
357
|
-
RUBY_BOX=1 boxwerk console
|
|
358
|
-
```
|
|
188
|
+
**Boot process:**
|
|
189
|
+
1. Setup Bundler and require all gems in root box
|
|
190
|
+
2. Find root `package.yml` by searching up from current directory
|
|
191
|
+
3. Build and validate dependency graph (DAG)
|
|
192
|
+
4. Boot packages in topological order, creating isolated boxes
|
|
193
|
+
5. Wire imports by lazily loading exports and injecting constants
|
|
194
|
+
6. Execute command in root package context
|
|
195
|
+
|
|
196
|
+
**Components:**
|
|
197
|
+
- **CLI**: Parses commands, validates environment, delegates to Setup
|
|
198
|
+
- **Setup**: Finds root package, builds graph, creates registry, boots packages
|
|
199
|
+
- **Graph**: Builds DAG, validates no cycles, performs topological sort
|
|
200
|
+
- **Package**: Parses `package.yml`, tracks exports/imports/box
|
|
201
|
+
- **Loader**: Creates boxes, loads exports lazily (Zeitwerk conventions), wires imports
|
|
202
|
+
- **Registry**: Tracks booted packages, ensures single boot per package
|
|
359
203
|
|
|
360
204
|
## Development
|
|
361
205
|
|
data/exe/boxwerk
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Boxwerk CLI
|
|
5
|
-
# Usage: boxwerk <command> [args...]
|
|
6
|
-
|
|
7
|
-
# Check if Ruby::Box is available and enabled
|
|
8
4
|
unless ENV['RUBY_BOX'] == '1'
|
|
9
5
|
puts "Error: Boxwerk requires Ruby::Box to be enabled"
|
|
10
6
|
puts "Please set RUBY_BOX=1 environment variable"
|
|
11
7
|
exit 1
|
|
12
8
|
end
|
|
13
9
|
|
|
14
|
-
# Verify Ruby::Box is actually available in this Ruby version
|
|
15
10
|
unless defined?(Ruby::Box)
|
|
16
11
|
puts "Error: Ruby::Box is not available in this Ruby version"
|
|
17
12
|
puts "Boxwerk requires Ruby 4.0 or later with Box support"
|
|
18
13
|
exit 1
|
|
19
14
|
end
|
|
20
15
|
|
|
21
|
-
# Setup Bundler, require any gems and run Boxwerk - all in the root box
|
|
22
16
|
Ruby::Box.root.eval(<<~RUBY)
|
|
23
17
|
require 'bundler/setup'
|
|
24
|
-
# Application gems need to be required in the root box to be accessible in package boxes
|
|
25
18
|
Bundler.require
|
|
26
|
-
# Run the CLI to handle the invocation
|
|
27
19
|
Boxwerk::CLI.run(ARGV)
|
|
28
20
|
RUBY
|
data/lib/boxwerk/cli.rb
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Boxwerk
|
|
4
|
-
# CLI
|
|
4
|
+
# CLI parses commands and delegates to Setup for package management.
|
|
5
|
+
# Handles run, console, and help commands.
|
|
5
6
|
module CLI
|
|
6
7
|
class << self
|
|
7
|
-
# Main entry point for CLI
|
|
8
|
-
# @param argv [Array<String>] Command line arguments
|
|
9
8
|
def run(argv)
|
|
10
9
|
if argv.empty?
|
|
11
10
|
print_usage
|
|
12
11
|
exit 1
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case command
|
|
14
|
+
case argv[0]
|
|
18
15
|
when 'run'
|
|
19
16
|
run_command(argv[1..-1])
|
|
20
17
|
when 'console'
|
|
@@ -23,7 +20,7 @@ module Boxwerk
|
|
|
23
20
|
print_usage
|
|
24
21
|
exit 0
|
|
25
22
|
else
|
|
26
|
-
puts "Error: Unknown command '#{
|
|
23
|
+
puts "Error: Unknown command '#{argv[0]}'"
|
|
27
24
|
puts ''
|
|
28
25
|
print_usage
|
|
29
26
|
exit 1
|
|
@@ -41,8 +38,6 @@ module Boxwerk
|
|
|
41
38
|
puts ' run <script.rb> [args...] Run a script in the root package context'
|
|
42
39
|
puts ' console [irb-args...] Start an IRB console in the root package context'
|
|
43
40
|
puts ' help Show this help message'
|
|
44
|
-
puts ''
|
|
45
|
-
puts 'All packages are loaded and wired before the command executes.'
|
|
46
41
|
end
|
|
47
42
|
|
|
48
43
|
def run_command(args)
|
|
@@ -54,50 +49,31 @@ module Boxwerk
|
|
|
54
49
|
end
|
|
55
50
|
|
|
56
51
|
script_path = args[0]
|
|
57
|
-
script_args = args[1..-1] || []
|
|
58
|
-
|
|
59
|
-
# Verify script exists
|
|
60
52
|
unless File.exist?(script_path)
|
|
61
53
|
puts "Error: Script not found: #{script_path}"
|
|
62
54
|
exit 1
|
|
63
55
|
end
|
|
64
56
|
|
|
65
|
-
# Perform Boxwerk setup (find package.yml, build graph, boot packages)
|
|
66
57
|
graph = perform_setup
|
|
67
|
-
|
|
68
|
-
# Execute the script in the root package's box
|
|
69
|
-
root_package = graph.root
|
|
70
|
-
execute_in_box(root_package.box, script_path, script_args)
|
|
58
|
+
execute_in_box(graph.root.box, script_path, args[1..-1] || [])
|
|
71
59
|
end
|
|
72
60
|
|
|
73
61
|
def console_command(args)
|
|
74
|
-
# Require IRB while we're still in root box context
|
|
75
62
|
require 'irb'
|
|
76
|
-
|
|
77
|
-
# Perform Boxwerk setup
|
|
78
63
|
graph = perform_setup
|
|
79
|
-
|
|
80
|
-
# Start IRB in the root package's box with provided args
|
|
81
|
-
root_package = graph.root
|
|
82
|
-
start_console_in_box(root_package.box, args)
|
|
64
|
+
start_console_in_box(graph.root.box, args)
|
|
83
65
|
end
|
|
84
66
|
|
|
85
67
|
def perform_setup
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
exit 1
|
|
91
|
-
end
|
|
68
|
+
Boxwerk::Setup.run!(start_dir: Dir.pwd)
|
|
69
|
+
rescue => e
|
|
70
|
+
puts "Error: #{e.message}"
|
|
71
|
+
exit 1
|
|
92
72
|
end
|
|
93
73
|
|
|
94
74
|
def execute_in_box(box, script_path, script_args)
|
|
95
|
-
# Set ARGV for the script using eval
|
|
96
75
|
box.eval("ARGV.replace(#{script_args.inspect})")
|
|
97
|
-
|
|
98
|
-
# Run the script in the isolated box
|
|
99
|
-
absolute_script_path = File.expand_path(script_path)
|
|
100
|
-
box.require(absolute_script_path)
|
|
76
|
+
box.require(File.expand_path(script_path))
|
|
101
77
|
end
|
|
102
78
|
|
|
103
79
|
def start_console_in_box(box, irb_args = [])
|
|
@@ -112,16 +88,8 @@ module Boxwerk
|
|
|
112
88
|
puts '=' * 70
|
|
113
89
|
puts ''
|
|
114
90
|
|
|
115
|
-
# Start IRB in the box context.
|
|
116
|
-
# TODO: This is currently broken. IRB runs the in the root box context.
|
|
117
|
-
# This should be fixed by calling `require 'irb'` inside the box, but
|
|
118
|
-
# that currently crashes the VM.
|
|
119
|
-
# Set ARGV to the provided IRB args so they can be processed by IRB.
|
|
120
|
-
# Always add --noautocomplete to disable autocomplete (currently broken with Ruby::Box)
|
|
121
|
-
# TODO: Enable autocomplete when it's not broken.
|
|
122
|
-
irb_args_with_noautocomplete = ['--noautocomplete'] + irb_args
|
|
123
91
|
box.eval(<<~RUBY)
|
|
124
|
-
ARGV.replace(#{
|
|
92
|
+
ARGV.replace(#{(['--noautocomplete'] + irb_args).inspect})
|
|
125
93
|
IRB.start
|
|
126
94
|
RUBY
|
|
127
95
|
end
|