cybertrain 0.1.1 → 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: 7660171ff57ac0e3f625dbffdef0a5a81de709bb80d2bcaf4e2b55be40034c24
4
- data.tar.gz: 467b4243e1b26cfa42b64e276f6f859d01c1c7f9bd96df015e1222b66b31dd5d
3
+ metadata.gz: 2bfb12d19791bff7ce88504f9ffb5fe6e2d90ae7a55aa24f5571b1909cf11976
4
+ data.tar.gz: 0eaac7a028bbf9be6c013b716ecec76f02c91fb10aa04e855f7b777f250c8712
5
5
  SHA512:
6
- metadata.gz: f56d35ff51a73b15c54376c145197847760d0238cf6599c68559d1c82c84f27998d65b0e53fd594ed7951f7d1b9d75ed3da7895571b4746e3ccb3b58e11bb7d1
7
- data.tar.gz: 49a0e1927cd913289307e6684065bf157a1c04c22a585bad2e38cd2826f523cf480aa8a2cea19cd2437fb21dd249dd21a4a3ad97e6c43e231a580f166c202aa6
6
+ metadata.gz: 66bf793c97cf16d109519514b10c15d929e627137e58a4663d04ad3e88a746fa12e95b39358f15ebe8ba643107a2f8f30dbee54cbc9e95ea5127a4cc870fb417
7
+ data.tar.gz: 02bcced8e7ab059cb479b06835420eb241271b4447ed0e7d16c2801c6ed50b6b0d3587ff6557ae9b36501a5a108b05c2a4e7d5757caf7887c353ed41e4c2deb7
data/README.md CHANGED
@@ -1,3 +1,14 @@
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
@@ -25,28 +36,40 @@ 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
74
  ([spikes/NOTES.md](spikes/NOTES.md), rule 23); `script/regen-snapshot
52
75
  test/<name>.rb` does that build-then-capture step for you and prints the diff stat.
@@ -55,38 +78,93 @@ test/<name>.rb` does that build-then-capture step for you and prints the diff st
55
78
 
56
79
  ```sh
57
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/
58
87
  ```
59
88
 
60
89
  The `cybertrain` command is plain Ruby and the gem runs it under CRuby
61
- (3.2 or newer); the framework itself is not in the gem. Its commands
62
- (`cybertrain help` prints them):
90
+ (3.2 or newer); neither the framework nor Spinel is in the gem. Its
91
+ commands (`cybertrain help` prints them):
63
92
 
64
- - `cybertrain new NAME` creates an application in NAME, then runs `spin
65
- lock` and `spin run gen` in it.
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.
66
96
  - `cybertrain generate scaffold NAME field:type ...` (alias `g scaffold`)
67
97
  adds a resource to the application in the current directory.
68
- - `cybertrain migration` generates, applies pending migrations, and
69
- generates again (`spin run gen; spin run db -- migrate; spin run gen`).
70
- - `cybertrain db COMMAND...` runs any database command: `status`,
71
- `rollback [N]`, `schema:dump`, `create`.
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`.
72
104
  - `cybertrain server [PORT]` generates, then starts the development server
73
105
  (`spin run gen; spin run NAME`, or `spin run NAME -- PORT`; default port
74
106
  3000).
75
107
  - `cybertrain build` builds NAME with `app/views/` embedded and assembles
76
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):
77
139
 
78
- `migration`, `db`, `server` and `build` run inside an application; NAME is
79
- the `[package] name` in its `spin.toml`.
140
+ ```sh
141
+ export PATH="$HOME/.cybertrain/bin:$PATH"
142
+ ```
80
143
 
81
- `cybertrain new` points the app's `spin.toml` at the release matching the
82
- CLI — `cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain",
83
- ref = "v0.1.1" }` — then runs `spin lock` (spin fetches the framework into
84
- its cache, `~/.cache/spin/packages/`, and pins the commit in `spin.lock`)
85
- and `spin run gen`, much as `rails new` runs `bundle install`. Nothing of
86
- the framework is copied into the app; commit `spin.toml` and `spin.lock`.
87
- `--skip-spin` leaves both steps for later, `--path DIR` depends on a local
88
- checkout instead, and `--version V` on an index version once cybertrain is
89
- on a `spin-index`.
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`.
90
168
 
91
169
  Working on cybertrain itself, run the CLI from the checkout (`spin install`
92
170
  builds `bin/cybertrain.rb` into `~/.local/bin/cybertrain`; `ruby -I.
@@ -119,14 +197,15 @@ blog/
119
197
  `bin/blog.rb` is the application's one entry point, the program `spin build
120
198
  blog` compiles: `./blog` serves, `./blog migrate` and `./blog db status`
121
199
  manage the database. `bin/gen.rb` runs the generator (`spin run gen`), and
122
- `bin/db.rb` runs the migrator during development (`cybertrain migration`
200
+ `bin/db.rb` runs the migrator during development (`cybertrain db migrate`
123
201
  uses it, since `bin/blog.rb` cannot compile until the first migration has
124
202
  generated `gen/models/`). `gen/` already holds `views.rb`, the committed,
125
203
  empty table of embedded views (see "How it works").
126
204
 
127
205
  Apps created before `cybertrain build` existed have `bin/server.rb` and
128
206
  no `bin/<name>.rb`: copy `bin/<name>.rb` from a fresh `cybertrain new` (with
129
- the same NAME), delete `bin/server.rb` and run `spin run gen`.
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`.
130
209
 
131
210
  `ApplicationController` starts with the one thing every controller inherits:
132
211
 
@@ -210,7 +289,7 @@ end
210
289
  ### Generate, migrate, run
211
290
 
212
291
  ```sh
213
- cybertrain migration # spin run gen; spin run db -- migrate; spin run gen
292
+ cybertrain db migrate # spin run gen; spin run db -- migrate; spin run gen
214
293
  cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views reload without a rebuild
215
294
  ```
216
295
 
@@ -218,14 +297,15 @@ cybertrain server # http://127.0.0.1:3000; rebuilds on Ruby edits, views
218
297
  writes `gen/`; `spin run db -- migrate` applies `db/migrate/*.rb` and
219
298
  rewrites `db/schema.rb`.
220
299
 
221
- Re-run `spin run gen` (and commit `gen/`) after touching the schema, routes,
222
- or a controller/model's callbacks and ivars — the dev server does this for
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
223
302
  you automatically (see "How it works").
224
303
 
225
304
  ### Add comments, nested under articles
226
305
 
227
306
  ```sh
228
307
  cybertrain generate scaffold comment commenter:string body:text article:references
308
+ cybertrain db migrate # the scaffold wrote a migration for the comments table
229
309
  ```
230
310
 
231
311
  `article:references` adds an `article_id` column, index and foreign key —
@@ -352,8 +432,8 @@ merge.
352
432
  **Views are interpreted, not compiled.** `app/views/**/*.html.erb` files look
353
433
  like Rails ERB but are parsed into an AST at request time, then walked by a
354
434
  tree-walking interpreter. Where the source comes from depends on the mode:
355
- in development (and test) the engine reads `app/views/` from disk and
356
- re-parses a template when it changes, so view edits need no rebuild.
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.
357
437
  `cybertrain build` runs `spin run gen -- --embed-views`, which writes every
358
438
  template's source into `gen/views.rb` as a string table for that one `spin
359
439
  build`, then runs `spin run gen` again to restore the empty table that is
@@ -381,9 +461,10 @@ adapter today (PostgreSQL via `libpq` FFI is noted as possible future work).
381
461
  `cybertrain build`) also polls `app/**/*.rb`, `config/**/*.rb`, `db/schema.rb`
382
462
  and `gen/**/*.rb` every half second (views are excluded — the engine reloads
383
463
  those itself — and so is `gen/views.rb`, empty in development). A change runs `spin run gen && spin build NAME` in the
384
- background; success sends the server `SIGHUP`, whose handler stops listening
385
- and `execv`s the new binary on the same port and PID, invisible to a client
386
- mid-session. A failed build keeps serving the old binary and banners 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
387
468
  compiler output on every HTML response. None of this loads in production.
388
469
 
389
470
  ## Differences from Rails
@@ -391,7 +472,7 @@ compiler output on every HTML response. None of this loads in production.
391
472
  | Rails | cybertrain |
392
473
  | --- | --- |
393
474
  | `rails console` | No console — Spinel has no `eval` |
394
- | Edit code, the running app picks it up | Ruby needs a rebuild; `cybertrain 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`) |
395
476
  | Edit a view, no reload needed | Same — views are parsed from disk per request in development |
396
477
  | `def new` | `def new_action` (`new` would shadow `Klass.new(ctx)`) |
397
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 |
@@ -458,7 +539,7 @@ systemd, Caddy with HTTPS, redeploys, rollbacks and backups.
458
539
  App tests are plain Spinel programs under `test/`, like the framework's own:
459
540
 
460
541
  ```ruby
461
- require "cybertrain/test"
542
+ require_relative "support/blog_test" # the app, BLOG and Cybertrain::Test
462
543
 
463
544
  test "title must be present" do
464
545
  article = Article.new(body: "a body long enough to pass length")
@@ -473,29 +554,47 @@ full HTTP flows, `Cybertrain::Test::Client` drives an app in-process (no
473
554
  socket) with a cookie jar, so sessions and CSRF behave as behind a browser:
474
555
 
475
556
  ```ruby
476
- require "cybertrain/test/client"
477
-
478
- client = Cybertrain::Test::Client.new(BLOG)
479
- client.get("/articles/new")
480
- res = client.post("/articles", "article[title]" => "Hello",
481
- "article[body]" => "I am on Rails!",
482
- "authenticity_token" => token) # from the rendered form
483
- assert_redirected_to res, "/articles/1"
484
- 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
485
568
  ```
486
569
 
487
- `examples/blog/test/articles.rb`/`comments.rb` (setup in
488
- `test/support/blog_test.rb`) show the full pattern, including token
489
- extraction, against a freshly migrated `storage/test.sqlite3`.
490
-
491
- Run with `spin test`: it compiles each `test/*.rb` into its own program and
492
- diffs stdout against `test/<name>.rb.expected`. Regenerate with `spin test
493
- --regen test/<name>.rb`; FFI/database tests can't run under CRuby, so their
494
- snapshot comes from the compiled binary instead: `spin test test/<name>.rb &&
495
- ./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).
496
590
 
497
591
  ## Learn more
498
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).
499
598
  - [docs/design.md](docs/design.md) — the design record (Japanese): every
500
599
  decision, what was rejected and why, and the Spinel constraints behind it.
501
600
  - [docs/template-language.md](docs/template-language.md) — the full template
@@ -516,8 +615,13 @@ before the gem is pushed:
516
615
  1. Bump `Cybertrain::VERSION` (`cybertrain/version.rb`) and `version` in
517
616
  `spin.toml` together (CI checks they match); spin caches a git
518
617
  dependency by that version.
519
- 2. Merge to `main`, then tag and push: `git tag v0.1.1 && git push origin v0.1.1`.
520
- 3. `gem build cybertrain.gemspec && gem push cybertrain-0.1.1.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.
521
625
 
522
626
  ## License
523
627
 
@@ -1,5 +1,5 @@
1
1
  # `cybertrain build`: dist/ = the app binary with app/views embedded, plus
2
- # public/. Also the command lists `cybertrain migration` and `server` run. Plain Ruby:
2
+ # public/. Also the command lists `cybertrain db` and `server` run. Plain Ruby:
3
3
  # runs under CRuby (the gem) and compiles under Spinel (spin install).
4
4
  module Cybertrain
5
5
  module CLI
@@ -29,10 +29,28 @@ module Cybertrain
29
29
  ["spin run gen -- --embed-views", "spin build #{name}", "spin run gen"]
30
30
  end
31
31
 
32
- # Through bin/db.rb: the app binary cannot compile before the first
33
- # migration has produced gen/models.
32
+ # `cybertrain db ARGS`, through bin/db.rb (the app binary cannot compile
33
+ # before the first migration has produced gen/models). bin/db.rb only
34
+ # knows the migrations gen/migrations.rb lists, so gen runs first;
35
+ # migrate and rollback rewrite db/schema.rb, which gen/models is
36
+ # derived from, so gen runs again after them.
37
+ def self.db_commands(args)
38
+ words = args.map { |arg| quote_arg(arg) }.join(" ")
39
+ commands = ["spin run gen", "spin run db -- #{words}"]
40
+ commands << "spin run gen" if args[0] == "migrate" || args[0] == "rollback"
41
+ commands
42
+ end
43
+
44
+ # `cybertrain migration`: the same as `cybertrain db migrate`.
34
45
  def self.migration_commands
35
- ["spin run gen", "spin run db -- migrate", "spin run gen"]
46
+ db_commands(["migrate"])
47
+ end
48
+
49
+ # A word sh reads as itself stays bare; anything else is single-quoted.
50
+ def self.quote_arg(text)
51
+ return text if !text.empty? && text.match(/\A[A-Za-z0-9_:.\/-]+\z/)
52
+
53
+ shell_quote(text)
36
54
  end
37
55
 
38
56
  # port is "" (the app's default, 3000) or a port? string.
@@ -1,12 +1,12 @@
1
1
  # `cybertrain new blog`: writes the skeleton of an application package.
2
2
  require "cybertrain/generator/inflector"
3
3
  require "cybertrain/cli/templates"
4
+ require "cybertrain/cli/build"
5
+ require "cybertrain/cli/toolchain"
4
6
 
5
7
  module Cybertrain
6
8
  module CLI
7
9
  module NewApp
8
- KEEP_DIRS = ["db/migrate", "app/models", "app/helpers", "gen", "storage", "tmp", "test"]
9
-
10
10
  # dir is the directory to create ("blog" or "path/to/blog"; its last
11
11
  # component names the package). framework_dep is the TOML value that
12
12
  # spin.toml's `cybertrain =` line gets: `{ path = "/abs/cybertrain" }` or
@@ -47,28 +47,33 @@ module Cybertrain
47
47
 
48
48
  # What `rails new` does with `bundle install`: resolve and lock the
49
49
  # framework (fetching it into spin's cache, ~/.cache/spin), then write
50
- # gen/ so `spin build` works straight away. Without spin on PATH it
51
- # only says what to run later. Returns false when a step failed.
50
+ # gen/ so `spin build` works straight away. Installs Spinel first when
51
+ # this machine has none of the pinned release (Toolchain.ensure!).
52
+ # Returns false when a step failed.
52
53
  def self.bootstrap(dir)
53
- unless system("command -v spin > /dev/null 2>&1")
54
- puts "skip spin lock / spin run gen: `spin` is not on PATH"
55
- puts " install Spinel (https://github.com/matz/spinel), then: #{bootstrap_command(dir)}"
56
- return true
54
+ unless Toolchain.ensure!
55
+ puts "skip spin lock / spin run gen: Spinel #{Cybertrain::SPINEL_TAG} is not available"
56
+ puts " fix the problem above (`cybertrain doctor` lists the checks), then: cybertrain setup && #{recovery_command(dir)}"
57
+ return false
57
58
  end
58
59
 
59
60
  puts "run spin lock && spin run gen"
60
61
  return true if system(bootstrap_command(dir))
61
62
 
62
- puts "error: bootstrapping #{dir} failed; fix the cause, then run: #{bootstrap_command(dir)}"
63
+ puts "error: bootstrapping #{dir} failed; fix the cause, then run: #{recovery_command(dir)}"
63
64
  false
64
65
  end
65
66
 
67
+ # Run by bootstrap once Toolchain.ensure! has put spin on this
68
+ # process's PATH.
66
69
  def self.bootstrap_command(dir)
67
- "cd #{shell_quote(dir)} && spin lock && spin run gen"
70
+ "cd #{Build.shell_quote(dir)} && spin lock && spin run gen"
68
71
  end
69
72
 
70
- def self.shell_quote(text)
71
- "'#{text.gsub("'", "'\\\\''")}'"
73
+ # The same steps for the user to run by hand: through `cybertrain
74
+ # spin`, since the Spinel cybertrain installed is not on their PATH.
75
+ def self.recovery_command(dir)
76
+ "cd #{Build.shell_quote(dir)} && cybertrain spin lock && cybertrain spin run gen"
72
77
  end
73
78
  end
74
79
  end
@@ -92,6 +92,13 @@ module Cybertrain
92
92
  @fields = fields
93
93
  end
94
94
 
95
+ # The index route helper's name without _path, as Rails' scaffold
96
+ # names it: the plural, or "<plural>_index" for a word that is its own
97
+ # plural (sheep_index_path), matching the routes DSL.
98
+ def index_helper
99
+ @plural == @singular ? "#{@plural}_index" : @plural
100
+ end
101
+
95
102
  # The column the model validates for presence ("" when there is none).
96
103
  def first_string_field
97
104
  @fields.each { |f| return f.field_name if f.field_type == "string" }
@@ -92,17 +92,17 @@ module Cybertrain
92
92
 
93
93
  ```sh
94
94
  cybertrain generate scaffold post title:string body:text
95
- cybertrain migration # gen, apply db/migrate, gen again
95
+ cybertrain db migrate # gen, apply db/migrate, gen again
96
96
  cybertrain server # http://127.0.0.1:3000 (`cybertrain server 4000` for another port)
97
97
  cybertrain build # dist/: the binary (views embedded) + public/
98
98
  ```
99
99
 
100
- `spin run gen` (which `migration`, `server` and `build` run for you)
100
+ `spin run gen` (which `db`, `server` and `build` run for you)
101
101
  regenerates `gen/` from db/schema.rb, config/routes.rb and app/;
102
102
  commit `gen/`. In development views under `app/views/` are read at
103
103
  run time: edit them without rebuilding. `cybertrain db status` and
104
104
  `cybertrain db rollback 1` reach the other database commands.
105
- `cybertrain migration` runs migrations through `bin/db.rb`; a
105
+ `cybertrain db migrate` runs migrations through `bin/db.rb`; a
106
106
  deployed `dist/#{package}` runs them with `./#{package} migrate`.
107
107
 
108
108
  To deploy, copy `dist/` to a machine with the same OS and CPU, then
@@ -326,7 +326,7 @@ module Cybertrain
326
326
  def destroy
327
327
  @#{s}.destroy
328
328
  flash[:notice] = "#{human} was successfully destroyed."
329
- redirect_to #{p}_path, status: :see_other
329
+ redirect_to #{res.index_helper}_path, status: :see_other
330
330
  end
331
331
 
332
332
  private
@@ -371,7 +371,7 @@ module Cybertrain
371
371
  end
372
372
  buf << "<p>\n"
373
373
  buf << " <%= link_to \"Edit this #{words}\", edit_#{s}_path(@#{s}) %> |\n"
374
- buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.plural}_path %>\n"
374
+ buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %>\n"
375
375
  buf << "</p>\n\n"
376
376
  buf << "<%= button_to \"Destroy this #{words}\", #{s}_path(@#{s}), method: :delete %>\n"
377
377
  buf
@@ -382,7 +382,7 @@ module Cybertrain
382
382
  buf = +""
383
383
  buf << "<h1>New #{s.tr("_", " ")}</h1>\n\n"
384
384
  buf << "<%= render \"form\", #{s}: @#{s} %>\n\n"
385
- buf << "<p><%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.plural}_path %></p>\n"
385
+ buf << "<p><%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %></p>\n"
386
386
  buf
387
387
  end
388
388
 
@@ -393,7 +393,7 @@ module Cybertrain
393
393
  buf << "<%= render \"form\", #{s}: @#{s} %>\n\n"
394
394
  buf << "<p>\n"
395
395
  buf << " <%= link_to \"Show this #{s.tr("_", " ")}\", #{s}_path(@#{s}) %> |\n"
396
- buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.plural}_path %>\n"
396
+ buf << " <%= link_to \"Back to #{res.plural.tr("_", " ")}\", #{res.index_helper}_path %>\n"
397
397
  buf << "</p>\n"
398
398
  buf
399
399
  end