@nocobase/app-installer 0.0.0-stage → 0.1.0-beta.10
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 +263 -2
- package/bin/run.js +43 -0
- package/dist/cli.js +212 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/install.js +333 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/rollback.js +289 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/status.js +134 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/upgrade.js +598 -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/app-source.js +163 -0
- package/dist/lib/app-source.js.map +1 -0
- package/dist/lib/backup.js +167 -0
- package/dist/lib/backup.js.map +1 -0
- package/dist/lib/checksum-repair.js +76 -0
- package/dist/lib/checksum-repair.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/offline-bundle.js +91 -0
- package/dist/lib/offline-bundle.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/remote.js +331 -0
- package/dist/lib/remote.js.map +1 -0
- package/dist/lib/run-command.js +198 -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 +54 -0
- package/dist/lib/source.js.map +1 -0
- package/dist/lib/state.js +87 -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/dist/remote-entry.js +98 -0
- package/dist/remote-entry.js.map +1 -0
- package/package.json +53 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds `ecosystem.config.cjs`, the pm2 configuration every start goes through.
|
|
3
|
+
*
|
|
4
|
+
* pm2 runs `node launcher.mjs` rather than the application's entry file. The file names nothing that changes: which
|
|
5
|
+
* release runs and with which variables is decided by `launcher.mjs` each time the process starts, so a plain
|
|
6
|
+
* `pm2 restart` picks up a switched `current` and an edited `app.env`.
|
|
7
|
+
*
|
|
8
|
+
* pm2 runs `node` itself, with the script as its argument. Pointing `script` at a module instead has pm2's fork mode
|
|
9
|
+
* load it through `ProcessContainerFork.js`, where `import.meta.main` is false, so `standalone.js` never calls
|
|
10
|
+
* `startServer()` and the application silently never starts.
|
|
11
|
+
*/
|
|
12
|
+
export function buildEcosystemConfig(options) {
|
|
13
|
+
return `// Written by app-installer. Starting the application always goes through this file.
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const root = __dirname;
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
apps: [
|
|
20
|
+
{
|
|
21
|
+
name: ${JSON.stringify(options.name)},
|
|
22
|
+
// Run node itself, so launcher.mjs, and the application after it, are the main module (import.meta.main).
|
|
23
|
+
script: ${JSON.stringify(options.nodePath)},
|
|
24
|
+
// launcher.mjs reads app.env and resolves \`current\` on every start, so \`pm2 restart\` applies changes to either.
|
|
25
|
+
args: [path.join(root, 'launcher.mjs')],
|
|
26
|
+
interpreter: 'none',
|
|
27
|
+
cwd: root,
|
|
28
|
+
autorestart: true,
|
|
29
|
+
min_uptime: '30s',
|
|
30
|
+
max_restarts: 10,
|
|
31
|
+
exp_backoff_restart_delay: 1000,
|
|
32
|
+
// pm2 sends SIGKILL 1.6s after SIGINT by default; the application finishes what it is doing first.
|
|
33
|
+
kill_timeout: 60000,
|
|
34
|
+
out_file: path.join(root, 'logs/app.out.log'),
|
|
35
|
+
error_file: path.join(root, 'logs/app.err.log'),
|
|
36
|
+
time: true,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds `launcher.mjs`, the script pm2 runs. On every start it reads `app.env`, resolves `current`, and replaces itself
|
|
44
|
+
* with `node <release>/dist/server/standalone.js` through `process.execve`.
|
|
45
|
+
*
|
|
46
|
+
* Replacing the process rather than starting a child keeps the application on the pid pm2 watches, receiving pm2's
|
|
47
|
+
* signals itself and running as the main module. A child would need its signals forwarded, and would outlive a
|
|
48
|
+
* launcher that pm2 kills, holding the port the next start needs. Node flags given to the launcher, such as
|
|
49
|
+
* `--max-old-space-size` in `args`, pass on to the application; pm2's `node_args` does not apply to an
|
|
50
|
+
* `interpreter: 'none'` process.
|
|
51
|
+
*
|
|
52
|
+
* pm2 gives the launcher an IPC channel, but the channel does not survive `process.execve`: the application starts
|
|
53
|
+
* without `process.send`, and pm2 options that rely on it, such as `wait_ready` and `shutdown_with_message`, are not
|
|
54
|
+
* available.
|
|
55
|
+
*
|
|
56
|
+
* The entry is resolved to its real path, so a running application keeps loading its modules from the release it
|
|
57
|
+
* started from even after `current` moves on.
|
|
58
|
+
*/
|
|
59
|
+
export function buildLauncher() {
|
|
60
|
+
return `// Written by app-installer. pm2 runs this file; it starts the release \`current\` points at, with app.env applied.
|
|
61
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
62
|
+
import path from 'node:path';
|
|
63
|
+
import { parseEnv } from 'node:util';
|
|
64
|
+
|
|
65
|
+
const root = import.meta.dirname;
|
|
66
|
+
const env = {
|
|
67
|
+
...process.env,
|
|
68
|
+
...parseEnv(readFileSync(path.join(root, 'app.env'), 'utf8')),
|
|
69
|
+
};
|
|
70
|
+
const entry = realpathSync(path.join(root, 'current/dist/server/standalone.js'));
|
|
71
|
+
|
|
72
|
+
// Become the application rather than start it as a child: same pid for pm2, signals delivered directly, and
|
|
73
|
+
// standalone.js is the main module, which is what makes it start the server. Node flags and arguments pass on
|
|
74
|
+
// unchanged.
|
|
75
|
+
process.execve(
|
|
76
|
+
process.execPath,
|
|
77
|
+
[process.execPath, ...process.execArgv, entry, ...process.argv.slice(2)],
|
|
78
|
+
env,
|
|
79
|
+
);
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=ecosystem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecosystem.js","sourceRoot":"","sources":["../../src/lib/ecosystem.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAyB;IAC5D,OAAO;;;;;;;;cAQK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;;gBAE1B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;CAiB/C,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO;;;;;;;;;;;;;;;;;;;;CAoBR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseEnv } from 'node:util';
|
|
3
|
+
/**
|
|
4
|
+
* Builds `app.env`, the one set of runtime variables both the application and the installer's own CLI calls read.
|
|
5
|
+
* `launcher.mjs` reads it on every start, so an edit takes effect with `pm2 restart`.
|
|
6
|
+
*
|
|
7
|
+
* `APP_CONFIG_FILE` and `APP_STORAGE_DIR` are absolute on purpose. A built server treats the directory above `dist/` as
|
|
8
|
+
* its deployment root and keeps `config.yml` and `storage/` there by default, which here is the release directory an
|
|
9
|
+
* upgrade replaces. `dist/.env` carries neither variable, so leaving one out silently moves the application's data into
|
|
10
|
+
* the release.
|
|
11
|
+
*/
|
|
12
|
+
export function buildAppEnv(layout, options) {
|
|
13
|
+
const entries = [
|
|
14
|
+
['NODE_ENV', 'production'],
|
|
15
|
+
...(options.basePath === undefined
|
|
16
|
+
? []
|
|
17
|
+
: [['APP_BASE_PATH', options.basePath]]),
|
|
18
|
+
['APP_CONFIG_FILE', layout.configFile],
|
|
19
|
+
['APP_STORAGE_DIR', layout.storageDir],
|
|
20
|
+
['APP_PUBLIC_ORIGIN', options.origin],
|
|
21
|
+
['APP_SERVER_HOST', options.host],
|
|
22
|
+
['APP_SERVER_PORT', String(options.port)],
|
|
23
|
+
// A failed start exits non-zero, so pm2 restarts it and the installer sees the failure.
|
|
24
|
+
['NOCOBASE_STRICT_STARTUP', 'true'],
|
|
25
|
+
];
|
|
26
|
+
return [
|
|
27
|
+
'# Written by app-installer. Read by launcher.mjs on every start and by every app-installer command.',
|
|
28
|
+
...entries.map(([key, value]) => `${key}=${quote(value)}`),
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|
|
32
|
+
function quote(value) {
|
|
33
|
+
return /^[\w@%+=:,./-]*$/u.test(value) ? value : JSON.stringify(value);
|
|
34
|
+
}
|
|
35
|
+
export async function readAppEnv(layout) {
|
|
36
|
+
return parseEnv(await readFile(layout.appEnv, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
39
|
+
const DEFAULT_PORT = '13000';
|
|
40
|
+
// The server mounts an application that sets no APP_BASE_PATH at `DEFAULT_APP_BASE_PATH` from
|
|
41
|
+
// `@nocobase/app-server/support`. app-installer runs as its own npx tool, without the server's code, so it repeats the
|
|
42
|
+
// value only to tell where such an installation is reached; it never writes it.
|
|
43
|
+
const DEFAULT_BASE_PATH = '/main';
|
|
44
|
+
function parsePort(value) {
|
|
45
|
+
const port = Number(value.trim());
|
|
46
|
+
return value.trim() !== '' &&
|
|
47
|
+
Number.isInteger(port) &&
|
|
48
|
+
port >= 1 &&
|
|
49
|
+
port <= 65535
|
|
50
|
+
? port
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
/** The path the application is mounted at, as a URL prefix: `/crm` stays, and the origin root is `''`. */
|
|
54
|
+
export function mountPathOf(env) {
|
|
55
|
+
return basePrefix(env);
|
|
56
|
+
}
|
|
57
|
+
/** The base path as a URL prefix: `/crm` stays, and the root `/` contributes nothing. */
|
|
58
|
+
function basePrefix(env) {
|
|
59
|
+
const trimmed = (env.APP_BASE_PATH ?? DEFAULT_BASE_PATH)
|
|
60
|
+
.trim()
|
|
61
|
+
.replace(/^\/+|\/+$/gu, '');
|
|
62
|
+
return trimmed ? `/${trimmed}` : '';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Where the application is reached and where it listens, as `app.env` says. The file is edited by hand to change
|
|
66
|
+
* either, so a trailing slash on the origin is dropped, as `install` drops it from `--origin`, and a port that is not
|
|
67
|
+
* one reads as `null` rather than as a number the application is not using.
|
|
68
|
+
*/
|
|
69
|
+
export function endpointsOf(env) {
|
|
70
|
+
const host = env.APP_SERVER_HOST ?? DEFAULT_HOST;
|
|
71
|
+
const rawPort = env.APP_SERVER_PORT ?? DEFAULT_PORT;
|
|
72
|
+
const origin = (env.APP_PUBLIC_ORIGIN ?? `http://${host}:${rawPort}`).replace(/\/+$/u, '');
|
|
73
|
+
return {
|
|
74
|
+
url: `${origin}${basePrefix(env)}/`,
|
|
75
|
+
origin,
|
|
76
|
+
host,
|
|
77
|
+
port: parsePort(rawPort),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** The address the installer checks health on: the listening address, with a wildcard bind reached through loopback. */
|
|
81
|
+
export function healthUrl(env) {
|
|
82
|
+
const { host, port } = endpointsOf(env);
|
|
83
|
+
const reachable = host === '0.0.0.0' || host === '::' || host === '' ? '127.0.0.1' : host;
|
|
84
|
+
const hostPart = reachable.includes(':') ? `[${reachable}]` : reachable;
|
|
85
|
+
return `http://${hostPart}:${port ?? env.APP_SERVER_PORT}${basePrefix(env)}/api/healthz`;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=env-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-file.js","sourceRoot":"","sources":["../../src/lib/env-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAcrC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,OAAsB;IAChE,MAAM,OAAO,GAAuB;QAClC,CAAC,UAAU,EAAE,YAAY,CAAC;QAC1B,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS;YAChC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,CAAC,eAAe,EAAE,OAAO,CAAC,QAAQ,CAAqB,CAAC,CAAC;QAC9D,CAAC,iBAAiB,EAAE,MAAM,CAAC,UAAU,CAAC;QACtC,CAAC,iBAAiB,EAAE,MAAM,CAAC,UAAU,CAAC;QACtC,CAAC,mBAAmB,EAAE,OAAO,CAAC,MAAM,CAAC;QACrC,CAAC,iBAAiB,EAAE,OAAO,CAAC,IAAI,CAAC;QACjC,CAAC,iBAAiB,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACzC,wFAAwF;QACxF,CAAC,yBAAyB,EAAE,MAAM,CAAC;KACpC,CAAC;IACF,OAAO;QACL,qGAAqG;QACrG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1D,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,SAAS,KAAK,CAAC,KAAa;IAC1B,OAAO,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AACzE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAc;IAEd,OAAO,QAAQ,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGpD,CAAC;AACJ,CAAC;AAaD,MAAM,YAAY,GAAG,WAAW,CAAC;AACjC,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,8FAA8F;AAC9F,uHAAuH;AACvH,gFAAgF;AAChF,MAAM,iBAAiB,GAAG,OAAO,CAAC;AAElC,SAAS,SAAS,CAAC,KAAa;IAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QACxB,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC;QACtB,IAAI,IAAI,CAAC;QACT,IAAI,IAAI,KAAK;QACb,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,0GAA0G;AAC1G,MAAM,UAAU,WAAW,CAAC,GAA2B;IACrD,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,yFAAyF;AACzF,SAAS,UAAU,CAAC,GAA2B;IAC7C,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,aAAa,IAAI,iBAAiB,CAAC;SACrD,IAAI,EAAE;SACN,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,GAA2B;IACrD,MAAM,IAAI,GAAG,GAAG,CAAC,eAAe,IAAI,YAAY,CAAC;IACjD,MAAM,OAAO,GAAG,GAAG,CAAC,eAAe,IAAI,YAAY,CAAC;IACpD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,iBAAiB,IAAI,UAAU,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC,OAAO,CAC3E,OAAO,EACP,EAAE,CACH,CAAC;IACF,OAAO;QACL,GAAG,EAAE,GAAG,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG;QACnC,MAAM;QACN,IAAI;QACJ,IAAI,EAAE,SAAS,CAAC,OAAO,CAAC;KACzB,CAAC;AACJ,CAAC;AAED,wHAAwH;AACxH,MAAM,UAAU,SAAS,CAAC,GAA2B;IACnD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,SAAS,GACb,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1E,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,OAAO,UAAU,QAAQ,IAAI,IAAI,IAAI,GAAG,CAAC,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC,cAAc,CAAC;AAC3F,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit codes are part of the contract: CI and agents branch on them.
|
|
3
|
+
*
|
|
4
|
+
* - `0` success, including a run that found nothing to do
|
|
5
|
+
* - `1` the operation failed, and the running application was not touched
|
|
6
|
+
* - `2` invalid usage or a failed precheck, before anything was written
|
|
7
|
+
* - `3` an upgrade failed after the switch and was rolled back; the previous release is running again
|
|
8
|
+
* - `4` rolling back failed too; the application needs a person, and the error says what to do
|
|
9
|
+
*/
|
|
10
|
+
export const EXIT_OK = 0;
|
|
11
|
+
export const EXIT_FAILED = 1;
|
|
12
|
+
export const EXIT_INVALID = 2;
|
|
13
|
+
export const EXIT_ROLLED_BACK = 3;
|
|
14
|
+
export const EXIT_ROLLBACK_FAILED = 4;
|
|
15
|
+
export class InstallerError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
exitCode;
|
|
18
|
+
suggestions;
|
|
19
|
+
details;
|
|
20
|
+
constructor(code, message, options = {}) {
|
|
21
|
+
super(message, { cause: options.cause });
|
|
22
|
+
this.name = 'InstallerError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.exitCode = options.exitCode ?? EXIT_FAILED;
|
|
25
|
+
this.suggestions = options.suggestions ?? [];
|
|
26
|
+
this.details = options.details;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function isInstallerError(error) {
|
|
30
|
+
return error instanceof InstallerError;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC;AACzB,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAClC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAatC,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvB,IAAI,CAAS;IACb,QAAQ,CAAS;IACjB,WAAW,CAAe;IAC1B,OAAO,CAAsC;IAE7D,YACE,IAAY,EACZ,OAAe,EACf,UAAiC,EAAE;QAEnC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,WAAW,CAAC;QAChD,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;QAC7C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;CACF;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,KAAK,YAAY,cAAc,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { interruptError, takeInterrupt } from "./interrupt.js";
|
|
2
|
+
/** One request to the application's health route; true only for a response whose body says `ok: true`. */
|
|
3
|
+
export async function checkHealth(url, fetchImpl = fetch) {
|
|
4
|
+
try {
|
|
5
|
+
const response = await fetchImpl(url, {
|
|
6
|
+
signal: AbortSignal.timeout(3_000),
|
|
7
|
+
});
|
|
8
|
+
if (!response.ok)
|
|
9
|
+
return false;
|
|
10
|
+
const body = (await response.json());
|
|
11
|
+
return body.ok === true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Polls the health route until it answers, the timeout passes, or `failed` says there is no point waiting. The server
|
|
19
|
+
* listens only once startup, migrations included, has finished, so a healthy answer means the application is ready.
|
|
20
|
+
*/
|
|
21
|
+
export async function waitForHealthy(url, options) {
|
|
22
|
+
const now = options.now ?? Date.now;
|
|
23
|
+
const sleep = options.sleep ??
|
|
24
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
25
|
+
const deadline = now() + options.timeoutMs;
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (takeInterrupt())
|
|
28
|
+
throw interruptError();
|
|
29
|
+
if (await checkHealth(url, options.fetchImpl))
|
|
30
|
+
return true;
|
|
31
|
+
if (now() >= deadline)
|
|
32
|
+
return false;
|
|
33
|
+
if (options.failed && (await options.failed()))
|
|
34
|
+
return false;
|
|
35
|
+
await sleep(options.intervalMs ?? 1_000);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Restarts pm2 has made before the application answered, beyond which a start counts as failed.
|
|
40
|
+
* `NOCOBASE_STRICT_STARTUP` makes a failed start exit, so a crash during startup is a real failure rather than a slow
|
|
41
|
+
* start.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_STARTUP_RESTARTS = 2;
|
|
44
|
+
/** A `failed` probe for `waitForHealthy`: the pm2 process has crashed repeatedly, or pm2 has given up on it. */
|
|
45
|
+
export function pm2StartFailed(pm2, name) {
|
|
46
|
+
return async () => {
|
|
47
|
+
const process = await pm2.describe(name).catch(() => undefined);
|
|
48
|
+
if (!process)
|
|
49
|
+
return false;
|
|
50
|
+
return (process.status === 'errored' ||
|
|
51
|
+
process.status === 'stopped' ||
|
|
52
|
+
process.restarts > MAX_STARTUP_RESTARTS);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/lib/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAc/D,0GAA0G;AAC1G,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAW,EACX,YAAuB,KAAK;IAE5B,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE;YACpC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QAC/B,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAqB,CAAC;QACzD,OAAO,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAW,EACX,OAAsB;IAEtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,MAAM,KAAK,GACT,OAAO,CAAC,KAAK;QACb,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IAC3C,SAAS,CAAC;QACR,IAAI,aAAa,EAAE;YAAE,MAAM,cAAc,EAAE,CAAC;QAC5C,IAAI,MAAM,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC;QAC3D,IAAI,GAAG,EAAE,IAAI,QAAQ;YAAE,OAAO,KAAK,CAAC;QACpC,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;QAC7D,MAAM,KAAK,CAAC,OAAO,CAAC,UAAU,IAAI,KAAK,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAE/B,gHAAgH;AAChH,MAAM,UAAU,cAAc,CAAC,GAAQ,EAAE,IAAY;IACnD,OAAO,KAAK,IAAI,EAAE;QAChB,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAChE,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,OAAO,CACL,OAAO,CAAC,MAAM,KAAK,SAAS;YAC5B,OAAO,CAAC,MAAM,KAAK,SAAS;YAC5B,OAAO,CAAC,QAAQ,GAAG,oBAAoB,CACxC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parse } from 'yaml';
|
|
3
|
+
/** The `config.yml` section the application creates its first administrator from. */
|
|
4
|
+
export const INITIAL_ADMIN_KEY = 'users.initialAdmin';
|
|
5
|
+
async function readSection(file) {
|
|
6
|
+
try {
|
|
7
|
+
const document = parse(await readFile(file, 'utf8'), {
|
|
8
|
+
logLevel: 'silent',
|
|
9
|
+
});
|
|
10
|
+
return document?.users?.initialAdmin ?? undefined;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function text(value) {
|
|
17
|
+
return typeof value === 'string' || typeof value === 'number'
|
|
18
|
+
? String(value)
|
|
19
|
+
: null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads the first administrator from `config.yml`, and compares its password with the one in the release's
|
|
23
|
+
* `config.example.yml`, which `config init` copies unless `--set` replaced it. The password itself never leaves here.
|
|
24
|
+
*/
|
|
25
|
+
export async function readInitialAdmin(configFile, exampleFile) {
|
|
26
|
+
const configured = await readSection(configFile);
|
|
27
|
+
const example = await readSection(exampleFile);
|
|
28
|
+
const password = text(configured?.password);
|
|
29
|
+
const examplePassword = text(example?.password);
|
|
30
|
+
return {
|
|
31
|
+
key: INITIAL_ADMIN_KEY,
|
|
32
|
+
username: text(configured?.username),
|
|
33
|
+
email: text(configured?.email),
|
|
34
|
+
defaultPassword: password === null || examplePassword === null
|
|
35
|
+
? null
|
|
36
|
+
: password === examplePassword,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=initial-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"initial-admin.js","sourceRoot":"","sources":["../../src/lib/initial-admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAE7B,qFAAqF;AACrF,MAAM,CAAC,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AAqBtD,KAAK,UAAU,WAAW,CACxB,IAAY;IAEZ,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE;YACnD,QAAQ,EAAE,QAAQ;SACnB,CAA8D,CAAC;QAChE,OAAO,QAAQ,EAAE,KAAK,EAAE,YAAY,IAAI,SAAS,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,IAAI,CAAC,KAAc;IAC1B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ;QAC3D,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QACf,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAAkB,EAClB,WAAmB;IAEnB,MAAM,UAAU,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC5C,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAChD,OAAO;QACL,GAAG,EAAE,iBAAiB;QACtB,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC;QACpC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;QAC9B,eAAe,EACb,QAAQ,KAAK,IAAI,IAAI,eAAe,KAAK,IAAI;YAC3C,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,QAAQ,KAAK,eAAe;KACnC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EXIT_FAILED, InstallerError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* SIGINT and SIGTERM stop the step that is running, not the installer. The step fails with `INTERRUPTED`, and the
|
|
4
|
+
* command's own failure handling runs as it would for any other failure: an install removes what it wrote, an upgrade
|
|
5
|
+
* after the switch rolls back. A second signal exits at once, whatever that leaves behind.
|
|
6
|
+
*/
|
|
7
|
+
let pending = false;
|
|
8
|
+
let received = 0;
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
export function interruptError() {
|
|
11
|
+
return new InstallerError('INTERRUPTED', 'Interrupted; the step that was running was stopped.', { exitCode: EXIT_FAILED });
|
|
12
|
+
}
|
|
13
|
+
/** Reports a pending interrupt once. The step that sees it fails; the recovery steps after it run normally. */
|
|
14
|
+
export function takeInterrupt() {
|
|
15
|
+
if (!pending)
|
|
16
|
+
return false;
|
|
17
|
+
pending = false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
export function throwIfInterrupted() {
|
|
21
|
+
if (takeInterrupt())
|
|
22
|
+
throw interruptError();
|
|
23
|
+
}
|
|
24
|
+
/** Called for each interrupt, so running child processes can be stopped. */
|
|
25
|
+
export function onInterrupt(listener) {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
return () => listeners.delete(listener);
|
|
28
|
+
}
|
|
29
|
+
export function installInterruptHandlers(stderr = process.stderr) {
|
|
30
|
+
const handler = (signal) => {
|
|
31
|
+
received += 1;
|
|
32
|
+
if (received > 1) {
|
|
33
|
+
stderr.write(`\n${signal} again; exiting now.\n`);
|
|
34
|
+
process.exit(130);
|
|
35
|
+
}
|
|
36
|
+
pending = true;
|
|
37
|
+
stderr.write(`\n${signal} received; stopping the current step and cleaning up. Send it again to exit immediately.\n`);
|
|
38
|
+
for (const listener of listeners)
|
|
39
|
+
listener();
|
|
40
|
+
};
|
|
41
|
+
process.on('SIGINT', handler);
|
|
42
|
+
process.on('SIGTERM', handler);
|
|
43
|
+
return () => {
|
|
44
|
+
process.off('SIGINT', handler);
|
|
45
|
+
process.off('SIGTERM', handler);
|
|
46
|
+
pending = false;
|
|
47
|
+
received = 0;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=interrupt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interrupt.js","sourceRoot":"","sources":["../../src/lib/interrupt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;GAIG;AACH,IAAI,OAAO,GAAG,KAAK,CAAC;AACpB,IAAI,QAAQ,GAAG,CAAC,CAAC;AACjB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;AAExC,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,cAAc,CACvB,aAAa,EACb,qDAAqD,EACrD,EAAE,QAAQ,EAAE,WAAW,EAAE,CAC1B,CAAC;AACJ,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,OAAO,GAAG,KAAK,CAAC;IAChB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,IAAI,aAAa,EAAE;QAAE,MAAM,cAAc,EAAE,CAAC;AAC9C,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,QAAoB;IAC9C,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,SAAgC,OAAO,CAAC,MAAM;IAE9C,MAAM,OAAO,GAAG,CAAC,MAAsB,EAAE,EAAE;QACzC,QAAQ,IAAI,CAAC,CAAC;QACd,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACjB,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,wBAAwB,CAAC,CAAC;YAClD,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;QACf,MAAM,CAAC,KAAK,CACV,KAAK,MAAM,4FAA4F,CACxG,CAAC;QACF,KAAK,MAAM,QAAQ,IAAI,SAAS;YAAE,QAAQ,EAAE,CAAC;IAC/C,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC/B,OAAO,GAAG,EAAE;QACV,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAChC,OAAO,GAAG,KAAK,CAAC;QAChB,QAAQ,GAAG,CAAC,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { formatCommandLine } from '@nocobase/cli-envelope';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { defaultRegistry, normalizeRegistry } from "./registry.js";
|
|
4
|
+
export const INSTALLER_PACKAGE = '@nocobase/app-installer';
|
|
5
|
+
/** This installer's own version; `src/lib` and `dist/lib` both sit two levels below the package root. */
|
|
6
|
+
export const INSTALLER_VERSION = createRequire(import.meta.url)('../../package.json').version;
|
|
7
|
+
/**
|
|
8
|
+
* An app-installer command as a suggestion's `run` names it: through npx, because nothing installs an `app-installer`
|
|
9
|
+
* binary on PATH, and with the registry named, because NocoBase 3 packages are not on the public npm registry. `--yes`
|
|
10
|
+
* answers npx's own install prompt, which would otherwise block an agent. The version is pinned to this installer's, so
|
|
11
|
+
* a recovery runs the same code that wrote the state it recovers from.
|
|
12
|
+
*/
|
|
13
|
+
export function installerCommandLine(args, options = {}) {
|
|
14
|
+
const registry = normalizeRegistry(options.registry ?? defaultRegistry());
|
|
15
|
+
const version = options.version ?? INSTALLER_VERSION;
|
|
16
|
+
return {
|
|
17
|
+
command: 'npx',
|
|
18
|
+
args: [
|
|
19
|
+
'--yes',
|
|
20
|
+
`--registry=${registry}`,
|
|
21
|
+
`${INSTALLER_PACKAGE}@${version}`,
|
|
22
|
+
...args,
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The same command written into prose or help, where it is read rather than run: `args` is inserted as given, so it may
|
|
28
|
+
* hold a placeholder such as `<the copied archive>`, and anything a shell would split must already be quoted.
|
|
29
|
+
*/
|
|
30
|
+
export function installerCommand(args, options = {}) {
|
|
31
|
+
return `${formatCommandLine(installerCommandLine([], options))} ${args}`;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=invocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invocation.js","sourceRoot":"","sources":["../../src/lib/invocation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAoB,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAEnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,yBAAyB,CAAC;AAE3D,yGAAyG;AACzG,MAAM,CAAC,MAAM,iBAAiB,GAC5B,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,oBAAoB,CACpD,CAAC,OAAO,CAAC;AASV;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAuB,EACvB,UAAmC,EAAE;IAErC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,QAAQ,IAAI,eAAe,EAAE,CAAC,CAAC;IAC1E,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,iBAAiB,CAAC;IACrD,OAAO;QACL,OAAO,EAAE,KAAK;QACd,IAAI,EAAE;YACJ,OAAO;YACP,cAAc,QAAQ,EAAE;YACxB,GAAG,iBAAiB,IAAI,OAAO,EAAE;YACjC,GAAG,IAAI;SACR;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,UAAmC,EAAE;IAErC,OAAO,GAAG,iBAAiB,CAAC,oBAAoB,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/** The directory inside `releases/<id>/` holding a release's deployment root: `dist/` and `config.example.yml`. */
|
|
3
|
+
export const RELEASE_APP_DIR = 'app';
|
|
4
|
+
export function layoutOf(root) {
|
|
5
|
+
return {
|
|
6
|
+
root,
|
|
7
|
+
appEnv: path.join(root, 'app.env'),
|
|
8
|
+
configFile: path.join(root, 'config.yml'),
|
|
9
|
+
storageDir: path.join(root, 'storage'),
|
|
10
|
+
logsDir: path.join(root, 'logs'),
|
|
11
|
+
outLog: path.join(root, 'logs/app.out.log'),
|
|
12
|
+
errorLog: path.join(root, 'logs/app.err.log'),
|
|
13
|
+
releasesDir: path.join(root, 'releases'),
|
|
14
|
+
backupsDir: path.join(root, 'backups'),
|
|
15
|
+
current: path.join(root, 'current'),
|
|
16
|
+
stateFile: path.join(root, 'installer.json'),
|
|
17
|
+
lockFile: path.join(root, '.installer.lock'),
|
|
18
|
+
ecosystemFile: path.join(root, 'ecosystem.config.cjs'),
|
|
19
|
+
launcherFile: path.join(root, 'launcher.mjs'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A release's identity: its version and when it was built, as in `1.2.0_20260927T005500Z`. Two builds of the same
|
|
24
|
+
* version are two releases, so a project that deploys without bumping its version still gets a release of its own
|
|
25
|
+
* each time. Neither `-`, which would make the id read as a semantic-version prerelease, nor `+`, which is awkward in a
|
|
26
|
+
* directory name and a shell command, separates the parts.
|
|
27
|
+
*/
|
|
28
|
+
export function releaseId(version, builtAt) {
|
|
29
|
+
const compact = new Date(builtAt)
|
|
30
|
+
.toISOString()
|
|
31
|
+
.replace(/\.\d+Z$/u, 'Z')
|
|
32
|
+
.replaceAll('-', '')
|
|
33
|
+
.replaceAll(':', '');
|
|
34
|
+
return `${version}_${compact}`;
|
|
35
|
+
}
|
|
36
|
+
/** A release's deployment root: the directory holding `dist/` and `config.example.yml`. */
|
|
37
|
+
export function releaseDir(layout, id) {
|
|
38
|
+
return path.join(layout.releasesDir, id, RELEASE_APP_DIR);
|
|
39
|
+
}
|
|
40
|
+
/** The path `current` points at, relative to the root so the whole root can be moved. */
|
|
41
|
+
export function releaseLinkTarget(id) {
|
|
42
|
+
return path.join('releases', id, RELEASE_APP_DIR);
|
|
43
|
+
}
|
|
44
|
+
/** Where an archive is unpacked before its manifest names the release it is. */
|
|
45
|
+
export function stagingDir(layout) {
|
|
46
|
+
return path.join(layout.releasesDir, `.staging-${process.pid}`);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/lib/layout.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,mHAAmH;AACnH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAC;AAuBrC,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,OAAO;QACL,IAAI;QACJ,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC;QAClC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC;QACzC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC;QACtC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC;QAChC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAC3C,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAC7C,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC;QACxC,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC;QACtC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC;QACnC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC;QAC5C,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC;QAC5C,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,sBAAsB,CAAC;QACtD,YAAY,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC;KAC9C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,OAAe;IACxD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC;SAC9B,WAAW,EAAE;SACb,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC;SACnB,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACvB,OAAO,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;AACjC,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,EAAU;IACnD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,EAAE,eAAe,CAAC,CAAC;AAC5D,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,iBAAiB,CAAC,EAAU;IAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,EAAE,eAAe,CAAC,CAAC;AACpD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAClE,CAAC"}
|
package/dist/lib/lock.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { link, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { EXIT_INVALID, InstallerError } from "./errors.js";
|
|
3
|
+
function isAlive(pid) {
|
|
4
|
+
try {
|
|
5
|
+
process.kill(pid, 0);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
// EPERM means the process exists but belongs to someone else.
|
|
10
|
+
return error.code === 'EPERM';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Takes the root's lock so two installers never run against the same installation. The file holds the owner's pid; a lock whose
|
|
15
|
+
* owner is gone is stale and is taken over rather than blocking every later run.
|
|
16
|
+
*
|
|
17
|
+
* The pid is written to a private file first and linked into place, so the lock never exists without its owner in it:
|
|
18
|
+
* with `open('wx')` followed by a write, a second installer could read the still-empty file, take it for stale, and
|
|
19
|
+
* proceed alongside the first.
|
|
20
|
+
*/
|
|
21
|
+
export async function acquireLock(file) {
|
|
22
|
+
const candidate = `${file}.${process.pid}`;
|
|
23
|
+
await writeFile(candidate, `${process.pid}\n`);
|
|
24
|
+
try {
|
|
25
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
26
|
+
try {
|
|
27
|
+
await link(candidate, file);
|
|
28
|
+
return async () => {
|
|
29
|
+
await rm(file, { force: true });
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== 'EEXIST')
|
|
34
|
+
throw error;
|
|
35
|
+
const owner = Number.parseInt((await readFile(file, 'utf8').catch(() => '')).trim(), 10);
|
|
36
|
+
if (Number.isInteger(owner) &&
|
|
37
|
+
owner !== process.pid &&
|
|
38
|
+
isAlive(owner)) {
|
|
39
|
+
throw new InstallerError('LOCKED', `Another app-installer (pid ${owner}) is working on this installation.`, {
|
|
40
|
+
exitCode: EXIT_INVALID,
|
|
41
|
+
suggestions: [
|
|
42
|
+
{
|
|
43
|
+
message: 'Wait for it to finish, then run the command again.',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
await rm(file, { force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new InstallerError('LOCKED', `Could not take the lock at ${file}.`, {
|
|
52
|
+
exitCode: EXIT_INVALID,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await rm(candidate, { force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/lib/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAI3D,SAAS,OAAO,CAAC,GAAW;IAC1B,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,8DAA8D;QAC9D,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC3D,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY;IAC5C,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3C,MAAM,SAAS,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;gBAC5B,OAAO,KAAK,IAAI,EAAE;oBAChB,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBAClC,CAAC,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,MAAM,KAAK,CAAC;gBACpE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAC3B,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EACrD,EAAE,CACH,CAAC;gBACF,IACE,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;oBACvB,KAAK,KAAK,OAAO,CAAC,GAAG;oBACrB,OAAO,CAAC,KAAK,CAAC,EACd,CAAC;oBACD,MAAM,IAAI,cAAc,CACtB,QAAQ,EACR,8BAA8B,KAAK,oCAAoC,EACvE;wBACE,QAAQ,EAAE,YAAY;wBACtB,WAAW,EAAE;4BACX;gCACE,OAAO,EAAE,oDAAoD;6BAC9D;yBACF;qBACF,CACF,CAAC;gBACJ,CAAC;gBACD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;QACD,MAAM,IAAI,cAAc,CAAC,QAAQ,EAAE,8BAA8B,IAAI,GAAG,EAAE;YACxE,QAAQ,EAAE,YAAY;SACvB,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EXIT_INVALID, InstallerError } from "./errors.js";
|
|
2
|
+
import { rebuildCommandLine } from "./prechecks.js";
|
|
3
|
+
/** A mount path as the server spells it: `/crm`, `/apps/crm`, or `''` for the origin root. */
|
|
4
|
+
export function normalizeMountPath(value) {
|
|
5
|
+
const trimmed = value.trim().replace(/^\/+|\/+$/gu, '');
|
|
6
|
+
return trimmed ? `/${trimmed}` : '';
|
|
7
|
+
}
|
|
8
|
+
/** `--base-path`, checked and normalized; `undefined` when it was not given. */
|
|
9
|
+
export function parseBasePathFlag(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (/[\s?#\\]/u.test(value) || value.split('/').includes('..')) {
|
|
13
|
+
throw new InstallerError('INVALID_USAGE', `--base-path ${JSON.stringify(value)} is not a URL path; give one such as /crm, or / for the origin root.`, { exitCode: EXIT_INVALID });
|
|
14
|
+
}
|
|
15
|
+
return normalizeMountPath(value);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A release built before relocatable builds has its mount path compiled into its client, and serves pages whose asset
|
|
19
|
+
* URLs all miss anywhere else. It runs only where it was built for.
|
|
20
|
+
*/
|
|
21
|
+
export function assertFixedMountPath(subject, builtFor, mountedAt) {
|
|
22
|
+
if (normalizeMountPath(builtFor) === normalizeMountPath(mountedAt))
|
|
23
|
+
return;
|
|
24
|
+
throw new InstallerError('BASE_PATH_MISMATCH', `${subject} was built for the base path ${normalizeMountPath(builtFor) || '/'} and cannot be mounted at ${normalizeMountPath(mountedAt) || '/'}.`, {
|
|
25
|
+
exitCode: EXIT_INVALID,
|
|
26
|
+
suggestions: [
|
|
27
|
+
{
|
|
28
|
+
message: 'Upgrade @nocobase/app-cli in the application project and build the archive again; a current build runs at any path:',
|
|
29
|
+
run: rebuildCommandLine(),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=mount-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount-path.js","sourceRoot":"","sources":["../../src/lib/mount-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpD,8FAA8F;AAC9F,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IACxD,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AACtC,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAC/B,KAAyB;IAEzB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,cAAc,CACtB,eAAe,EACf,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,sEAAsE,EAC1G,EAAE,QAAQ,EAAE,YAAY,EAAE,CAC3B,CAAC;IACJ,CAAC;IACD,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,QAAgB,EAChB,SAAiB;IAEjB,IAAI,kBAAkB,CAAC,QAAQ,CAAC,KAAK,kBAAkB,CAAC,SAAS,CAAC;QAAE,OAAO;IAC3E,MAAM,IAAI,cAAc,CACtB,oBAAoB,EACpB,GAAG,OAAO,gCAAgC,kBAAkB,CAAC,QAAQ,CAAC,IAAI,GAAG,6BAA6B,kBAAkB,CAAC,SAAS,CAAC,IAAI,GAAG,GAAG,EACjJ;QACE,QAAQ,EAAE,YAAY;QACtB,WAAW,EAAE;YACX;gBACE,OAAO,EACL,qHAAqH;gBACvH,GAAG,EAAE,kBAAkB,EAAE;aAC1B;SACF;KACF,CACF,CAAC;AACJ,CAAC"}
|