@nocobase/app-installer 0.0.0-stage → 0.1.0-beta.4
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.
- package/README.md +199 -2
- package/bin/run.js +43 -0
- package/dist/cli.js +168 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/install.js +319 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/rollback.js +284 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/status.js +132 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/upgrade.js +549 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/lib/app-cli.js +89 -0
- package/dist/lib/app-cli.js.map +1 -0
- package/dist/lib/backup.js +167 -0
- package/dist/lib/backup.js.map +1 -0
- package/dist/lib/confirm.js +36 -0
- package/dist/lib/confirm.js.map +1 -0
- package/dist/lib/current-link.js +22 -0
- package/dist/lib/current-link.js.map +1 -0
- package/dist/lib/ecosystem.js +82 -0
- package/dist/lib/ecosystem.js.map +1 -0
- package/dist/lib/env-file.js +87 -0
- package/dist/lib/env-file.js.map +1 -0
- package/dist/lib/errors.js +32 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/health.js +55 -0
- package/dist/lib/health.js.map +1 -0
- package/dist/lib/initial-admin.js +39 -0
- package/dist/lib/initial-admin.js.map +1 -0
- package/dist/lib/interrupt.js +50 -0
- package/dist/lib/interrupt.js.map +1 -0
- package/dist/lib/invocation.js +33 -0
- package/dist/lib/invocation.js.map +1 -0
- package/dist/lib/layout.js +48 -0
- package/dist/lib/layout.js.map +1 -0
- package/dist/lib/lock.js +59 -0
- package/dist/lib/lock.js.map +1 -0
- package/dist/lib/mount-path.js +34 -0
- package/dist/lib/mount-path.js.map +1 -0
- package/dist/lib/output.js +61 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/pm2.js +92 -0
- package/dist/lib/pm2.js.map +1 -0
- package/dist/lib/prechecks.js +199 -0
- package/dist/lib/prechecks.js.map +1 -0
- package/dist/lib/registry.js +9 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/release.js +173 -0
- package/dist/lib/release.js.map +1 -0
- package/dist/lib/run-command.js +108 -0
- package/dist/lib/run-command.js.map +1 -0
- package/dist/lib/service.js +100 -0
- package/dist/lib/service.js.map +1 -0
- package/dist/lib/source.js +44 -0
- package/dist/lib/source.js.map +1 -0
- package/dist/lib/state.js +84 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/version.js +51 -0
- package/dist/lib/version.js.map +1 -0
- package/package.json +51 -3
package/README.md
CHANGED
|
@@ -1,3 +1,200 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nocobase/app-installer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installs, upgrades and rolls back a NocoBase 3 application on a server, and runs it under pm2. It installs a deployment archive you build in the application project with `pnpm build --tar`: the server unpacks it and runs it, and needs neither the sources nor pnpm.
|
|
4
|
+
|
|
5
|
+
Each installation lives in a directory of its own, with its own port and pm2 process, so one server can run several. Docker remains the other way to run an application.
|
|
6
|
+
|
|
7
|
+
Earlier versions could also build the published Hub template on the server (`--template hub`). That source is gone: an installation recorded as built from a template is refused with `STATE_UNSUPPORTED`, and stays manageable with the app-installer version that installed it.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Linux or macOS. On Windows, use WSL: switching releases relies on symbolic links and atomic renames.
|
|
12
|
+
- Node.js 24 or later.
|
|
13
|
+
- pm2 4.3 or later, installed globally (`npm install -g pm2`), to start the application. A copy fetched through `npx` does not work, because `pm2 startup` writes a boot service that names pm2's own path.
|
|
14
|
+
|
|
15
|
+
NocoBase 3 packages are published to the public npm registry. The commands below use `--yes` to skip npx's own install prompt. `--registry` or `NOCOBASE_REGISTRY` selects a private registry when needed; the installer records that registry in `installer.json`, so later commands for an existing installation keep using its original source.
|
|
16
|
+
|
|
17
|
+
## Building an archive
|
|
18
|
+
|
|
19
|
+
In the application project, build for the server rather than for the machine you build on, and copy the archive over:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm build --target linux-x64 --node-version 24 --tar
|
|
23
|
+
scp storage/exports/dist.tar.gz user@server:/tmp/crm.tar.gz
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`--target` and `--node-version` must match the server: native modules are compiled for one platform, architecture, C library and Node major, and the installer refuses an archive built for another. The build is not tied to a mount path: the installation chooses it with `--base-path`. A database other than SQLite needs its driver in the project before the build (`pnpm add @nocobase/db-postgres`), since an archive carries the drivers it was built with. The archive records in `dist/package.json` that it is relocatable, and when it was built. An archive from an `@nocobase/app-cli` that predates relocatable builds records the base path it was compiled for instead, and runs only at that path; one older still records neither, and is refused until the project upgrades it and builds again.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx --yes @nocobase/app-installer install /srv/nocobase/crm --archive /tmp/crm.tar.gz --origin https://apps.example.com
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The target must be new or empty. Before writing anything, the command also checks that the port is free — naming a free one when it is not — that no pm2 process already uses the name (`pm2 start` on a taken name would restart that process with this installation's configuration rather than start a new one), and that every variable named by `--set-from-env` is set. It then unpacks the archive into a release directory; writes the configuration; applies the database migrations; and starts the application with pm2, waiting until its health check answers or pm2 reports the process as crashed. Anything it wrote is removed again if it fails before the application is switched on, so running it again starts clean.
|
|
35
|
+
|
|
36
|
+
Ctrl-C (or SIGTERM) stops the step that is running and lets that cleanup happen; a second one exits at once.
|
|
37
|
+
|
|
38
|
+
| Flag | Default | Purpose |
|
|
39
|
+
| ------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `--dir` | | The target, as the other commands name it; the same as the directory argument. |
|
|
41
|
+
| `--archive` | | The deployment archive to install, by local path. Required. |
|
|
42
|
+
| `--origin` | `http://HOST:PORT` | Public origin without the base path. Set it before exposing the application. |
|
|
43
|
+
| `--base-path` | the server's, `/main` | Where the application is mounted, such as `/crm`, or `/` for the origin root. Written to `app.env`. |
|
|
44
|
+
| `--host`, `--port` | `127.0.0.1`, `13000` | Where the application listens. Keep the loopback default behind a reverse proxy; give each installation its own port. |
|
|
45
|
+
| `--dialect` | `sqlite` | Database. Anything else needs `--set`; the archive must carry `@nocobase/db-<dialect>`. |
|
|
46
|
+
| `--set` | | `key=value` passed to `nocobase config set`, repeatable. Values are YAML scalars. |
|
|
47
|
+
| `--set-from-env` | | `key=VARIABLE` read from the environment, repeatable. Use it for passwords. |
|
|
48
|
+
| `--registry` | `https://registry.npmjs.org` | Registry the suggested installer commands use; `NOCOBASE_REGISTRY` also sets it. |
|
|
49
|
+
| `--name` | `nocobase-<directory name>` | pm2 process name. |
|
|
50
|
+
| `--no-start` | | Install without starting; the result names the command that starts it. |
|
|
51
|
+
| `--health-timeout` | `180` | Seconds to wait for the health check. |
|
|
52
|
+
| `--json` | | Print one JSON result on stdout. Progress always goes to stderr. |
|
|
53
|
+
|
|
54
|
+
A PostgreSQL installation, with the password taken from the environment:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
CRM_DB_PASSWORD=... npx --yes @nocobase/app-installer install /srv/nocobase/crm \
|
|
58
|
+
--archive /tmp/crm.tar.gz --origin https://apps.example.com --dialect postgres \
|
|
59
|
+
--set database.connections.main.host=db.internal \
|
|
60
|
+
--set database.connections.main.username=crm \
|
|
61
|
+
--set-from-env database.connections.main.password=CRM_DB_PASSWORD
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`--set` values are read as YAML scalars by `nocobase config set`: `0123` becomes the number `123` and `no` becomes `false`, so quote text that looks like either, as in `--set 'key="0123"'`. Values are masked in the installer's own output, but a secret passed with `--set` is still visible to other users in the process list while the command runs; pass secrets with `--set-from-env`.
|
|
65
|
+
|
|
66
|
+
Afterwards, run `pm2 startup` once and execute the command it prints so pm2 restarts the application after a reboot, and proxy the origin to the application's port with `location /`. Several installations behind one origin are told apart by their base paths. The first sign-in comes from `users.initialAdmin` in `config.yml`. The result's `initialAdmin` names that account's username and email, and `defaultPassword` says whether its password is still the one `config.example.yml` ships (`admin123`), to be changed after signing in; the password itself is never printed.
|
|
67
|
+
|
|
68
|
+
## Upgrade
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx --yes @nocobase/app-installer upgrade --dir /srv/nocobase/crm --archive /tmp/crm.tar.gz
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
An upgrade moves to the archive given with `--archive`, which must hold the same application (its package name owns the migration history) and not an older version; an archive from before relocatable builds must also have been built for the path `app.env` mounts.
|
|
75
|
+
|
|
76
|
+
Every build is a release of its own, named by its version and UTC build time, as in `0.3.0_20260927T005500Z`, so deploying a project without bumping its version is still an upgrade. An archive already on disk is reused, and the one already running changes nothing.
|
|
77
|
+
|
|
78
|
+
Everything that takes time happens while the current release keeps serving: the new release is unpacked beside it, then checked with its own CLI — `config check`, and `db apply --dry-run` to count the pending migrations. Only then does the downtime start: the application is stopped, its SQLite databases, `config.yml` and `app.env` are copied to `backups/<time>_<from>_to_<to>/`, `current` is switched, the migrations are applied, and the new release is started and must pass its health check.
|
|
79
|
+
|
|
80
|
+
If migrating or starting the new release fails, the upgrade rolls itself back: `current` returns to the previous release, the databases are restored from the backup when the new release may have migrated them, and the previous release is started again. The command then exits with code `3`. If the previous release does not come back either, it exits with `4`, keeps the new release on disk, and leaves the operation pending, so `rollback` finishes the job once the cause is fixed.
|
|
81
|
+
|
|
82
|
+
A version older than the running one is refused: the older release knows nothing of the newer migrations. Going back is what `rollback` is for. The pm2 process named in `installer.json` must belong to this installation, and once the application is stopped its port must be free, so the health check cannot be answered by anything but the new release.
|
|
83
|
+
|
|
84
|
+
| Flag | Default | Purpose |
|
|
85
|
+
| ------------------ | ------- | --------------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `--archive` | | The new archive, by local path. Required. |
|
|
87
|
+
| `--backup-done` | | Required when `config.yml` names a database the installer cannot back up — any but SQLite: back it up yourself. |
|
|
88
|
+
| `--keep` | `3` | Releases to keep on disk (at least 2). The new release and the one upgraded from are always kept. |
|
|
89
|
+
| `--health-timeout` | `180` | Seconds to wait for the new release's health check. |
|
|
90
|
+
| `--yes` | | Proceed without the confirmation prompt; required with `--json` or without a terminal. |
|
|
91
|
+
| `--json` | | Print one JSON result on stdout. |
|
|
92
|
+
|
|
93
|
+
The backup covers what rolling back needs: every SQLite database `config.yml` declares, with its journals, and the configuration. It is not a replacement for regular backups of `storage/`, whose uploaded files it leaves out, and backups are not pruned: remove old ones from `backups/` yourself.
|
|
94
|
+
|
|
95
|
+
When the machine's Node major changes, a release's native modules no longer load, and `status` says so. Build the archive again with the new `--node-version` and upgrade to it: the new build is a release of its own, switched to through the same steps and the same backup as any upgrade. The release it replaces stays on disk but cannot be rolled back to, since it was built for another Node.
|
|
96
|
+
|
|
97
|
+
## Rollback
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx --yes @nocobase/app-installer rollback --dir /srv/nocobase/crm
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Returns to the release the last upgrade came from, or to `--to`, which takes a release id or a version (the newest build of it) among the releases on disk. When the upgrade being undone applied migrations, the SQLite databases are restored from the backup taken before it, which discards whatever was written since; `--no-restore` keeps the current databases instead. An external database is never in a backup, so nothing is restored for it and the command says so: restore it from your own backup. Rolling back never runs migrations backwards. A release built for another Node major is refused, since its native modules would not load; the error says how to get one that does.
|
|
104
|
+
|
|
105
|
+
If the release it returns to does not start, the rollback stays pending, and running `rollback` again retries it — restore included — once the cause is fixed.
|
|
106
|
+
|
|
107
|
+
If an upgrade or rollback is interrupted while the application is down, `installer.json` records it: `status` warns, `upgrade` refuses to start, and `rollback` recovers. It undoes an interrupted upgrade — restoring the databases only when the upgrade had already switched releases, since before that nothing was migrated — and finishes an interrupted rollback.
|
|
108
|
+
|
|
109
|
+
## Status
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx --yes @nocobase/app-installer status --dir /srv/nocobase/crm
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Reports the application, its source, the running release with its version and build time, its `endpoints` — the public URL and origin, and the host and port it listens on — whether it answers its health check, the pm2 process, the releases on disk and their size, and whether the machine's Node major still matches the one the release was built for. It changes nothing, and does not start the pm2 daemon when it is not running.
|
|
116
|
+
|
|
117
|
+
## Changing the origin or port
|
|
118
|
+
|
|
119
|
+
`app.env` holds where the application is reached and where it listens: `APP_PUBLIC_ORIGIN`, `APP_BASE_PATH`, `APP_SERVER_HOST` and `APP_SERVER_PORT`. Edit them there and restart the process under its pm2 name, `nocobase-<directory name>` unless `--name` chose another:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pm2 restart nocobase-crm
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`launcher.mjs` reads `app.env` every time the process starts, so nothing needs registering again. A new port must be free, and a reverse proxy has to forward to it. Every app-installer command reads the same file, so `status` and the next `upgrade` check health at the new address. A new `APP_BASE_PATH` takes effect the same way, since a current build is not tied to a mount path; the reverse proxy has to route the new path. A release from before relocatable builds runs only at the path it was built for, so `upgrade` and `rollback` refuse one that does not match `app.env`.
|
|
126
|
+
|
|
127
|
+
## Layout
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
/srv/nocobase/crm/
|
|
131
|
+
├── app.env runtime variables, read by pm2 and by every app-installer command
|
|
132
|
+
├── config.yml written by `nocobase config init`, shared by every release
|
|
133
|
+
├── storage/ APP_STORAGE_DIR: databases, uploaded files and anything else the application keeps
|
|
134
|
+
├── logs/ app.out.log and app.err.log, collected by pm2
|
|
135
|
+
├── ecosystem.config.cjs the pm2 configuration every start goes through
|
|
136
|
+
├── launcher.mjs what pm2 runs: reads app.env and starts the release current points at
|
|
137
|
+
├── releases/<version>_<time>/app/ dist/ and config.example.yml from the deployment archive
|
|
138
|
+
├── current -> releases/<version>_<time>/app
|
|
139
|
+
├── backups/ databases and configuration copied before each upgrade
|
|
140
|
+
└── installer.json what the installer knows: application, base path, source, releases, history
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`APP_CONFIG_FILE` and `APP_STORAGE_DIR` in `app.env` are absolute. A built server treats the directory above `dist/` as its deployment root and would otherwise keep its configuration and data inside the release directory; a release too old to read `APP_STORAGE_DIR` does exactly that, and the installer refuses it.
|
|
144
|
+
|
|
145
|
+
pm2 runs `node launcher.mjs` (`interpreter: 'none'`), and on every start `launcher.mjs` reads `app.env`, resolves `current` and replaces itself with `node <release>/dist/server/standalone.js` through `process.execve`. The application therefore keeps the pid pm2 watches, receives pm2's signals directly, and runs as the main module; pointing pm2's `script` at `standalone.js` instead would load it through pm2's own wrapper, where `import.meta.main` is false, and the application would never start. Because nothing about the release or its variables is fixed when pm2 registers the process, a plain `pm2 restart` applies a switched `current` or an edited `app.env`. `install` writes both files once; edits to them are kept. Node flags for the application, such as `--max-old-space-size=4096`, go in `args` before `launcher.mjs`, which passes them on, or in `NODE_OPTIONS` in `app.env`; pm2's `node_args` does not apply to an `interpreter: 'none'` process. The launcher's IPC channel from pm2 does not survive `process.execve`, so `wait_ready` and `shutdown_with_message` are not available.
|
|
146
|
+
|
|
147
|
+
## Exit codes
|
|
148
|
+
|
|
149
|
+
| Code | Meaning |
|
|
150
|
+
| ---- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| `0` | Success. |
|
|
152
|
+
| `1` | The operation failed. An application that was already running was not touched. |
|
|
153
|
+
| `2` | Invalid usage, a failed precheck — including an unsupported Node.js — or a declined confirmation; nothing was written. |
|
|
154
|
+
| `3` | An upgrade failed after the switch and was rolled back; the previous release is running. |
|
|
155
|
+
| `4` | Rolling back failed as well; the application is down and the error lists what to do. |
|
|
156
|
+
|
|
157
|
+
## Error codes
|
|
158
|
+
|
|
159
|
+
Under `--json`, a failure prints `ok: false` and `status: "failure"` with `error.code`, a message, `error.suggestions` and, where there is any, `error.details`: the same envelope as the application CLI's `pnpm nocobase … --json`. A suggestion's `run`, where it has one, is `{ command, args }`, an executable and its arguments to run as given, without a shell; a step that takes two commands is two suggestions. Codes are stable; branch on them rather than on the message.
|
|
160
|
+
|
|
161
|
+
| Code | Exit | Meaning and what to do |
|
|
162
|
+
| ----------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `INVALID_USAGE` | `2` | An unknown command or flag, a malformed `--set`, an `--origin` with a path, no install directory, no `--archive`, or an `--archive` URL. Fix the command line. |
|
|
164
|
+
| `NODE_UNSUPPORTED` | `2` | Node.js is older than 24. Install Node.js 24 or later. |
|
|
165
|
+
| `PLATFORM_UNSUPPORTED` | `2` | Windows. Run the installer inside WSL. |
|
|
166
|
+
| `PM2_MISSING` | `2` | pm2 is not on PATH. Install it globally, or install with `--no-start`. |
|
|
167
|
+
| `PM2_UNSUPPORTED` | `2` | pm2 is older than 4.3, which does not read `ecosystem.config.cjs`, or `pm2 --version` printed no version. Update it globally, then run `pm2 update`. |
|
|
168
|
+
| `PM2_NAME_IN_USE` | `2` | Another process owns the pm2 name. Choose another with `--name` when installing; for an installation, stop or rename that process. |
|
|
169
|
+
| `PORT_IN_USE` | `2` | Something already listens on the host and port. When installing, `details.freePort` and the suggestion name a free port; otherwise stop what holds it. |
|
|
170
|
+
| `ENV_MISSING` | `2` | A variable named by `--set-from-env` is not set. |
|
|
171
|
+
| `TARGET_NOT_EMPTY` | `2` | The install target holds files, or is a file. Install into a new or empty directory; an installation is managed with the other commands. |
|
|
172
|
+
| `NOT_INSTALLED` | `2` | The directory holds no `installer.json`. Run from the installation root, or name it with `--dir`. |
|
|
173
|
+
| `STATE_UNSUPPORTED` | `2` | `installer.json` was written by a newer app-installer, or records an installation built from a published template, which only the app-installer that installed it manages. |
|
|
174
|
+
| `LOCKED` | `2` | Another app-installer is working on this installation. Wait for it to finish. |
|
|
175
|
+
| `ARCHIVE_NOT_FOUND` | `2` | `--archive` names no file. |
|
|
176
|
+
| `ARCHIVE_INVALID` | `2` | The archive holds no `dist/package.json`: it is not one `pnpm build --tar` wrote. |
|
|
177
|
+
| `CONFIG_UNREADABLE` | `2` | `config.yml` could not be read as YAML, so the databases an upgrade would have to back up are unknown. Fix the file; nothing was changed. |
|
|
178
|
+
| `ARCHIVE_TOO_OLD` | `2` | The archive records neither that it is relocatable nor its base path, or not its build time. Upgrade `@nocobase/app-cli` in the project and build it again. |
|
|
179
|
+
| `DRIVER_MISSING` | `2` | The archive lacks `@nocobase/db-<dialect>` for the chosen dialect. Add it to the project and build again. |
|
|
180
|
+
| `APP_MISMATCH` | `2` | The archive holds another application than the one installed. Install it into a directory of its own. |
|
|
181
|
+
| `BASE_PATH_MISMATCH` | `2` | A release from before relocatable builds was built for another path than `--base-path` or `app.env` names. Build it again with a current `@nocobase/app-cli`, which runs at any path. |
|
|
182
|
+
| `CONFIRMATION_REQUIRED` | `2` | `upgrade` or `rollback` needs consent, and `--json` or the lack of a terminal leaves no prompt to ask on. `details.notes` says what the operation does; pass `--yes` once that is accepted. |
|
|
183
|
+
| `CANCELLED` | `2` | The confirmation prompt was declined. Nothing changed. |
|
|
184
|
+
| `OPERATION_INTERRUPTED` | `2` | An earlier upgrade or rollback stopped while the application was down. Run `rollback` to recover before anything else. |
|
|
185
|
+
| `DOWNGRADE` | `2` | The target is an older version than the running one. Use `rollback --to`. |
|
|
186
|
+
| `BACKUP_REQUIRED` | `2` | `config.yml` names a database the installer cannot back up. Back it up, then pass `--backup-done`. |
|
|
187
|
+
| `NOTHING_TO_ROLL_BACK` | `2` | No upgrade led to the current release. Name a release on disk with `--to`. |
|
|
188
|
+
| `RELEASE_MISSING` | `2` | The release to return to is not on disk; `status` lists those that are. |
|
|
189
|
+
| `NODE_MISMATCH` | `2` | The release was built for another Node major, so its native modules would not load; the suggestions say how to get one that does. |
|
|
190
|
+
| `BUILD_TARGET_MISMATCH` | `2` | The archive was built for another platform or Node major than this machine; the suggestion names the build command that fits. |
|
|
191
|
+
| `UNPACK_FAILED` | `1` | Unpacking the deployment archive failed. |
|
|
192
|
+
| `STORAGE_IN_RELEASE` | `1` | The release wrote its data inside its own directory: its `@nocobase/app-server` predates `APP_STORAGE_DIR`. Upgrade it in the project and build again; an upgrade rolls back on it. |
|
|
193
|
+
| `APP_CLI_FAILED` | `1` | The release's own CLI printed no result. A failure it does report keeps its own code instead, such as `CONFIG_INVALID` from `config check`. |
|
|
194
|
+
| `START_FAILED` | `1` | The application is installed but did not pass its health check; `details.log` holds the end of its error log. Fix the cause and start it with the suggested command. |
|
|
195
|
+
| `UPGRADE_ABORTED` | `1` | The upgrade stopped before switching releases and the previous release is running again; exit `4` when it did not come back. |
|
|
196
|
+
| `ROLLBACK_ABORTED` | `1` | Stopping the application failed; nothing was restored or switched. |
|
|
197
|
+
| `INTERRUPTED` | `1` | Ctrl-C or SIGTERM stopped the running step before the application was touched. An install removes what it wrote unless the application was already switched on; an upgrade removes the release it was unpacking. Once an upgrade or rollback has stopped the application, it reports its own code instead. |
|
|
198
|
+
| `UNEXPECTED` | `1` | An error the installer does not recognise. The message is all there is; report it. |
|
|
199
|
+
| `UPGRADE_ROLLED_BACK` | `3` | The new release failed after the switch; the previous one runs again, on the restored databases when the new one may have migrated them. |
|
|
200
|
+
| `ROLLBACK_FAILED` | `4` | The application is down. Follow the suggestions: read the log, fix the cause, and run `rollback` again, which retries the restore. |
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import {
|
|
7
|
+
exitWhenFlushed,
|
|
8
|
+
isSupportedNodeVersion,
|
|
9
|
+
unsupportedNodeVersionOutput,
|
|
10
|
+
} from '@nocobase/cli-envelope/node-guard';
|
|
11
|
+
|
|
12
|
+
if (!isSupportedNodeVersion()) {
|
|
13
|
+
// The document names the command as typed: the arguments before the first flag, as the application CLI's does.
|
|
14
|
+
const { stream, text } = unsupportedNodeVersionOutput({
|
|
15
|
+
name: 'app-installer',
|
|
16
|
+
argv: process.argv.slice(2),
|
|
17
|
+
});
|
|
18
|
+
process[stream].write(`${text}\n`);
|
|
19
|
+
await exitWhenFlushed(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Development runs straight from `src`, which Node 24 strips types from. A published install lives in `node_modules`,
|
|
26
|
+
* where Node refuses to strip types, so it runs the compiled `dist` instead. `files` ships only `bin` and `dist`, which
|
|
27
|
+
* makes the presence of `src` a reliable signal for which mode this is.
|
|
28
|
+
*/
|
|
29
|
+
const srcEntry = path.join(root, 'src/cli.ts');
|
|
30
|
+
const useDist =
|
|
31
|
+
process.env.NOCOBASE_APP_INSTALLER_USE_DIST === '1' || !existsSync(srcEntry);
|
|
32
|
+
const entry = useDist ? '../dist/cli.js' : '../src/cli.ts';
|
|
33
|
+
|
|
34
|
+
const pjson = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
35
|
+
|
|
36
|
+
const { runInstaller } = await import(entry);
|
|
37
|
+
|
|
38
|
+
const exitCode = await runInstaller({
|
|
39
|
+
argv: process.argv.slice(2),
|
|
40
|
+
version: pjson.version,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await exitWhenFlushed(exitCode);
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { parse } from '@oclif/core/parser';
|
|
2
|
+
import { INSTALL_ARGS, INSTALL_FLAGS, install, } from "./commands/install.js";
|
|
3
|
+
import { ROLLBACK_FLAGS, rollback, } from "./commands/rollback.js";
|
|
4
|
+
import { STATUS_FLAGS, status } from "./commands/status.js";
|
|
5
|
+
import { UPGRADE_FLAGS, upgrade, } from "./commands/upgrade.js";
|
|
6
|
+
import { EXIT_INVALID, EXIT_OK, InstallerError } from "./lib/errors.js";
|
|
7
|
+
import { installInterruptHandlers } from "./lib/interrupt.js";
|
|
8
|
+
import { installerCommand, installerCommandLine } from "./lib/invocation.js";
|
|
9
|
+
import { MINIMUM_PM2_VERSION } from "./lib/prechecks.js";
|
|
10
|
+
import { createReporter, errorEnvelope, exitCodeOf, formatError, successEnvelope, } from "./lib/output.js";
|
|
11
|
+
import { createPm2 } from "./lib/pm2.js";
|
|
12
|
+
function describeFlags(flags) {
|
|
13
|
+
return Object.entries(flags).map(([name, flag]) => {
|
|
14
|
+
const label = flag.allowNo ? `--[no-]${name}` : `--${name}`;
|
|
15
|
+
const value = flag.default;
|
|
16
|
+
const fallback = typeof value === 'string' || typeof value === 'number' || value === true
|
|
17
|
+
? ` (default: ${String(value)})`
|
|
18
|
+
: '';
|
|
19
|
+
return ` ${label.padEnd(20)} ${flag.description ?? ''}${fallback}`;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** Every usage line and example is a command that runs as-is, the same way error suggestions are. */
|
|
23
|
+
export function formatHelp() {
|
|
24
|
+
return [
|
|
25
|
+
'Install and manage a NocoBase 3 application on a server, from a deployment archive.',
|
|
26
|
+
'',
|
|
27
|
+
'USAGE',
|
|
28
|
+
` $ ${installerCommand('COMMAND [FLAGS]')}`,
|
|
29
|
+
'',
|
|
30
|
+
'COMMANDS',
|
|
31
|
+
' install DIRECTORY Install into DIRECTORY (or --dir DIRECTORY) from --archive, and start it with pm2.',
|
|
32
|
+
' upgrade Upgrade the installation in --dir, the current directory by default, to a new --archive.',
|
|
33
|
+
' rollback Return to the release the last upgrade came from, or to --to.',
|
|
34
|
+
' status Report the release, endpoints, health, pm2 process and releases on disk.',
|
|
35
|
+
'',
|
|
36
|
+
'INSTALL FLAGS',
|
|
37
|
+
...describeFlags(INSTALL_FLAGS),
|
|
38
|
+
'',
|
|
39
|
+
'UPGRADE FLAGS',
|
|
40
|
+
...describeFlags(UPGRADE_FLAGS),
|
|
41
|
+
'',
|
|
42
|
+
'ROLLBACK FLAGS',
|
|
43
|
+
...describeFlags(ROLLBACK_FLAGS),
|
|
44
|
+
'',
|
|
45
|
+
'STATUS FLAGS',
|
|
46
|
+
...describeFlags(STATUS_FLAGS),
|
|
47
|
+
'',
|
|
48
|
+
'EXAMPLES',
|
|
49
|
+
` $ ${installerCommand('install /srv/nocobase/crm --archive /tmp/crm.tar.gz --origin https://apps.example.com')}`,
|
|
50
|
+
` $ ${installerCommand('upgrade --dir /srv/nocobase/crm --archive /tmp/crm.tar.gz --yes')}`,
|
|
51
|
+
` $ ${installerCommand('install /srv/nocobase/crm --archive /tmp/crm.tar.gz --dialect postgres --set database.connections.main.host=db.internal --set-from-env database.connections.main.password=CRM_DB_PASSWORD')}`,
|
|
52
|
+
` $ ${installerCommand('rollback --dir /srv/nocobase/crm')}`,
|
|
53
|
+
` $ ${installerCommand('status --dir /srv/nocobase/crm --json')}`,
|
|
54
|
+
'',
|
|
55
|
+
'NOTES',
|
|
56
|
+
` Requires Node.js 24+ and, to start the application, pm2 ${MINIMUM_PM2_VERSION}+ installed globally.`,
|
|
57
|
+
' Build an archive in the application project with pnpm build --target <platform> --node-version <major> --tar, matching this machine.',
|
|
58
|
+
' Suggested installer commands use https://registry.npmjs.org by default; override with --registry or NOCOBASE_REGISTRY.',
|
|
59
|
+
' To change the origin or port, edit app.env in the installation root, then run pm2 restart with the process name.',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
async function parseCommand(argv, definition) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = await parse(argv, {
|
|
65
|
+
args: definition.args,
|
|
66
|
+
flags: definition.flags,
|
|
67
|
+
strict: true,
|
|
68
|
+
});
|
|
69
|
+
return { ...parsed.args, flags: parsed.flags };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// oclif spreads one problem over several lines ("Missing 1 required arg:" then the argument); keep the first two.
|
|
73
|
+
const message = error instanceof Error
|
|
74
|
+
? error.message
|
|
75
|
+
.split('\n')
|
|
76
|
+
.map((line) => line.trim())
|
|
77
|
+
.filter((line) => line !== '' && !line.startsWith('See more help'))
|
|
78
|
+
.slice(0, 2)
|
|
79
|
+
.join(' ')
|
|
80
|
+
: String(error);
|
|
81
|
+
throw new InstallerError('INVALID_USAGE', message, {
|
|
82
|
+
exitCode: EXIT_INVALID,
|
|
83
|
+
cause: error,
|
|
84
|
+
suggestions: [
|
|
85
|
+
{ message: 'See the usage:', run: installerCommandLine(['--help']) },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function runInstaller(options) {
|
|
91
|
+
const stdout = options.stdout ?? process.stdout;
|
|
92
|
+
const stderr = options.stderr ?? process.stderr;
|
|
93
|
+
const [command, ...rest] = options.argv;
|
|
94
|
+
const json = options.argv.includes('--json');
|
|
95
|
+
const reporter = createReporter(json, stderr);
|
|
96
|
+
if (command === undefined ||
|
|
97
|
+
command === '--help' ||
|
|
98
|
+
command === '-h' ||
|
|
99
|
+
command === 'help') {
|
|
100
|
+
stdout.write(`${formatHelp()}\n`);
|
|
101
|
+
return EXIT_OK;
|
|
102
|
+
}
|
|
103
|
+
if (command === '--version') {
|
|
104
|
+
stdout.write(json
|
|
105
|
+
? `${JSON.stringify(successEnvelope(command, { version: options.version }, []))}\n`
|
|
106
|
+
: `${options.version}\n`);
|
|
107
|
+
return EXIT_OK;
|
|
108
|
+
}
|
|
109
|
+
// `install --help` asks for help, not for a flag the command does not define.
|
|
110
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
111
|
+
stdout.write(`${formatHelp()}\n`);
|
|
112
|
+
return EXIT_OK;
|
|
113
|
+
}
|
|
114
|
+
const deps = {
|
|
115
|
+
reporter,
|
|
116
|
+
pm2: options.pm2 ?? createPm2('pm2', options.run),
|
|
117
|
+
fetchImpl: options.fetchImpl,
|
|
118
|
+
cwd: options.cwd,
|
|
119
|
+
run: options.run,
|
|
120
|
+
};
|
|
121
|
+
const removeInterruptHandlers = installInterruptHandlers(stderr);
|
|
122
|
+
try {
|
|
123
|
+
let outcome;
|
|
124
|
+
if (command === 'install') {
|
|
125
|
+
outcome = await install(await parseCommand(rest, {
|
|
126
|
+
args: INSTALL_ARGS,
|
|
127
|
+
flags: INSTALL_FLAGS,
|
|
128
|
+
}), deps);
|
|
129
|
+
}
|
|
130
|
+
else if (command === 'upgrade') {
|
|
131
|
+
outcome = await upgrade(await parseCommand(rest, { flags: UPGRADE_FLAGS }), deps);
|
|
132
|
+
}
|
|
133
|
+
else if (command === 'rollback') {
|
|
134
|
+
outcome = await rollback(await parseCommand(rest, { flags: ROLLBACK_FLAGS }), deps);
|
|
135
|
+
}
|
|
136
|
+
else if (command === 'status') {
|
|
137
|
+
outcome = await status(await parseCommand(rest, { flags: STATUS_FLAGS }), deps);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw new InstallerError('INVALID_USAGE', `Unknown command "${command}".`, {
|
|
141
|
+
exitCode: EXIT_INVALID,
|
|
142
|
+
suggestions: [
|
|
143
|
+
{
|
|
144
|
+
message: 'See the usage:',
|
|
145
|
+
run: installerCommandLine(['--help']),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
stdout.write(json
|
|
151
|
+
? `${JSON.stringify(successEnvelope(command, outcome.result, reporter.warnings, outcome.status))}\n`
|
|
152
|
+
: `${outcome.summary.join('\n')}\n`);
|
|
153
|
+
return EXIT_OK;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (json) {
|
|
157
|
+
stdout.write(`${JSON.stringify(errorEnvelope(command, error, reporter.warnings))}\n`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
stderr.write(`${formatError(error)}\n`);
|
|
161
|
+
}
|
|
162
|
+
return exitCodeOf(error);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
removeInterruptHandlers();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|