@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.
Files changed (71) hide show
  1. package/README.md +263 -2
  2. package/bin/run.js +43 -0
  3. package/dist/cli.js +212 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/install.js +333 -0
  6. package/dist/commands/install.js.map +1 -0
  7. package/dist/commands/rollback.js +289 -0
  8. package/dist/commands/rollback.js.map +1 -0
  9. package/dist/commands/status.js +134 -0
  10. package/dist/commands/status.js.map +1 -0
  11. package/dist/commands/upgrade.js +598 -0
  12. package/dist/commands/upgrade.js.map +1 -0
  13. package/dist/lib/app-cli.js +89 -0
  14. package/dist/lib/app-cli.js.map +1 -0
  15. package/dist/lib/app-source.js +163 -0
  16. package/dist/lib/app-source.js.map +1 -0
  17. package/dist/lib/backup.js +167 -0
  18. package/dist/lib/backup.js.map +1 -0
  19. package/dist/lib/checksum-repair.js +76 -0
  20. package/dist/lib/checksum-repair.js.map +1 -0
  21. package/dist/lib/confirm.js +36 -0
  22. package/dist/lib/confirm.js.map +1 -0
  23. package/dist/lib/current-link.js +22 -0
  24. package/dist/lib/current-link.js.map +1 -0
  25. package/dist/lib/ecosystem.js +82 -0
  26. package/dist/lib/ecosystem.js.map +1 -0
  27. package/dist/lib/env-file.js +87 -0
  28. package/dist/lib/env-file.js.map +1 -0
  29. package/dist/lib/errors.js +32 -0
  30. package/dist/lib/errors.js.map +1 -0
  31. package/dist/lib/health.js +55 -0
  32. package/dist/lib/health.js.map +1 -0
  33. package/dist/lib/initial-admin.js +39 -0
  34. package/dist/lib/initial-admin.js.map +1 -0
  35. package/dist/lib/interrupt.js +50 -0
  36. package/dist/lib/interrupt.js.map +1 -0
  37. package/dist/lib/invocation.js +33 -0
  38. package/dist/lib/invocation.js.map +1 -0
  39. package/dist/lib/layout.js +48 -0
  40. package/dist/lib/layout.js.map +1 -0
  41. package/dist/lib/lock.js +59 -0
  42. package/dist/lib/lock.js.map +1 -0
  43. package/dist/lib/mount-path.js +34 -0
  44. package/dist/lib/mount-path.js.map +1 -0
  45. package/dist/lib/offline-bundle.js +91 -0
  46. package/dist/lib/offline-bundle.js.map +1 -0
  47. package/dist/lib/output.js +61 -0
  48. package/dist/lib/output.js.map +1 -0
  49. package/dist/lib/pm2.js +92 -0
  50. package/dist/lib/pm2.js.map +1 -0
  51. package/dist/lib/prechecks.js +199 -0
  52. package/dist/lib/prechecks.js.map +1 -0
  53. package/dist/lib/registry.js +9 -0
  54. package/dist/lib/registry.js.map +1 -0
  55. package/dist/lib/release.js +173 -0
  56. package/dist/lib/release.js.map +1 -0
  57. package/dist/lib/remote.js +331 -0
  58. package/dist/lib/remote.js.map +1 -0
  59. package/dist/lib/run-command.js +198 -0
  60. package/dist/lib/run-command.js.map +1 -0
  61. package/dist/lib/service.js +100 -0
  62. package/dist/lib/service.js.map +1 -0
  63. package/dist/lib/source.js +54 -0
  64. package/dist/lib/source.js.map +1 -0
  65. package/dist/lib/state.js +87 -0
  66. package/dist/lib/state.js.map +1 -0
  67. package/dist/lib/version.js +51 -0
  68. package/dist/lib/version.js.map +1 -0
  69. package/dist/remote-entry.js +98 -0
  70. package/dist/remote-entry.js.map +1 -0
  71. package/package.json +53 -3
@@ -0,0 +1,89 @@
1
+ import path from 'node:path';
2
+ import { InstallerError } from "./errors.js";
3
+ import { formatCommandLine } from '@nocobase/cli-envelope';
4
+ import { CommandFailedError, runCommand, tail, } from "./run-command.js";
5
+ /**
6
+ * Carries a suggestion of the release's CLI over into the installer's error, with its command folded into the message.
7
+ * None of them runs as-is from an installation root: they name the release's own `dist/cli/index.js`, which a failed
8
+ * install or upgrade has already removed and which reads the wrong configuration without `app.env`, or `pnpm add`,
9
+ * which would turn the installation root into a project.
10
+ */
11
+ export function toSuggestion(entry) {
12
+ const run = typeof entry.run === 'string'
13
+ ? entry.run
14
+ : entry.run
15
+ ? formatCommandLine(entry.run)
16
+ : undefined;
17
+ const message = entry.message ?? '';
18
+ return {
19
+ message: run
20
+ ? [message, `(the application CLI's command: ${run})`]
21
+ .filter(Boolean)
22
+ .join(' ')
23
+ : message,
24
+ };
25
+ }
26
+ /**
27
+ * The command as it may appear in a message: `config set` values are masked, since a `--set` may carry a secret that
28
+ * would otherwise end up in the JSON result and the terminal. `--from-env` pairs name a variable, not a value, and stay.
29
+ */
30
+ export function describeAppCommand(args) {
31
+ const setting = args[0] === 'config' && args[1] === 'set';
32
+ let fromEnv = false;
33
+ const shown = args.map((arg) => {
34
+ if (arg === '--from-env')
35
+ fromEnv = true;
36
+ if (!setting || fromEnv || arg.startsWith('-'))
37
+ return arg;
38
+ const index = arg.indexOf('=');
39
+ return index > 0 ? `${arg.slice(0, index + 1)}***` : arg;
40
+ });
41
+ return `nocobase ${shown.join(' ')}`;
42
+ }
43
+ /**
44
+ * Runs the release's own CLI, `node <release>/dist/cli/index.js <args> --json`, and returns its envelope. It is the code
45
+ * that runs in production, so configuration and migrations are checked by the same version that will serve them.
46
+ */
47
+ export async function runAppCli(args, options) {
48
+ const entry = path.join(options.releaseDir, 'dist/cli/index.js');
49
+ const label = describeAppCommand(args);
50
+ const run = options.run ?? runCommand;
51
+ let stdout;
52
+ let stderr;
53
+ try {
54
+ ({ stdout, stderr } = await run(process.execPath, [entry, ...args, '--json'], {
55
+ cwd: options.cwd,
56
+ env: { ...process.env, ...options.env },
57
+ timeoutMs: 30 * 60_000,
58
+ }));
59
+ }
60
+ catch (error) {
61
+ if (!(error instanceof CommandFailedError))
62
+ throw error;
63
+ stdout = error.stdout;
64
+ stderr = error.stderr;
65
+ }
66
+ let envelope;
67
+ try {
68
+ envelope = JSON.parse(stdout);
69
+ }
70
+ catch {
71
+ throw new InstallerError('APP_CLI_FAILED', `${label} did not print a JSON result.`, { details: { stderr: tail(stderr), stdout: tail(stdout) } });
72
+ }
73
+ if (!envelope.ok) {
74
+ throw new InstallerError(envelope.error?.code ?? 'APP_CLI_FAILED', `${label} failed: ${envelope.error?.message ?? 'no message'}`, {
75
+ suggestions: (envelope.error?.suggestions ?? []).map(toSuggestion),
76
+ details: { command: label },
77
+ });
78
+ }
79
+ return envelope;
80
+ }
81
+ /**
82
+ * Counts the migration and seed tasks a `db apply --dry-run` would run. `status` is not a reliable signal: a dry run
83
+ * reports `success-noop` even when tasks are pending, so the plan itself is read.
84
+ */
85
+ export function pendingTaskCount(envelope) {
86
+ const plan = envelope.result?.plan;
87
+ return (plan ?? []).reduce((count, entry) => count + (entry.tasks?.length ?? 0), 0);
88
+ }
89
+ //# sourceMappingURL=app-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-cli.js","sourceRoot":"","sources":["../../src/lib/app-cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { extract } from 'tar';
5
+ import { parse, stringify } from 'yaml';
6
+ import { EXIT_INVALID, InstallerError, isInstallerError } from "./errors.js";
7
+ import { unpackRelease } from "./release.js";
8
+ import { CommandFailedError, tail } from "./run-command.js";
9
+ /** Bare names are official applications; other publishers use their full scoped package name. */
10
+ export function parseAppSpec(value) {
11
+ const match = /^(?:(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*)|([a-z0-9][a-z0-9._-]*))(?:@([a-zA-Z0-9][a-zA-Z0-9.+_-]*))?$/u.exec(value);
12
+ if (!match) {
13
+ throw new InstallerError('INVALID_USAGE', '--app expects an application name, optionally followed by @<version or tag>, e.g. @acme/crm or @acme/crm@beta.', { exitCode: EXIT_INVALID });
14
+ }
15
+ return {
16
+ package: match[1] ?? `@nocobase/${match[2]}`,
17
+ selector: match[3] ?? 'latest',
18
+ };
19
+ }
20
+ /** Explicit application scope prevents a user's scoped registry from overriding --registry. */
21
+ function registryArgs(packageName, registry) {
22
+ return [
23
+ `--registry=${registry}`,
24
+ `--${packageName.split('/')[0]}:registry=${registry}`,
25
+ ];
26
+ }
27
+ /** npm handles the user's registry authentication; resolve once, then pack that exact version. */
28
+ export async function resolveApp(spec, registry, run) {
29
+ // Validate selectors read from state or --to as well as those supplied to install.
30
+ parseAppSpec(`${spec.package}@${spec.selector}`);
31
+ try {
32
+ const result = await run('npm', [
33
+ 'view',
34
+ `${spec.package}@${spec.selector}`,
35
+ 'name',
36
+ 'version',
37
+ '--json',
38
+ ...registryArgs(spec.package, registry),
39
+ ], { timeoutMs: 120_000 });
40
+ const metadata = JSON.parse(result.stdout);
41
+ if (metadata.name !== spec.package ||
42
+ !metadata.version ||
43
+ !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(metadata.version)) {
44
+ throw new InstallerError('APP_INVALID', `The registry did not resolve ${spec.package}@${spec.selector} to one application version.`);
45
+ }
46
+ return { package: spec.package, selector: metadata.version };
47
+ }
48
+ catch (error) {
49
+ if (isInstallerError(error))
50
+ throw error;
51
+ throw new InstallerError('APP_RESOLVE_FAILED', `Could not resolve ${spec.package}@${spec.selector} from ${registry}.`, {
52
+ cause: error,
53
+ ...(error instanceof CommandFailedError
54
+ ? { details: { output: tail(`${error.stdout}\n${error.stderr}`) } }
55
+ : {}),
56
+ });
57
+ }
58
+ }
59
+ /** Build the published application itself, without scaffolding or rewriting its identity. */
60
+ export async function buildApp(options) {
61
+ const { app, registry, layout, run, reporter } = options;
62
+ const temporary = await mkdtemp(path.join(os.tmpdir(), 'nocobase-app-build-'));
63
+ const env = {
64
+ ...process.env,
65
+ npm_config_registry: registry,
66
+ NPM_CONFIG_REGISTRY: registry,
67
+ PNPM_CONFIG_REGISTRY: registry,
68
+ PNPM_CONFIG_MINIMUM_RELEASE_AGE: '0',
69
+ CI: 'true',
70
+ };
71
+ try {
72
+ const pnpm = await run('pnpm', ['--version'], { cwd: temporary, env });
73
+ if (Number.parseInt(pnpm.stdout, 10) < 11 ||
74
+ !/^\d+\./u.test(pnpm.stdout.trim())) {
75
+ throw new InstallerError('PNPM_REQUIRED', 'Installing an npm application requires pnpm 11 or later.', { exitCode: EXIT_INVALID });
76
+ }
77
+ reporter.progress(`Downloading ${app.package}@${app.selector}`);
78
+ const packed = await run('npm', [
79
+ 'pack',
80
+ `${app.package}@${app.selector}`,
81
+ '--ignore-scripts',
82
+ '--json',
83
+ ...registryArgs(app.package, registry),
84
+ ], { cwd: temporary, env, timeoutMs: 120_000 });
85
+ const entries = JSON.parse(packed.stdout);
86
+ const filename = entries[0]?.filename;
87
+ if (!filename || filename !== path.basename(filename))
88
+ throw new InstallerError('APP_INVALID', 'npm pack returned no application tarball.');
89
+ extract({
90
+ file: path.join(temporary, filename),
91
+ cwd: temporary,
92
+ strip: 1,
93
+ strict: true,
94
+ sync: true,
95
+ });
96
+ const manifest = JSON.parse(await readFile(path.join(temporary, 'package.json'), 'utf8'));
97
+ if (manifest.name !== app.package ||
98
+ manifest.version !== app.selector ||
99
+ manifest.nocobase?.templateKind !== 'app' ||
100
+ !manifest.scripts?.build) {
101
+ throw new InstallerError('APP_INVALID', `${app.package}@${app.selector} is not a buildable NocoBase application package.`, { exitCode: EXIT_INVALID });
102
+ }
103
+ const workspaceFile = path.join(temporary, 'pnpm-workspace.yaml');
104
+ const existing = await readFile(workspaceFile, 'utf8').catch((error) => {
105
+ if (error.code === 'ENOENT')
106
+ return '';
107
+ throw error;
108
+ });
109
+ const workspace = (parse(existing) ?? {});
110
+ await writeFile(workspaceFile, stringify({
111
+ verifyDepsBeforeRun: false,
112
+ strictDepBuilds: false,
113
+ ...workspace,
114
+ allowBuilds: {
115
+ esbuild: true,
116
+ oracledb: true,
117
+ 'better-sqlite3': false,
118
+ 'tesseract.js': false,
119
+ ...workspace.allowBuilds,
120
+ },
121
+ }));
122
+ const npmrc = path.join(temporary, '.npmrc');
123
+ await writeFile(npmrc, `\nregistry=${registry}\n@nocobase:registry=${registry}\n${app.package.split('/')[0]}:registry=${registry}\n`, { flag: 'a' });
124
+ reporter.progress(`Installing build dependencies for ${app.package}@${app.selector}`);
125
+ await run('pnpm', [
126
+ 'install',
127
+ '--no-frozen-lockfile',
128
+ '--prod=false',
129
+ '--config.strict-peer-dependencies=false',
130
+ `--registry=${registry}`,
131
+ ], { cwd: temporary, env, timeoutMs: 20 * 60_000 });
132
+ reporter.progress(`Building ${app.package}@${app.selector} for this server`);
133
+ await run('pnpm', ['build', '--tar'], {
134
+ cwd: temporary,
135
+ env,
136
+ timeoutMs: 30 * 60_000,
137
+ });
138
+ const prepared = await unpackRelease({
139
+ layout,
140
+ archive: path.join(temporary, 'storage/exports/dist.tar.gz'),
141
+ });
142
+ if (prepared.appName !== app.package || prepared.version !== app.selector) {
143
+ if (!prepared.reused)
144
+ await rm(path.dirname(prepared.dir), { recursive: true, force: true });
145
+ throw new InstallerError('APP_INVALID', 'The build changed the published application name or version.');
146
+ }
147
+ return prepared;
148
+ }
149
+ catch (error) {
150
+ if (isInstallerError(error))
151
+ throw error;
152
+ throw new InstallerError('APP_BUILD_FAILED', `Building ${app.package}@${app.selector} failed: ${error instanceof Error ? error.message : String(error)}`, {
153
+ cause: error,
154
+ ...(error instanceof CommandFailedError
155
+ ? { details: { output: tail(`${error.stdout}\n${error.stderr}`) } }
156
+ : {}),
157
+ });
158
+ }
159
+ finally {
160
+ await rm(temporary, { recursive: true, force: true });
161
+ }
162
+ }
163
+ //# sourceMappingURL=app-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-source.js","sourceRoot":"","sources":["../../src/lib/app-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { copyFile, mkdir, readFile, readdir, rm, writeFile, } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { parse } from 'yaml';
5
+ import { EXIT_INVALID, InstallerError } from "./errors.js";
6
+ /** A SQLite database is the main file plus whichever journal files exist beside it. */
7
+ const SQLITE_SUFFIXES = ['', '-wal', '-shm', '-journal'];
8
+ /** Written into every backup, so a restore knows where each database goes even after `config.yml` changes. */
9
+ const MANIFEST = 'backup.json';
10
+ /** `${NAME}` references, resolved the way the runtime resolves them from its environment; `undefined` if one is unset. */
11
+ function interpolate(value, env) {
12
+ let missing = false;
13
+ const resolved = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_, name) => {
14
+ const found = env[name];
15
+ if (found === undefined)
16
+ missing = true;
17
+ return found ?? '';
18
+ });
19
+ return missing ? undefined : resolved;
20
+ }
21
+ /**
22
+ * The application's databases as `config.yml` declares them. A SQLite path is resolved against the storage directory,
23
+ * as the runtime resolves it; `:memory:` holds nothing to back up. A connection `config.yml` does not declare is not
24
+ * seen, which is why an upgrade stops when the default connection is SQLite and its file is missing.
25
+ */
26
+ export function readDatabaseInventory(layout, env) {
27
+ let document = {};
28
+ try {
29
+ document = (parse(readFileSync(layout.configFile, 'utf8')) ??
30
+ {});
31
+ }
32
+ catch (error) {
33
+ // Treating this as "no databases" would let an upgrade proceed without a backup and without --backup-done.
34
+ throw new InstallerError('CONFIG_UNREADABLE', `${layout.configFile} could not be read as YAML, so the databases to back up are unknown: ${error instanceof Error ? error.message : String(error)}`, { exitCode: EXIT_INVALID, cause: error });
35
+ }
36
+ const inventory = {
37
+ defaultConnection: typeof document.database?.default === 'string'
38
+ ? document.database.default
39
+ : 'main',
40
+ sqlite: [],
41
+ external: [],
42
+ unresolved: [],
43
+ };
44
+ const lookup = { ...process.env, ...env };
45
+ for (const [connection, raw] of Object.entries(document.database?.connections ?? {})) {
46
+ const config = (raw ?? {});
47
+ if (config.dialect !== 'sqlite') {
48
+ inventory.external.push(connection);
49
+ continue;
50
+ }
51
+ const declared = config.database ?? config.filename;
52
+ if (typeof declared !== 'string' || declared === '') {
53
+ inventory.unresolved.push(connection);
54
+ continue;
55
+ }
56
+ const file = interpolate(declared, lookup);
57
+ if (file === undefined) {
58
+ inventory.unresolved.push(connection);
59
+ continue;
60
+ }
61
+ if (file === ':memory:')
62
+ continue;
63
+ inventory.sqlite.push({
64
+ connection,
65
+ file: path.resolve(layout.storageDir, file),
66
+ });
67
+ }
68
+ return inventory;
69
+ }
70
+ /** UTC, so backups taken on machines in different time zones sort the same way. */
71
+ function stamp(date) {
72
+ const pad = (value) => String(value).padStart(2, '0');
73
+ return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
74
+ }
75
+ export function backupName(from, to, date) {
76
+ return `${stamp(date)}_${from}_to_${to}`;
77
+ }
78
+ async function existingFiles(file) {
79
+ const base = path.basename(file);
80
+ const present = new Set(await readdir(path.dirname(file)).catch(() => []));
81
+ return SQLITE_SUFFIXES.map((suffix) => `${base}${suffix}`).filter((name) => present.has(name));
82
+ }
83
+ async function readManifest(layout, backupRelative) {
84
+ try {
85
+ return JSON.parse(await readFile(path.join(layout.root, backupRelative, MANIFEST), 'utf8'));
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ /**
92
+ * Whether a backup holds a database that can be restored. An upgrade of an application on an external database backs
93
+ * up only `config.yml` and `app.env`, and a backup interrupted before its copy finished has no manifest at all.
94
+ */
95
+ export function backupHasDatabase(layout, backupRelative) {
96
+ const file = path.join(layout.root, backupRelative, MANIFEST);
97
+ if (!existsSync(file))
98
+ return false;
99
+ try {
100
+ const manifest = JSON.parse(readFileSync(file, 'utf8'));
101
+ return manifest.databases.some((database) => database.files.length > 0);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ export async function removeBackup(layout, backupRelative) {
108
+ await rm(path.join(layout.root, backupRelative), {
109
+ recursive: true,
110
+ force: true,
111
+ });
112
+ }
113
+ /** Where a connection's files go inside a backup: one directory per connection, so equal file names never collide. */
114
+ function backupDatabaseDir(target, connection) {
115
+ return path.join(target, 'sqlite', connection);
116
+ }
117
+ /**
118
+ * Copies the application's SQLite databases, `config.yml` and `app.env` while the application is stopped, so the files
119
+ * are consistent. The manifest is written last: a backup cut short has none and is never restored from. This is what
120
+ * a failed upgrade restores, not a replacement for regular backups of `storage/`, whose uploaded files it leaves out.
121
+ */
122
+ export async function backupForUpgrade(layout, name, databases) {
123
+ const target = path.join(layout.backupsDir, name);
124
+ await mkdir(target, { recursive: true });
125
+ await copyFile(layout.configFile, path.join(target, 'config.yml'));
126
+ await copyFile(layout.appEnv, path.join(target, 'app.env'));
127
+ const copied = [];
128
+ for (const database of databases) {
129
+ const files = await existingFiles(database.file);
130
+ const directory = backupDatabaseDir(target, database.connection);
131
+ await mkdir(directory, { recursive: true });
132
+ for (const file of files) {
133
+ await copyFile(path.join(path.dirname(database.file), file), path.join(directory, file));
134
+ }
135
+ copied.push({ ...database, files });
136
+ }
137
+ const manifest = { databases: copied };
138
+ await writeFile(path.join(target, MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
139
+ return { relative: path.relative(layout.root, target), databases: copied };
140
+ }
141
+ /**
142
+ * Puts backed-up databases back where the manifest says they were. Journal files that exist now but were not in the
143
+ * backup are removed, since SQLite would otherwise replay a newer write-ahead log onto the older database; a database
144
+ * that did not exist when the backup was taken is removed, since it was created by the release being undone.
145
+ */
146
+ export async function restoreDatabase(layout, backupRelative) {
147
+ const source = path.join(layout.root, backupRelative);
148
+ const manifest = await readManifest(layout, backupRelative);
149
+ if (!manifest ||
150
+ !manifest.databases.some((database) => database.files.length > 0)) {
151
+ throw new Error(`${source} holds no SQLite database to restore.`);
152
+ }
153
+ for (const database of manifest.databases) {
154
+ const directory = path.dirname(database.file);
155
+ await mkdir(directory, { recursive: true });
156
+ for (const leftover of await existingFiles(database.file)) {
157
+ if (!database.files.includes(leftover)) {
158
+ await rm(path.join(directory, leftover), { force: true });
159
+ }
160
+ }
161
+ for (const file of database.files) {
162
+ await copyFile(path.join(backupDatabaseDir(source, database.connection), file), path.join(directory, file));
163
+ }
164
+ }
165
+ return manifest.databases;
166
+ }
167
+ //# sourceMappingURL=backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup.js","sourceRoot":"","sources":["../../src/lib/backup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { runAppCli } from "./app-cli.js";
3
+ import { EXIT_INVALID, InstallerError } from "./errors.js";
4
+ const object = (value) => typeof value === 'object' && value !== null;
5
+ const nonempty = (value) => typeof value === 'string' && value.length > 0;
6
+ /** Reject an unsupported or incomplete CLI response instead of treating it as an empty repair plan. */
7
+ export function checksumRepairs(result) {
8
+ const invalid = () => {
9
+ throw new InstallerError('CHECKSUM_CHECK_INVALID', 'The target application must support db repair --dry-run --json with complete checksum results. No upgrade was applied.', { exitCode: EXIT_INVALID });
10
+ };
11
+ if (!object(result) ||
12
+ result.dryRun !== true ||
13
+ !Array.isArray(result.results))
14
+ return invalid();
15
+ const changes = [];
16
+ for (const entry of result.results) {
17
+ if (!object(entry) ||
18
+ !nonempty(entry.connection) ||
19
+ (entry.kind !== 'migrations' && entry.kind !== 'seeds'))
20
+ return invalid();
21
+ if (entry.status === 'skipped')
22
+ continue;
23
+ if (entry.status !== 'completed' || !Array.isArray(entry.repaired))
24
+ return invalid();
25
+ for (const record of entry.repaired) {
26
+ if (!object(record) ||
27
+ !nonempty(record.packageName) ||
28
+ !nonempty(record.name) ||
29
+ !nonempty(record.recordedChecksum) ||
30
+ !nonempty(record.sourceChecksum))
31
+ return invalid();
32
+ changes.push({
33
+ connection: entry.connection,
34
+ kind: entry.kind,
35
+ packageName: record.packageName,
36
+ name: record.name,
37
+ recordedChecksum: record.recordedChecksum,
38
+ sourceChecksum: record.sourceChecksum,
39
+ });
40
+ }
41
+ }
42
+ return changes.sort((a, b) => {
43
+ const left = JSON.stringify(a);
44
+ const right = JSON.stringify(b);
45
+ return left < right ? -1 : left > right ? 1 : 0;
46
+ });
47
+ }
48
+ export async function previewChecksumRepair(cli, identity) {
49
+ const envelope = await runAppCli(['db', 'repair', '--dry-run'], cli);
50
+ const changes = checksumRepairs(envelope.result);
51
+ // Build timestamps can change when an npm application is rebuilt for the confirmation run.
52
+ // Bind approval to the installation, current release, target version and exact checksum changes instead.
53
+ const token = createHash('sha256')
54
+ .update(JSON.stringify({ directory: cli.cwd, ...identity, changes }))
55
+ .digest('hex');
56
+ return { token, changes };
57
+ }
58
+ export function requireChecksumApproval(plan, approved) {
59
+ if (!plan.changes.length && approved === undefined)
60
+ return;
61
+ if (approved === plan.token)
62
+ return;
63
+ throw new InstallerError('CHECKSUM_REPAIR_REQUIRED', 'Review every migration and seed checksum change before authorizing repair. --yes alone does not authorize checksum repair.', {
64
+ exitCode: EXIT_INVALID,
65
+ details: { checksumRepair: plan },
66
+ suggestions: [
67
+ ...plan.changes.map((change) => ({
68
+ message: `${change.connection}/${change.kind}: ${change.packageName}:${change.name} ${change.recordedChecksum} -> ${change.sourceChecksum}`,
69
+ })),
70
+ {
71
+ message: `After reviewing this plan, repeat the same upgrade with --repair-checksums ${plan.token}. The installer backs up before repairing.`,
72
+ },
73
+ ],
74
+ });
75
+ }
76
+ //# sourceMappingURL=checksum-repair.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksum-repair.js","sourceRoot":"","sources":["../../src/lib/checksum-repair.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { EXIT_INVALID, InstallerError } from "./errors.js";
3
+ /**
4
+ * Asks before an operation that takes the application down or discards data. `--yes` answers for scripts; without it, a run that
5
+ * cannot ask — `--json`, or no terminal — stops instead of guessing.
6
+ */
7
+ export async function confirm(lines, options) {
8
+ if (options.yes)
9
+ return;
10
+ const input = options.input ?? process.stdin;
11
+ const output = options.output ?? process.stderr;
12
+ if (options.json || !input.isTTY) {
13
+ throw new InstallerError('CONFIRMATION_REQUIRED', `${lines[0]} Pass --yes to proceed without a prompt.`, {
14
+ exitCode: EXIT_INVALID,
15
+ details: { notes: lines.slice(1) },
16
+ suggestions: [
17
+ { message: 'Run it again with --yes once you have read:' },
18
+ ...lines.slice(1).map((message) => ({ message })),
19
+ ],
20
+ });
21
+ }
22
+ output.write(`${lines.map((line) => ` ${line}`).join('\n')}\n`);
23
+ const readline = createInterface({ input, output });
24
+ try {
25
+ const answer = await readline.question('Continue? [y/N] ');
26
+ if (!/^y(es)?$/iu.test(answer.trim())) {
27
+ throw new InstallerError('CANCELLED', 'Cancelled; nothing was changed.', {
28
+ exitCode: EXIT_INVALID,
29
+ });
30
+ }
31
+ }
32
+ finally {
33
+ readline.close();
34
+ }
35
+ }
36
+ //# sourceMappingURL=confirm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirm.js","sourceRoot":"","sources":["../../src/lib/confirm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAS3D;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,KAAwB,EACxB,OAAuB;IAEvB,IAAI,OAAO,CAAC,GAAG;QAAE,OAAO;IACxB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC;IAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAChD,IAAI,OAAO,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACjC,MAAM,IAAI,cAAc,CACtB,uBAAuB,EACvB,GAAG,KAAK,CAAC,CAAC,CAAC,0CAA0C,EACrD;YACE,QAAQ,EAAE,YAAY;YACtB,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;YAClC,WAAW,EAAE;gBACX,EAAE,OAAO,EAAE,6CAA6C,EAAE;gBAC1D,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;aAClD;SACF,CACF,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACpD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC;QAC3D,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,cAAc,CAAC,WAAW,EAAE,iCAAiC,EAAE;gBACvE,QAAQ,EAAE,YAAY;aACvB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;YAAS,CAAC;QACT,QAAQ,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { readlink, rename, rm, symlink } from 'node:fs/promises';
2
+ /**
3
+ * Points `current` at a release in one step: the new link is created beside it and renamed over it, so `current`
4
+ * always names a complete release and never goes missing in between.
5
+ */
6
+ export async function switchCurrent(layout, linkTarget) {
7
+ const temporary = `${layout.current}.${process.pid}.tmp`;
8
+ await rm(temporary, { force: true });
9
+ await symlink(linkTarget, temporary, 'dir');
10
+ await rename(temporary, layout.current);
11
+ }
12
+ export async function readCurrent(layout) {
13
+ try {
14
+ return await readlink(layout.current);
15
+ }
16
+ catch (error) {
17
+ if (error.code === 'ENOENT')
18
+ return undefined;
19
+ throw error;
20
+ }
21
+ }
22
+ //# sourceMappingURL=current-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"current-link.js","sourceRoot":"","sources":["../../src/lib/current-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAGjE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,MAAc,EACd,UAAkB;IAElB,MAAM,SAAS,GAAG,GAAG,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;IACzD,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,OAAO,CAAC,UAAU,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC5C,MAAM,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACzE,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}