cybertrain 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 213798cb904eb6eb4ba8f45d0ef3c4d89db13533689a40ca0faf8419514bdc25
4
- data.tar.gz: 4a975e911f2e3bbd3de9c3e5d126a5095a52a36bbe21365e97620c0b13b921d1
3
+ metadata.gz: 2bfb12d19791bff7ce88504f9ffb5fe6e2d90ae7a55aa24f5571b1909cf11976
4
+ data.tar.gz: 0eaac7a028bbf9be6c013b716ecec76f02c91fb10aa04e855f7b777f250c8712
5
5
  SHA512:
6
- metadata.gz: 44538d5ef1697095ca313333baaf0c7a47ece4ae98ed480b39da2553cb99f3da265862d24e921a7e60f8226bbb02a52b804cbc7f95b553152730b19f626772e9
7
- data.tar.gz: 7edfda7ced1ce88bea85fca1a45ced852ebdee842861fb3ebd6ee0cfe9bfdb317bdecce8e1f330a1095ba1712f5e6f122eb2e48e6b0f447849ea1a29db7b95fb
6
+ metadata.gz: 66bf793c97cf16d109519514b10c15d929e627137e58a4663d04ad3e88a746fa12e95b39358f15ebe8ba643107a2f8f30dbee54cbc9e95ea5127a4cc870fb417
7
+ data.tar.gz: 02bcced8e7ab059cb479b06835420eb241271b4447ed0e7d16c2801c6ed50b6b0d3587ff6557ae9b36501a5a108b05c2a4e7d5757caf7887c353ed41e4c2deb7
data/README.md CHANGED
@@ -1,10 +1,21 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="site/assets/brand/lockup-stacked-dark.svg">
4
+ <img alt="CyberTrain" src="site/assets/brand/lockup-stacked-light.svg" width="360">
5
+ </picture>
6
+ </p>
7
+ <p align="center">
8
+ <a href="https://saeki-mototsune.github.io/CyberTrain/">Homepage</a> ·
9
+ <a href="https://saeki-mototsune.github.io/CyberTrain/tutorial.html">Tutorial</a>
10
+ </p>
11
+
1
12
  # cybertrain
2
13
 
3
14
  cybertrain is a Rails-shaped web application framework written natively for
4
15
  [Spinel](https://github.com/matz/spinel), matz's ahead-of-time Ruby compiler.
5
16
  There is no Rack, no gems at runtime, and no Ruby interpreter in the deployed
6
- artifact: `spin build` compiles an application — controllers, models, views
7
- and the framework itself — into a single native binary. Where Rails leans on
17
+ artifact: `cybertrain build` compiles an application — controllers, models,
18
+ views and the framework itself — into a single native binary. Where Rails leans on
8
19
  runtime metaprogramming (`method_missing`, `define_method`, `eval`) for
9
20
  `before_action :set_post` or `Post.where(title: "x")`, cybertrain gets the
10
21
  same vocabulary by generating plain, committed Ruby from `db/schema.rb` and
@@ -25,51 +36,135 @@ Rails" below.
25
36
 
26
37
  ## Requirements
27
38
 
28
- - Spinel `2026.09.12`, with `spinel` and `spin` on `PATH` (see `SPINEL_TAG`
29
- in [.github/workflows/ci.yml](.github/workflows/ci.yml)); untested against
30
- other versions.
31
- - A C toolchain (`cc`) — Spinel compiles every program to C.
32
- - Ruby 3.2+ and RubyGems, for the `cybertrain` command only (applications
33
- never run on CRuby).
39
+ - Spinel `2026.09.12` (`Cybertrain::SPINEL_TAG`, the same release as
40
+ `SPINEL_TAG` in [.github/workflows/ci.yml](.github/workflows/ci.yml));
41
+ untested against other versions. You do not install it by hand: the
42
+ `cybertrain` command builds that release into `~/.cybertrain` the first
43
+ time it needs it (`cybertrain setup` does that on request), unless a
44
+ `spinel` of that release is already on `PATH` — see "Installing the CLI".
45
+ - To build it: a C toolchain (`cc`, or the compiler `CC` names) and `make`
46
+ — Spinel compiles every program to C — plus `git` and `curl`.
34
47
  - SQLite 3 headers/library (`libsqlite3-dev` on Debian/Ubuntu; present with
35
48
  Xcode's command line tools on macOS) — models use Spinel's FFI directly,
36
- not a gem. Building Spinel itself also needs OpenSSL's headers
37
- (`libssl-dev`); cybertrain itself does not link OpenSSL.
49
+ not a gem.
50
+ - Ruby 3.2+ and RubyGems, for the `cybertrain` command only (applications
51
+ never run on CRuby).
52
+ - OpenSSL's headers (`libssl-dev`) are optional: without them Spinel builds
53
+ without its `openssl` package, which cybertrain does not use.
54
+
55
+ `cybertrain doctor` checks these tools and headers and the Spinel install;
56
+ when a required one is missing it also prints the install command for your
57
+ platform.
38
58
 
39
- The framework has no separate test runner; it tests itself:
59
+ The framework has no separate test runner; it tests itself. `spin` comes
60
+ with Spinel, and `cybertrain spin` runs the copy `cybertrain` installs
61
+ without touching `PATH` (or put that copy on `PATH` with the `export
62
+ PATH=...` line `cybertrain setup` prints, and call `spin` directly):
40
63
 
41
64
  ```sh
42
- spin test
65
+ cybertrain spin test
43
66
  ```
44
67
 
45
68
  compiles and runs every program under `test/` and diffs its output against a
46
69
  committed `test/<name>.rb.expected` snapshot — minitest/RSpec can't run under
47
70
  Spinel, since they find test methods by reflection and an ahead-of-time
48
- compiler has nothing to reflect on at run time. `spin test --regen
49
- test/<name>.rb` rewrites a snapshot from CRuby's output; a few tests that
71
+ compiler has nothing to reflect on at run time. `cybertrain spin test
72
+ --regen test/<name>.rb` rewrites a snapshot from CRuby's output; a few tests that
50
73
  touch SQLite through FFI take theirs from the compiled binary instead
51
- ([spikes/NOTES.md](spikes/NOTES.md), rule 23).
74
+ ([spikes/NOTES.md](spikes/NOTES.md), rule 23); `script/regen-snapshot
75
+ test/<name>.rb` does that build-then-capture step for you and prints the diff stat.
52
76
 
53
77
  ## Installing the CLI
54
78
 
55
79
  ```sh
56
80
  gem install cybertrain
81
+ cybertrain new blog # also installs Spinel, the first time (a few minutes)
82
+ cd blog
83
+ cybertrain g scaffold article title:string body:text
84
+ cybertrain db migrate
85
+ cybertrain server # http://127.0.0.1:3000
86
+ cybertrain build # production: dist/
57
87
  ```
58
88
 
59
89
  The `cybertrain` command is plain Ruby and the gem runs it under CRuby
60
- (3.2 or newer); the framework itself is not in the gem. Two commands:
61
- `cybertrain new NAME` scaffolds an application, `cybertrain generate
62
- scaffold NAME field:type ...` (alias `g scaffold`) adds a resource to one.
63
-
64
- `cybertrain new` points the app's `spin.toml` at the release matching the
65
- CLI — `cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain",
66
- ref = "v0.1.0" }` — then runs `spin lock` (spin fetches the framework into
67
- its cache, `~/.cache/spin/packages/`, and pins the commit in `spin.lock`)
68
- and `spin run gen`, much as `rails new` runs `bundle install`. Nothing of
69
- the framework is copied into the app; commit `spin.toml` and `spin.lock`.
70
- `--skip-spin` leaves both steps for later, `--path DIR` depends on a local
71
- checkout instead, and `--version V` on an index version once cybertrain is
72
- on a `spin-index`.
90
+ (3.2 or newer); neither the framework nor Spinel is in the gem. Its
91
+ commands (`cybertrain help` prints them):
92
+
93
+ - `cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]]
94
+ [--skip-spin]` creates an application in NAME, then runs `spin lock` and
95
+ `spin run gen` in it, installing Spinel first when it is needed.
96
+ - `cybertrain generate scaffold NAME field:type ...` (alias `g scaffold`)
97
+ adds a resource to the application in the current directory.
98
+ - `cybertrain db migrate` generates, applies pending migrations, and
99
+ generates again (`spin run gen; spin run db -- migrate; spin run gen`);
100
+ `cybertrain migration` is kept as an alias.
101
+ - `cybertrain db COMMAND...` runs any other database command after `spin
102
+ run gen`: `status`, `rollback [N]` (which generates again afterwards),
103
+ `schema:dump`, `create`.
104
+ - `cybertrain server [PORT]` generates, then starts the development server
105
+ (`spin run gen; spin run NAME`, or `spin run NAME -- PORT`; default port
106
+ 3000).
107
+ - `cybertrain build` builds NAME with `app/views/` embedded and assembles
108
+ `dist/` (the binary, `public/`, `storage/`, `tmp/`).
109
+ - `cybertrain spin ARGS...` runs `spin` with the pinned Spinel from any
110
+ directory (`cybertrain spin test`, `cybertrain spin run gen -- --check`).
111
+ - `cybertrain setup [--force]` installs Spinel `2026.09.12` into
112
+ `~/.cybertrain` unless a `spinel` of that release is already on `PATH`;
113
+ `--force` rebuilds the copy under `~/.cybertrain`.
114
+ - `cybertrain doctor` checks the C toolchain, the SQLite headers and the
115
+ Spinel install.
116
+ - `cybertrain version` prints the version.
117
+
118
+ `db`, `server` and `build` run inside an application; NAME is the
119
+ `[package] name` in its `spin.toml`.
120
+
121
+ Every command that runs `spin` (`new`, `db`, `server`, `build`, `spin`) first makes
122
+ sure a Spinel of the release in `Cybertrain::SPINEL_TAG` is available. A
123
+ `spinel` on `PATH` (with a `spin`) whose `spinel --version` reports that
124
+ release is used as it is; otherwise the copy `cybertrain` keeps under
125
+ `~/.cybertrain/spinel/2026.09.12/` is used; otherwise `cybertrain` builds
126
+ that copy from the release tag (`git clone`, `make deps`, `make -j`, `make
127
+ install PREFIX=...`), once, in a few minutes. The build log is
128
+ `~/.cybertrain/log/spinel-2026.09.12-build.log` (its last lines are printed
129
+ if a step fails); if a tool the build needs is missing, `cybertrain` says
130
+ which and prints the install command for your platform before it starts. A
131
+ `spinel` of another release on `PATH` is never used or touched:
132
+ `cybertrain` keeps its own copy (and says so when it installs it).
133
+
134
+ `cybertrain setup` does the same on request and, whenever its own copy is
135
+ the one in use, prints the line that puts it on `PATH`, for running `spin`
136
+ yourself (`~/.cybertrain/bin` holds `spinel` and `spin` links to the release
137
+ in use, so the line survives a release bump; `cybertrain spin ...` needs no
138
+ `PATH` at all):
139
+
140
+ ```sh
141
+ export PATH="$HOME/.cybertrain/bin:$PATH"
142
+ ```
143
+
144
+ Two environment variables override all this: `CYBERTRAIN_HOME` replaces
145
+ `~/.cybertrain` (a path without spaces, quotes, `$` or `:`, which Spinel's
146
+ Makefile and `PATH` cannot take), and `CYBERTRAIN_SPINEL_HOME=PREFIX` forces
147
+ an existing install — `cybertrain` then uses the `spinel` and `spin` in `PREFIX/bin` (or
148
+ in `PREFIX` itself, if `spinel` is directly in it) and stops with an error
149
+ if they are not that release.
150
+
151
+ `cybertrain new` writes the application with its `spin.toml` pointing at
152
+ the release matching the CLI (`cybertrain = { git =
153
+ "https://github.com/saeki-mototsune/cybertrain", ref = "v0.2.0" }`), makes
154
+ sure Spinel is installed, then runs `spin lock` (spin fetches the framework
155
+ into its cache, `~/.cache/spin/packages/`, and pins the commit in
156
+ `spin.lock`) and `spin run gen`, much as `rails new` runs `bundle install`,
157
+ and prints the next steps. Nothing of the framework is copied into the app;
158
+ commit `spin.toml` and `spin.lock`. If Spinel or one of the two `spin`
159
+ steps fails, the files stay, `new` exits with 1 and prints what to run once
160
+ the cause is fixed (for Spinel: `cybertrain setup`, then `cybertrain spin
161
+ lock && cybertrain spin run gen` in the new directory).
162
+
163
+ `--skip-spin` only writes the files: no Spinel install, no `spin lock`, no
164
+ `spin run gen`. `--path DIR` depends on a local checkout instead,
165
+ `--git URL [--ref R]` on the framework at URL (at branch or tag R), and
166
+ `--version V` on an index version once cybertrain is on a `spin-index`.
167
+ Give only one of `--path`, `--version` and `--git`; `--ref` needs `--git`.
73
168
 
74
169
  Working on cybertrain itself, run the CLI from the checkout (`spin install`
75
170
  builds `bin/cybertrain.rb` into `~/.local/bin/cybertrain`; `ruby -I.
@@ -87,7 +182,7 @@ articles and comments.
87
182
  ```sh
88
183
  cybertrain new blog
89
184
  cd blog
90
- spin build # works right away: `new` already locked the framework and ran `spin run gen`
185
+ cybertrain server # works right away: `new` already locked the framework and ran `spin run gen`
91
186
  ```
92
187
 
93
188
  ```
@@ -95,10 +190,23 @@ blog/
95
190
  spin.toml config/app.rb config/routes.rb
96
191
  db/schema.rb db/migrate/
97
192
  app/controllers/application_controller.rb app/models/ app/views/ app/helpers/
98
- public/ bin/server.rb bin/gen.rb bin/db.rb
193
+ public/ bin/blog.rb bin/gen.rb bin/db.rb
99
194
  gen/ storage/ tmp/ test/
100
195
  ```
101
196
 
197
+ `bin/blog.rb` is the application's one entry point, the program `spin build
198
+ blog` compiles: `./blog` serves, `./blog migrate` and `./blog db status`
199
+ manage the database. `bin/gen.rb` runs the generator (`spin run gen`), and
200
+ `bin/db.rb` runs the migrator during development (`cybertrain db migrate`
201
+ uses it, since `bin/blog.rb` cannot compile until the first migration has
202
+ generated `gen/models/`). `gen/` already holds `views.rb`, the committed,
203
+ empty table of embedded views (see "How it works").
204
+
205
+ Apps created before `cybertrain build` existed have `bin/server.rb` and
206
+ no `bin/<name>.rb`: copy `bin/<name>.rb` from a fresh `cybertrain new` (with
207
+ the same NAME), delete `bin/server.rb`, add `/dist/` to `.gitignore` (the old
208
+ template did not ignore it) and run `cybertrain spin run gen`.
209
+
102
210
  `ApplicationController` starts with the one thing every controller inherits:
103
211
 
104
212
  ```ruby
@@ -181,19 +289,23 @@ end
181
289
  ### Generate, migrate, run
182
290
 
183
291
  ```sh
184
- spin run gen # reads db/schema.rb + config/routes.rb, scans app/, writes gen/
185
- spin run db -- migrate # applies db/migrate/*.rb, rewrites db/schema.rb (arguments go after --)
186
- spin run server # http://127.0.0.1:3000
292
+ cybertrain db migrate # spin run gen; spin run db -- migrate; spin run gen
293
+ cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views reload without a rebuild
187
294
  ```
188
295
 
189
- Re-run `spin run gen` (and commit `gen/`) after touching the schema, routes,
190
- or a controller/model's callbacks and ivars — the dev server does this for
296
+ `spin run gen` reads `db/schema.rb` and `config/routes.rb`, scans `app/` and
297
+ writes `gen/`; `spin run db -- migrate` applies `db/migrate/*.rb` and
298
+ rewrites `db/schema.rb`.
299
+
300
+ Re-run `cybertrain spin run gen` (and commit `gen/`) after touching the
301
+ schema, routes, or a controller/model's callbacks and ivars — the dev server does this for
191
302
  you automatically (see "How it works").
192
303
 
193
304
  ### Add comments, nested under articles
194
305
 
195
306
  ```sh
196
307
  cybertrain generate scaffold comment commenter:string body:text article:references
308
+ cybertrain db migrate # the scaffold wrote a migration for the comments table
197
309
  ```
198
310
 
199
311
  `article:references` adds an `article_id` column, index and foreign key —
@@ -277,6 +389,27 @@ child routes to the nested collection (`article_comments_path`), a persisted
277
389
  one to the nested member (`article_comment_path`) — generated into
278
390
  `gen/routes.rb` from the nested `resources` block above.
279
391
 
392
+ ### Build for deployment
393
+
394
+ ```sh
395
+ cybertrain build
396
+ ```
397
+
398
+ writes `dist/`: `dist/blog`, the app compiled with every `app/views/`
399
+ template embedded (rendered from memory, error messages still say
400
+ `articles/show.html.erb:12`), `dist/public/`, and empty `dist/storage/` and
401
+ `dist/tmp/`. Copy `dist/` to a machine with the same OS and CPU (the binary
402
+ links the system SQLite) and run `./blog migrate && ./blog`; a built binary
403
+ is production by default (`CYBERTRAIN_ENV` overrides). Set
404
+ `CYBERTRAIN_SECRET_KEY_BASE`. Static files can also be served by the reverse
405
+ proxy from `dist/public/` — see [docs/deploy.md](docs/deploy.md).
406
+
407
+ A running `cybertrain server` skips its rebuilds while `cybertrain build` is
408
+ in progress (the build holds `tmp/cybertrain-build.lock`, since both write
409
+ `build/bin/blog`), logs the skip, and rebuilds as usual on the next save
410
+ once the build has finished. The lock records the build's PID; one left
411
+ behind by a killed build is ignored and removed.
412
+
280
413
  ## How it works
281
414
 
282
415
  **Build-time code generation.** `spin run gen` (`bin/gen.rb`) executes
@@ -289,17 +422,24 @@ zero-argument `def`s. From that it writes plain, readable Ruby under `gen/`:
289
422
  `gen/models/<name>.rb` (accessors, casts, finders, FK-derived associations),
290
423
  `gen/routes.rb` (route table, dispatcher, `*_path`/`*_url` helpers),
291
424
  `gen/controllers.rb` (`view_assigns` and `run_callback` per controller),
292
- `gen/migrations.rb` and `gen/app.rb` (the `require_relative` manifest).
425
+ `gen/migrations.rb`, `gen/views.rb` (the embedded views table, empty unless
426
+ building for deployment) and `gen/app.rb` (the `require_relative` manifest).
293
427
  `gen/` is committed, not gitignored — `spin build`/`spin test` have no hook
294
428
  to generate first, so the checked-in output has to already be what compiles.
295
429
  CI re-runs `spin run gen` and fails on a diff, so stale generated code can't
296
430
  merge.
297
431
 
298
432
  **Views are interpreted, not compiled.** `app/views/**/*.html.erb` files look
299
- like Rails ERB but are read from disk, parsed into an AST at request time
300
- (cached after first parse in production; re-parsed on change in
301
- development), then walked by a tree-walking interpreter — Spinel has no
302
- `eval`, so this is a closed grammar (literals, `@ivar`/local lookups, calls
433
+ like Rails ERB but are parsed into an AST at request time, then walked by a
434
+ tree-walking interpreter. Where the source comes from depends on the mode:
435
+ in development (and test) the engine reads `app/views/` from disk, and in
436
+ development re-parses a template when it changes, so view edits need no rebuild.
437
+ `cybertrain build` runs `spin run gen -- --embed-views`, which writes every
438
+ template's source into `gen/views.rb` as a string table for that one `spin
439
+ build`, then runs `spin run gen` again to restore the empty table that is
440
+ committed; the production binary renders only that table (parsing each
441
+ template once) and refuses to boot when it is empty (`error: views are not
442
+ embedded in this binary`). Spinel has no `eval`, so this is a closed grammar (literals, `@ivar`/local lookups, calls
303
443
  through fixed per-type dispatch tables, `if`/`unless`,
304
444
  `each`/`each_with_index`/one helper block), not real Ruby. Full grammar,
305
445
  helpers and gaps: [docs/template-language.md](docs/template-language.md).
@@ -316,13 +456,15 @@ faster and stall-free for this I/O-bound shape at 100 concurrent connections.
316
456
  and foreign keys on; every query is bound, never interpolated. It's the only
317
457
  adapter today (PostgreSQL via `libpq` FFI is noted as possible future work).
318
458
 
319
- **The development loop.** `spin run server` in `development` (the default
320
- `CYBERTRAIN_ENV`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
459
+ **The development loop.** `cybertrain server` (`spin run gen`, then `spin run NAME`) in
460
+ `development` (the default `CYBERTRAIN_ENV`, except in a binary from
461
+ `cybertrain build`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
321
462
  and `gen/**/*.rb` every half second (views are excluded — the engine reloads
322
- those itself). A change runs `spin run gen && spin build server` in the
323
- background; success sends the server `SIGHUP`, whose handler stops listening
324
- and `execv`s the new binary on the same port and PID, invisible to a client
325
- mid-session. A failed build keeps serving the old binary and banners the
463
+ those itself — and so is `gen/views.rb`, empty in development). A change runs `spin run gen && spin build NAME` in the
464
+ background; success requests a restart (the same flag an external `kill -HUP`
465
+ sets): a monitor thread stops the listener and drains open requests, then the
466
+ main thread `execv`s the new binary on the same port and PID, invisible to a
467
+ client mid-session. A failed build keeps serving the old binary and banners the
326
468
  compiler output on every HTML response. None of this loads in production.
327
469
 
328
470
  ## Differences from Rails
@@ -330,7 +472,7 @@ compiler output on every HTML response. None of this loads in production.
330
472
  | Rails | cybertrain |
331
473
  | --- | --- |
332
474
  | `rails console` | No console — Spinel has no `eval` |
333
- | Edit code, the running app picks it up | Ruby needs a rebuild; `spin run server` in development does this for you (rebuild, `SIGHUP`, `execv`) |
475
+ | Edit code, the running app picks it up | Ruby needs a rebuild; `cybertrain server` in development does this for you (rebuild, stop, `execv`) |
334
476
  | Edit a view, no reload needed | Same — views are parsed from disk per request in development |
335
477
  | `def new` | `def new_action` (`new` would shadow `Klass.new(ctx)`) |
336
478
  | `before_action { do_thing }` (implicit `self`) | `before_action { \|c\| c.do_thing }` — no `instance_exec` on a stored block, so callbacks take the controller/record explicitly |
@@ -361,7 +503,7 @@ end
361
503
 
362
504
  | Variable | Attribute | Default |
363
505
  | --- | --- | --- |
364
- | `CYBERTRAIN_ENV` | `env` | `"development"` (also `"test"`, `"production"`) |
506
+ | `CYBERTRAIN_ENV` | `env` | `"development"`; `"production"` in a binary from `cybertrain build` (also `"test"`) |
365
507
  | `PORT` | `port` | `3000` |
366
508
  | `CYBERTRAIN_DATABASE` | `database_path` | `storage/#{env}.sqlite3` |
367
509
  | `CYBERTRAIN_SECRET_KEY_BASE` | `secret_key_base` | required in production; dev/test auto-generate one into `tmp/secret_key` |
@@ -374,10 +516,12 @@ Other attributes with fixed, overridable defaults: `host` (`"127.0.0.1"`),
374
516
  marks the session cookie `Secure`; `false` elsewhere), `pool_size` (4),
375
517
  `static_files`/`csrf` (`true`).
376
518
 
377
- **Deployment:** `spin build server` produces `build/bin/server`. Ship it with
378
- `app/views/` (read at request time, never compiled in), `public/` (static
379
- assets, or let a reverse proxy serve it) and `storage/` (the SQLite file),
380
- `CYBERTRAIN_ENV=production` and `CYBERTRAIN_SECRET_KEY_BASE` set. The binary
519
+ **Deployment:** `cybertrain build` produces `dist/`: the binary `dist/NAME`
520
+ with the views embedded, `dist/public/` (static assets, or let a reverse
521
+ proxy serve it), and `dist/storage/` (the SQLite file by default) and
522
+ `dist/tmp/`. Run it from `dist/` with `CYBERTRAIN_SECRET_KEY_BASE` set;
523
+ it is production unless `CYBERTRAIN_ENV` says otherwise, and `./NAME
524
+ migrate` applies the migrations compiled into it. The binary
381
525
  speaks plain HTTP/1.1 only; put nginx/Caddy in front for TLS and HTTP/2 (the
382
526
  session cookie is `Secure` in production, so serve it over HTTPS). In
383
527
  production an exception answers 500 and a bare error response (the router's
@@ -395,7 +539,7 @@ systemd, Caddy with HTTPS, redeploys, rollbacks and backups.
395
539
  App tests are plain Spinel programs under `test/`, like the framework's own:
396
540
 
397
541
  ```ruby
398
- require "cybertrain/test"
542
+ require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
399
543
 
400
544
  test "title must be present" do
401
545
  article = Article.new(body: "a body long enough to pass length")
@@ -410,29 +554,47 @@ full HTTP flows, `Cybertrain::Test::Client` drives an app in-process (no
410
554
  socket) with a cookie jar, so sessions and CSRF behave as behind a browser:
411
555
 
412
556
  ```ruby
413
- require "cybertrain/test/client"
414
-
415
- client = Cybertrain::Test::Client.new(BLOG)
416
- client.get("/articles/new")
417
- res = client.post("/articles", "article[title]" => "Hello",
418
- "article[body]" => "I am on Rails!",
419
- "authenticity_token" => token) # from the rendered form
420
- assert_redirected_to res, "/articles/1"
421
- assert_response res, :see_other
557
+ require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
558
+
559
+ test "POST /articles creates an article and redirects to it" do
560
+ client = Cybertrain::Test::Client.new(BLOG)
561
+ client.get("/articles/new")
562
+ res = client.post("/articles", { "authenticity_token" => BlogTest.form_token(client),
563
+ "article[title]" => "Hello Rails",
564
+ "article[body]" => "I am on Rails! This is my first article." })
565
+ assert_redirected_to res, BlogTest.article_path(Article.last)
566
+ assert_response res, :see_other
567
+ end
422
568
  ```
423
569
 
424
- `examples/blog/test/articles.rb`/`comments.rb` (setup in
425
- `test/support/blog_test.rb`) show the full pattern, including token
426
- extraction, against a freshly migrated `storage/test.sqlite3`.
427
-
428
- Run with `spin test`: it compiles each `test/*.rb` into its own program and
429
- diffs stdout against `test/<name>.rb.expected`. Regenerate with `spin test
430
- --regen test/<name>.rb`; FFI/database tests can't run under CRuby, so their
431
- snapshot comes from the compiled binary instead: `spin test test/<name>.rb &&
432
- ./build/test/<name> > test/<name>.rb.expected`.
570
+ Both snippets start from the example's helper,
571
+ [`examples/blog/test/support/blog_test.rb`](examples/blog/test/support/blog_test.rb):
572
+ copy it into an app's `test/support/`, where files are required, not run.
573
+ It migrates a fresh `storage/test.sqlite3`, boots the app as `BLOG`, loads
574
+ `Cybertrain::Test` and its client, and reads the CSRF token out of the last
575
+ rendered form (`BlogTest.form_token`). End each test file with
576
+ `Cybertrain::Test.run!`; `examples/blog/test/articles.rb` and `comments.rb`
577
+ show the full pattern.
578
+
579
+ Run with `cybertrain spin test`: it compiles each `test/*.rb` into its own
580
+ program and diffs its output (stdout and stderr together) against
581
+ `test/<name>.rb.expected`. Regenerate with `cybertrain spin test --regen
582
+ test/<name>.rb`; FFI/database tests can't run under CRuby, so their snapshot
583
+ comes from the compiled binary instead: `rm -f build/test/<name>; cybertrain
584
+ spin test test/<name>.rb; [ -x build/test/<name> ] && ./build/test/<name> >
585
+ test/<name>.rb.expected 2>&1` (`cybertrain spin test` exits non-zero while the
586
+ snapshot is stale, so it is followed by `;`, not `&&`; the `[ -x ]` check skips
587
+ the capture when the compile failed, so the old snapshot is kept; in the
588
+ framework checkout, `script/regen-snapshot test/<name>.rb` does exactly this and
589
+ prints the diff stat).
433
590
 
434
591
  ## Learn more
435
592
 
593
+ - [Homepage](https://saeki-mototsune.github.io/CyberTrain/) and
594
+ [tutorial](https://saeki-mototsune.github.io/CyberTrain/tutorial.html) —
595
+ the source lives in [site/](site/), published by
596
+ [.github/workflows/pages.yml](.github/workflows/pages.yml); the logo files and
597
+ brand notes are in [site/assets/brand/](site/assets/brand/BRAND.md).
436
598
  - [docs/design.md](docs/design.md) — the design record (Japanese): every
437
599
  decision, what was rejected and why, and the Spinel constraints behind it.
438
600
  - [docs/template-language.md](docs/template-language.md) — the full template
@@ -453,8 +615,13 @@ before the gem is pushed:
453
615
  1. Bump `Cybertrain::VERSION` (`cybertrain/version.rb`) and `version` in
454
616
  `spin.toml` together (CI checks they match); spin caches a git
455
617
  dependency by that version.
456
- 2. Merge to `main`, then tag and push: `git tag v0.1.0 && git push origin v0.1.0`.
457
- 3. `gem build cybertrain.gemspec && gem push cybertrain-0.1.0.gem`.
618
+ 2. Merge to `main`, then tag and push: `git tag v0.2.0 && git push origin v0.2.0`.
619
+ 3. `gem build cybertrain.gemspec && gem push cybertrain-0.2.0.gem`.
620
+
621
+ `Cybertrain::SPINEL_TAG` (also in `cybertrain/version.rb`) is the Spinel
622
+ release `cybertrain setup` installs. It must equal `SPINEL_TAG` in
623
+ [.github/workflows/ci.yml](.github/workflows/ci.yml), which CI checks, so
624
+ change the two together when the framework moves to another Spinel release.
458
625
 
459
626
  ## License
460
627