Skip to content

Commit 986e68a

Browse files
wheels-bot[bot]github-actions[bot]bpamiriclaudePeter Amiri
authored
fix(cli): reload and packages add no longer claim onApplicationStart won't re-fire (#3126)
* fix(cli): reload and packages add no longer claim onApplicationStart won't re-fire An authorized ?reload=true&password=... calls applicationStop(), so the next request re-fires onApplicationStart in full — config/services.cfm and the PackageLoader ($loadPackages) all re-run (verified live on Lucee 7, #3110). The reload command's note and the packages-install activation line claimed the opposite, telling users to run `wheels stop && wheels start`. Correct both: the reload note now describes the real full-restart behavior and surfaces the password-resolution caveat (a missing/wrong password silently skips the restart, #3059 / #3062); the install output now says `wheels reload` (or restart) activates the package. Tutorial/guide passages making the same claim are handled separately by bot-update-docs.yml. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): correct reload contract — wheels reload re-fires onApplicationStart Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Peter Amiri <peter@alurium.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Peter Amiri <petera@pai.com>
1 parent df62f46 commit 986e68a

9 files changed

Lines changed: 54 additions & 26 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `wheels reload` and `wheels packages add` no longer claim that `?reload=true` skips `onApplicationStart`. An authorized reload calls `applicationStop()`, so the next request re-fires `onApplicationStart` in full (re-running `config/services.cfm` and the package loader) — the CLI now says a reload activates an installed package and notes that only a missing/wrong reload password silently skips the restart (#3110)

cli/lucli/Module.cfc

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -899,12 +899,15 @@ component extends="modules.BaseModule" {
899899
}
900900

901901
out("Application reloaded successfully.", "green");
902-
// Surface the hot-vs-cold reload contract — Wheels does NOT
903-
// re-fire onApplicationStart on `?reload=true`. Users editing
904-
// app/events/onapplicationstart.cfm or config/services.cfm need
905-
// a full restart. See finding #8 in the 2026-04-29 fresh-VM
906-
// triage.
907-
out("Note: onApplicationStart does NOT re-fire. For init-code edits, run `wheels stop && wheels start`.", "cyan");
902+
// Surface the actual reload contract (verified live on Lucee 7,
903+
// see #3110): an authorized `?reload=true&password=...` calls
904+
// applicationStop(), so the next request re-fires onApplicationStart
905+
// in full — app/events/onapplicationstart.cfm, config/services.cfm,
906+
// and the PackageLoader all re-run. Caveat: the restart only
907+
// happens when the reload password resolves; a missing or wrong
908+
// password silently serves the request without restarting
909+
// (#3059 / #3062).
910+
out("Note: an authorized reload re-fires onApplicationStart (re-runs config/services.cfm and the package loader). A missing or wrong reload password silently skips the restart.", "cyan");
908911
verbose("URL: http://localhost:#serverPort#/?reload=true&password=***");
909912
return "";
910913
}

cli/lucli/services/packages/PackagesMainCli.cfc

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,15 @@ component {
208208
local.manifest = variables.registry.fetchManifest(arguments.name);
209209
local.picked = variables.resolver.pick(local.manifest, variables.runtime, arguments.pin);
210210
local.vendor = variables.installer.install(arguments.name, local.picked, arguments.force);
211-
// Activation requires a cold restart, not a soft reload: PackageLoader
212-
// runs in onApplicationStart and `wheels reload` doesn't re-fire that
213-
// hook. Telling users to `wheels reload` here directly contradicts
214-
// the chapter-8 caveat and produces "uiCard not defined"-style errors
215-
// when the helper resolution fails. Onboarding F14.
211+
// Activation only needs a reload, not a cold restart: an authorized
212+
// `wheels reload` calls applicationStop(), so the next request re-fires
213+
// onApplicationStart — which runs the PackageLoader ($loadPackages).
214+
// Verified live on Lucee 7 (see #3110). A full `wheels stop && wheels
215+
// start` also works but is no longer required. Caveat: a reload only
216+
// restarts when its password resolves; a missing/wrong password
217+
// silently skips the restart (#3059 / #3062).
216218
return "Installed " & arguments.name & "@" & local.picked.version & "" & local.vendor & Chr(10)
217-
& "Run `wheels stop && wheels start` to activate it." & Chr(10);
219+
& "Run `wheels reload` (or restart) to activate it." & Chr(10);
218220
}
219221

220222
private string function $updateAll(struct opts) {

cli/lucli/tests/specs/commands/ReloadCommandSpec.cfc

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
* a spec (see finding #8 in
66
* docs/superpowers/plans/2026-04-29-fresh-vm-onboarding-findings.md).
77
*
8+
* Contract (verified live on Lucee 7, see #3110): an authorized
9+
* `?reload=true&password=...` calls applicationStop(), so the next request
10+
* re-fires onApplicationStart in full — config/services.cfm and the
11+
* PackageLoader re-run. The earlier "does NOT re-fire" note was wrong; it
12+
* stemmed from reloads whose password never resolved (a missing/wrong
13+
* password silently skips the restart, see #3059 / #3062).
14+
*
815
* The #3059 blocks cover reporting honesty: reload() used to print
916
* "Application reloaded successfully." whenever the HTTP exchange completed,
1017
* never inspecting the status code. The framework's reload gate restarts the
@@ -60,10 +67,11 @@ component extends="wheels.wheelstest.system.BaseSpec" {
6067

6168
describe("wheels reload — output hints", () => {
6269

63-
it("emits a note that onApplicationStart does NOT re-fire on a hot reload", () => {
70+
it("emits a note that an authorized reload re-fires onApplicationStart", () => {
6471
var moduleSource = fileRead(expandPath("/cli/lucli/Module.cfc"));
65-
expect(moduleSource).toInclude("onApplicationStart does NOT re-fire");
66-
expect(moduleSource).toInclude("wheels stop && wheels start");
72+
expect(moduleSource).toInclude("re-fires onApplicationStart");
73+
// The old, false claim must be gone.
74+
expect(moduleSource).notToInclude("onApplicationStart does NOT re-fire");
6775
});
6876

6977
it("honors an explicit --password override before falling back to auto-detect", () => {

cli/lucli/tests/specs/packages/PackagesMainCliSpec.cfc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ component extends="wheels.wheelstest.system.BaseSpec" {
119119
DirectoryDelete(proj, true);
120120
});
121121

122+
it("install tells the user a reload activates the package (reload re-fires onApplicationStart)", () => {
123+
// An authorized `wheels reload` calls applicationStop(), so the
124+
// next request re-fires onApplicationStart — which runs the
125+
// PackageLoader. A cold restart is no longer required to
126+
// activate an installed package (see #3110).
127+
var proj = $scratch();
128+
var stack = $buildStack(proj);
129+
var out = stack.cli.install({target: "wheels-fake"});
130+
expect(out).toInclude("wheels reload");
131+
expect(out).notToInclude("wheels stop && wheels start");
132+
stack.cache.refresh();
133+
DirectoryDelete(proj, true);
134+
});
135+
122136
it("install honours @version pin", () => {
123137
var proj = $scratch();
124138
var stack = $buildStack(proj);

web/sites/guides/src/content/docs/v4-0-0/command-line-tools/commands/packages/install.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ sidebar:
77
order: 4
88
---
99

10-
Adds a package into `vendor/<name>/` from the registry. To activate it, do a cold restart (`wheels stop && wheels start`) — `PackageLoader` discovers the new `vendor/<name>/package.json` at startup. A `wheels reload` is not enough to pick up a newly-installed package.
10+
Adds a package into `vendor/<name>/` from the registry. To activate it, run `wheels reload` (or `wheels stop && wheels start`) — `PackageLoader` discovers the new `vendor/<name>/package.json` when `onApplicationStart` re-fires. An authorized `wheels reload` is sufficient; a missing or wrong reload password silently skips the restart.
1111

1212
:::note[Why `add`, not `install`?]
1313
LuCLI's built-in extension installer intercepts the literal subcommand `install` across every module. Typing `wheels packages install <name>` runs LuCLI's own dependency resolver — it prints `No git or extension dependencies to install` and exits without touching `vendor/`. Use `add` instead. (The same rename happened to `wheels browser setup`, which was previously `wheels browser install`.)
@@ -51,9 +51,9 @@ The same `SemVer` matcher that `PackageLoader` uses at runtime.
5151
```sh title="illustrative — terminal session"
5252
$ wheels packages add wheels-sentry
5353
Installed wheels-sentry@1.0.0 → /Users/me/app/vendor/wheels-sentry
54-
Restart the server (`wheels stop && wheels start`) to activate it.
54+
Run `wheels reload` (or restart) to activate it.
5555

5656
$ wheels packages add wheels-sentry@1.0.0 --force
5757
Installed wheels-sentry@1.0.0 → /Users/me/app/vendor/wheels-sentry
58-
Restart the server (`wheels stop && wheels start`) to activate it.
58+
Run `wheels reload` (or restart) to activate it.
5959
```

web/sites/guides/src/content/docs/v4-0-0/digging-deeper/packages.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Install a package:
3636

3737
```bash title="your shell"
3838
wheels packages add wheels-hotwire
39-
wheels stop && wheels start
39+
wheels reload
4040
```
4141

4242
Browse, search, inspect, update, or remove:
@@ -57,7 +57,7 @@ Deactivation is a single command — `remove` deletes the `vendor/<name>/` direc
5757
The registry caches its index for 24 hours. If you want to see a just-published version immediately, `wheels packages registry refresh` busts the cache. `wheels packages registry info` shows the configured registry URL and cache state.
5858
</Aside>
5959

60-
The restart re-runs `PackageLoader` at startup, which rediscovers what's in `vendor/` and reconciles the mixin, service, and middleware tables with the new state. A plain `wheels reload` is not enough — `PackageLoader` only re-scans `vendor/` on a cold start.
60+
The reload re-runs `PackageLoader`, which rediscovers what's in `vendor/` and reconciles the mixin, service, and middleware tables with the new state. An authorized `wheels reload` calls `applicationStop()` so the next request re-fires `onApplicationStart` in full — `wheels stop && wheels start` also works. Caveat: a missing or wrong reload password silently skips the restart.
6161

6262
## First-party packages
6363

@@ -156,7 +156,7 @@ component output="false" {
156156
}
157157
```
158158

159-
After installing the package into `vendor/myfeature/` and restarting the server (`wheels stop && wheels start`), every controller has `myHelper()` available. The framework collects public methods from the package instance and merges them into the application mixin tables — the same machinery that runs for plugins, but scoped to the targets you named in the manifest.
159+
After installing the package into `vendor/myfeature/` and reloading (`wheels reload` or `wheels stop && wheels start`), every controller has `myHelper()` available. The framework collects public methods from the package instance and merges them into the application mixin tables — the same machinery that runs for plugins, but scoped to the targets you named in the manifest.
160160

161161
Lifecycle hook names the loader specifically skips when collecting mixins: `init`, `onPluginLoad`, `onPluginActivate`, `register`, `boot`. If you name a method any of those, it won't be mixed in — the loader treats them as package infrastructure.
162162

web/sites/guides/src/content/docs/v4-0-0/start-here/tutorial/06-authentication.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ if (StructKeyExists(application, "wheelsdi") && application.wheelsdi.containsIns
557557

558558
Don't put this in `config/app.cfm` — that file is for `Application.cfc` `this`-scope settings (`this.name`, `this.datasources`, `this.sessionTimeout`, etc.), not for init code. The DI container isn't initialized when `config/app.cfm` runs, so `application.wheelsdi` doesn't exist yet and the registration silently no-ops.
559559

560-
Both `wheels reload` and a full `wheels stop && wheels start` re-fire `onApplicationStart` — an authorized reload stops the application and the next request boots it fresh, re-running this file and `config/services.cfm`. Either way, this registers the strategy exactly once, and the `hasStrategy` check keeps repeated restarts from stacking duplicates. (Some CLI messages still claim `wheels reload` skips `onApplicationStart`; that's outdated — tracked in [#3110](https://github.com/wheels-dev/wheels/issues/3110).)
560+
Both `wheels reload` and a full `wheels stop && wheels start` re-fire `onApplicationStart` — an authorized reload stops the application and the next request boots it fresh, re-running this file and `config/services.cfm`. Either way, this registers the strategy exactly once, and the `hasStrategy` check keeps repeated restarts from stacking duplicates. (CLI versions predating the [#3110](https://github.com/wheels-dev/wheels/issues/3110) fix printed a note claiming `wheels reload` skips `onApplicationStart`; that note was wrong.)
561561

562562
### Rewrite the Sessions controller
563563

@@ -744,7 +744,7 @@ Whether you stopped at 6a or continued to 6b, the user-facing behavior should be
744744

745745
**"The password always mismatches, even for a user I just created."** The `beforeValidation` callback isn't firing, or the form is storing plaintext. Open the database and look at the `users` row directly: `passwordHash` should be exactly 64 uppercase hex characters, `passwordSalt` should be populated with a ~44-char base64 string. If either is blank, check that you named the callback correctly in `config()` (`beforeValidation("hashPassword")` — case-sensitive) and that the form submits `user[password]`, not just `password`.
746746

747-
**"6b: `authenticator` service not found."** Three things to check, in this order. **(1) Does the file have a `<cfscript>` wrapper?** Without it, Lucee treats the body as markup — you'll see the bare `local.di = injector();` lines printed at the top of every page after a cold restart, and the registration code never runs. Compare against `config/settings.cfm` if unsure of the shape. **(2) Did the application actually restart?** Both `wheels reload` and `wheels stop && wheels start` re-fire `onApplicationStart` and re-run `config/services.cfm` — but a reload with a wrong or missing reload password silently skips the restart. If in doubt, `wheels stop && wheels start` removes that variable. (CLI messages claiming `wheels reload` never re-fires `onApplicationStart` are outdated — see [#3110](https://github.com/wheels-dev/wheels/issues/3110).) **(3) Component path typo?** The `.to(...)` argument must be the exact dotted path — `wheels.auth.Authenticator`, not `Authenticator` — and the `injector()` call has to come first.
747+
**"6b: `authenticator` service not found."** Three things to check, in this order. **(1) Does the file have a `<cfscript>` wrapper?** Without it, Lucee treats the body as markup — you'll see the bare `local.di = injector();` lines printed at the top of every page after a cold restart, and the registration code never runs. Compare against `config/settings.cfm` if unsure of the shape. **(2) Did the application actually restart?** Both `wheels reload` and `wheels stop && wheels start` re-fire `onApplicationStart` and re-run `config/services.cfm` — but a reload with a wrong or missing reload password silently skips the restart. If in doubt, `wheels stop && wheels start` removes that variable. (CLI messages claiming `wheels reload` never re-fires `onApplicationStart` predate the [#3110](https://github.com/wheels-dev/wheels/issues/3110) fix.) **(3) Component path typo?** The `.to(...)` argument must be the exact dotted path — `wheels.auth.Authenticator`, not `Authenticator` — and the `injector()` call has to come first.
748748

749749
## What's next
750750

web/sites/guides/src/content/docs/v4-0-0/start-here/tutorial/08-bonus-basecoat.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The canonical way to install a Wheels package is `wheels packages add <name>`. T
6666

6767
```text title="expected output"
6868
Installed wheels-basecoat@<version> → /your/path/blog/vendor/wheels-basecoat
69-
Run `wheels stop && wheels start` to activate it.
69+
Run `wheels reload` (or restart) to activate it.
7070
```
7171

7272
2. Verify the install:
@@ -83,7 +83,7 @@ The canonical way to install a Wheels package is `wheels packages add <name>`. T
8383
wheels reload
8484
```
8585

86-
`wheels reload` works because an authorized reload stops and restarts the application, re-running `onApplicationStart` — and with it the package loader. A full `wheels stop && wheels start` (which the install message suggests) does the same thing, just more heavily. (The CLI message claiming only a stop/start activates a package is outdated — tracked in [#3110](https://github.com/wheels-dev/wheels/issues/3110).)
86+
`wheels reload` works because an authorized reload stops and restarts the application, re-running `onApplicationStart` — and with it the package loader. A full `wheels stop && wheels start` (as the install message also hints) does the same thing, just more heavily. (CLI versions predating the [#3110](https://github.com/wheels-dev/wheels/issues/3110) fix claimed only a stop/start activates a package; that claim was wrong.)
8787

8888
</Steps>
8989

@@ -296,7 +296,7 @@ Three things to verify:
296296

297297
**`No matching function [UIBUTTON] found`** — the package didn't activate. Both `wheels reload` and `wheels stop && wheels start` re-run `PackageLoader` (an authorized reload restarts the application, re-firing `onApplicationStart`), so the usual cause is a reload that silently no-op'd — a wrong or missing reload password — or the package landing somewhere other than `vendor/`. Run `wheels stop && wheels start` to rule out the password variable, and `ls vendor/wheels-basecoat` to confirm the files are there.
298298

299-
**`No matching function [$UIBUILDID] found`** (or `[$UILUCIDEICON]`) — the package activated but its internal helpers can't be found. Fixed in `wheels-basecoat 1.0.3`. Older versions declared the `$`-prefixed helpers `private`, but Wheels' `PackageLoader` only carries PUBLIC methods across the mixin boundary, so public callers like `uiField` couldn't reach them. Run `wheels packages update wheels-basecoat --yes && wheels stop && wheels start`.
299+
**`No matching function [$UIBUILDID] found`** (or `[$UILUCIDEICON]`) — the package activated but its internal helpers can't be found. Fixed in `wheels-basecoat 1.0.3`. Older versions declared the `$`-prefixed helpers `private`, but Wheels' `PackageLoader` only carries PUBLIC methods across the mixin boundary, so public callers like `uiField` couldn't reach them. Run `wheels packages update wheels-basecoat --yes` then `wheels reload` (or restart).
300300

301301
**`No version of 'wheels-basecoat' satisfies runtime '0.0.0-dev'`** — the framework can't read its own version. Likely on a Wheels release older than `4.0.0-SNAPSHOT+1670` (the snapshot that includes the runtime-detection fix). Upgrade with `brew upgrade wheels` (macOS/Linux) or `scoop update wheels` (Windows) and re-run.
302302

0 commit comments

Comments
 (0)