@nocobase/app-installer 0.0.0-stage → 0.1.0-beta.3
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 +217 -2
- package/bin/run.js +43 -0
- package/dist/cli.js +171 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/install.js +383 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/rollback.js +287 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/status.js +155 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/upgrade.js +709 -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 +86 -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 +61 -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 +236 -0
- package/dist/lib/prechecks.js.map +1 -0
- package/dist/lib/registry.js +59 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/release.js +328 -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 +100 -0
- package/dist/lib/source.js.map +1 -0
- package/dist/lib/state.js +73 -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
|
@@ -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,61 @@
|
|
|
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 const HUB_TEMPLATE = {
|
|
5
|
+
name: 'hub',
|
|
6
|
+
title: 'Hub',
|
|
7
|
+
package: '@nocobase/app-template-hub',
|
|
8
|
+
projectName: 'hub',
|
|
9
|
+
basePath: '/hub',
|
|
10
|
+
};
|
|
11
|
+
export const TEMPLATES = [HUB_TEMPLATE];
|
|
12
|
+
export function layoutOf(root) {
|
|
13
|
+
return {
|
|
14
|
+
root,
|
|
15
|
+
appEnv: path.join(root, 'app.env'),
|
|
16
|
+
configFile: path.join(root, 'config.yml'),
|
|
17
|
+
storageDir: path.join(root, 'storage'),
|
|
18
|
+
logsDir: path.join(root, 'logs'),
|
|
19
|
+
outLog: path.join(root, 'logs/app.out.log'),
|
|
20
|
+
errorLog: path.join(root, 'logs/app.err.log'),
|
|
21
|
+
releasesDir: path.join(root, 'releases'),
|
|
22
|
+
buildDir: path.join(root, '.build'),
|
|
23
|
+
backupsDir: path.join(root, 'backups'),
|
|
24
|
+
current: path.join(root, 'current'),
|
|
25
|
+
stateFile: path.join(root, 'installer.json'),
|
|
26
|
+
lockFile: path.join(root, '.installer.lock'),
|
|
27
|
+
ecosystemFile: path.join(root, 'ecosystem.config.cjs'),
|
|
28
|
+
launcherFile: path.join(root, 'launcher.mjs'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A release's identity: its version and when it was built, as in `1.2.0_20260927T005500Z`. Two builds of the same
|
|
33
|
+
* version are two releases, so a project that deploys without bumping its version still gets a release of its own
|
|
34
|
+
* each time. Neither `-`, which would make the id read as a semantic-version prerelease, nor `+`, which is awkward in a
|
|
35
|
+
* directory name and a shell command, separates the parts.
|
|
36
|
+
*/
|
|
37
|
+
export function releaseId(version, builtAt) {
|
|
38
|
+
const compact = new Date(builtAt)
|
|
39
|
+
.toISOString()
|
|
40
|
+
.replace(/\.\d+Z$/u, 'Z')
|
|
41
|
+
.replaceAll('-', '')
|
|
42
|
+
.replaceAll(':', '');
|
|
43
|
+
return `${version}_${compact}`;
|
|
44
|
+
}
|
|
45
|
+
/** A release's deployment root: the directory holding `dist/` and `config.example.yml`. */
|
|
46
|
+
export function releaseDir(layout, id) {
|
|
47
|
+
return path.join(layout.releasesDir, id, RELEASE_APP_DIR);
|
|
48
|
+
}
|
|
49
|
+
/** The path `current` points at, relative to the root so the whole root can be moved. */
|
|
50
|
+
export function releaseLinkTarget(id) {
|
|
51
|
+
return path.join('releases', id, RELEASE_APP_DIR);
|
|
52
|
+
}
|
|
53
|
+
/** Where a template build runs, one directory per version. */
|
|
54
|
+
export function buildRoot(layout, version) {
|
|
55
|
+
return path.join(layout.buildDir, version);
|
|
56
|
+
}
|
|
57
|
+
/** Where an archive is unpacked before its manifest names the release it is. */
|
|
58
|
+
export function stagingDir(layout) {
|
|
59
|
+
return path.join(layout.releasesDir, `.staging-${process.pid}`);
|
|
60
|
+
}
|
|
61
|
+
//# 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;AAyBrC,MAAM,CAAC,MAAM,YAAY,GAAuB;IAC9C,IAAI,EAAE,KAAK;IACX,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE,4BAA4B;IACrC,WAAW,EAAE,KAAK;IAClB,QAAQ,EAAE,MAAM;CACjB,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAkC,CAAC,YAAY,CAAC,CAAC;AAwBvE,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,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC;QACnC,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,8DAA8D;AAC9D,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,OAAe;IACvD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAC7C,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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { commandFailureJson, commandSuccessJson, formatCommandLine, } from '@nocobase/cli-envelope';
|
|
2
|
+
import { EXIT_FAILED, isInstallerError } from "./errors.js";
|
|
3
|
+
export function createReporter(json, stderr = process.stderr) {
|
|
4
|
+
const warnings = [];
|
|
5
|
+
return {
|
|
6
|
+
json,
|
|
7
|
+
warnings,
|
|
8
|
+
progress(message) {
|
|
9
|
+
stderr.write(`${message}\n`);
|
|
10
|
+
},
|
|
11
|
+
warn(message) {
|
|
12
|
+
warnings.push(message);
|
|
13
|
+
stderr.write(`Warning: ${message}\n`);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function successEnvelope(command, result, warnings, status = 'success') {
|
|
18
|
+
return commandSuccessJson(command, status, result, warnings);
|
|
19
|
+
}
|
|
20
|
+
/** An `InstallerError` as it was raised; anything else as `UNEXPECTED`, with only its message. */
|
|
21
|
+
export function errorEnvelope(command, error, warnings) {
|
|
22
|
+
if (isInstallerError(error)) {
|
|
23
|
+
return commandFailureJson(command, {
|
|
24
|
+
code: error.code,
|
|
25
|
+
message: error.message,
|
|
26
|
+
suggestions: error.suggestions,
|
|
27
|
+
details: error.details,
|
|
28
|
+
}, warnings);
|
|
29
|
+
}
|
|
30
|
+
return commandFailureJson(command, {
|
|
31
|
+
code: 'UNEXPECTED',
|
|
32
|
+
message: error instanceof Error ? error.message : String(error),
|
|
33
|
+
suggestions: [],
|
|
34
|
+
}, warnings);
|
|
35
|
+
}
|
|
36
|
+
export function exitCodeOf(error) {
|
|
37
|
+
return isInstallerError(error) ? error.exitCode : EXIT_FAILED;
|
|
38
|
+
}
|
|
39
|
+
/** Renders an error for a person: the message, then each suggestion with the command to run under it. */
|
|
40
|
+
export function formatError(error) {
|
|
41
|
+
const envelope = errorEnvelope('', error, []);
|
|
42
|
+
if (envelope.ok)
|
|
43
|
+
return '';
|
|
44
|
+
const lines = [`Error: ${envelope.error.message}`];
|
|
45
|
+
// A failed step's own output is usually the actual reason; show its tail rather than only the step name. Only an
|
|
46
|
+
// InstallerError carries details, and it holds them as the record they were raised with.
|
|
47
|
+
const details = isInstallerError(error) ? error.details : undefined;
|
|
48
|
+
for (const key of ['output', 'log']) {
|
|
49
|
+
const text = details?.[key];
|
|
50
|
+
if (typeof text === 'string' && text.trim() !== '') {
|
|
51
|
+
lines.push(...text.split('\n').map((line) => ` | ${line}`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const suggestion of envelope.error.suggestions) {
|
|
55
|
+
lines.push(` ${suggestion.message}`);
|
|
56
|
+
if (suggestion.run)
|
|
57
|
+
lines.push(` ${formatCommandLine(suggestion.run)}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/pm2.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { CommandFailedError, runCommand, } from "./run-command.js";
|
|
5
|
+
/** pm2 prints daemon notices such as "[PM2] Spawning PM2 daemon" before the JSON; the array is the last line that parses. */
|
|
6
|
+
export function parseJlist(stdout) {
|
|
7
|
+
const lines = stdout.trim().split('\n').reverse();
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
if (!line.startsWith('['))
|
|
10
|
+
continue;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(line);
|
|
13
|
+
return parsed.map((entry) => ({
|
|
14
|
+
name: entry.name,
|
|
15
|
+
pid: entry.pid,
|
|
16
|
+
status: entry.pm2_env?.status ?? 'unknown',
|
|
17
|
+
restarts: entry.pm2_env?.restart_time ?? 0,
|
|
18
|
+
...(entry.pm2_env?.pm_cwd ? { cwd: entry.pm2_env.pm_cwd } : {}),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A notice line that happens to start with "[".
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether pm2's daemon is running, read from its pid file. Any pm2 command, `jlist` included, starts the daemon when
|
|
29
|
+
* it is not running, so a read-only question has to be answered without asking pm2.
|
|
30
|
+
*/
|
|
31
|
+
export function isDaemonRunning(env = process.env) {
|
|
32
|
+
const home = env.PM2_HOME || path.join(os.homedir(), '.pm2');
|
|
33
|
+
let pid;
|
|
34
|
+
try {
|
|
35
|
+
pid = Number.parseInt(readFileSync(path.join(home, 'pm2.pid'), 'utf8'), 10);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isInteger(pid))
|
|
41
|
+
return false;
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return error.code === 'EPERM';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function createPm2(bin = 'pm2', run = runCommand, daemonRunning = isDaemonRunning) {
|
|
51
|
+
return {
|
|
52
|
+
async version() {
|
|
53
|
+
const { stdout } = await run(bin, ['--version']);
|
|
54
|
+
// pm2 may print daemon notices around the version, so take the line that is one.
|
|
55
|
+
const lines = stdout
|
|
56
|
+
.trim()
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((line) => line.trim());
|
|
59
|
+
return ([...lines].reverse().find((line) => /^\d+\.\d+\.\d+/u.test(line)) ??
|
|
60
|
+
lines.at(-1) ??
|
|
61
|
+
'');
|
|
62
|
+
},
|
|
63
|
+
async start(ecosystemFile, cwd) {
|
|
64
|
+
await run(bin, ['start', ecosystemFile], { cwd });
|
|
65
|
+
},
|
|
66
|
+
async stop(name) {
|
|
67
|
+
await run(bin, ['stop', name]);
|
|
68
|
+
},
|
|
69
|
+
async remove(name) {
|
|
70
|
+
try {
|
|
71
|
+
await run(bin, ['delete', name]);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof CommandFailedError &&
|
|
75
|
+
/not found/iu.test(`${error.stdout}\n${error.stderr}`)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async save() {
|
|
82
|
+
await run(bin, ['save']);
|
|
83
|
+
},
|
|
84
|
+
async describe(name) {
|
|
85
|
+
if (!daemonRunning())
|
|
86
|
+
return undefined;
|
|
87
|
+
const { stdout } = await run(bin, ['jlist']);
|
|
88
|
+
return parseJlist(stdout).find((entry) => entry.name === name);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=pm2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pm2.js","sourceRoot":"","sources":["../../src/lib/pm2.ts"],"names":[],"mappings":"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"}
|