@selvajs/cli 4.7.4-beta.1 → 4.8.0-beta.11
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 +7 -12
- package/package.json +10 -4
- package/src/checks/boot.js +151 -0
- package/src/checks/config.js +89 -0
- package/src/checks/result.js +27 -0
- package/src/checks/runtime.js +289 -0
- package/src/cli.js +2 -2
- package/src/commands/create.js +43 -69
- package/src/commands/doctor.js +151 -170
- package/src/commands/init.js +9 -32
- package/src/commands/keys.js +3 -17
- package/src/commands/migrate.js +225 -105
- package/src/commands/pm2.js +45 -9
- package/src/deployment-package.js +143 -0
- package/src/env.js +57 -3
- package/src/node-range.js +66 -0
- package/src/paths.js +68 -5
- package/src/prompts.js +4 -16
- package/bin/create.js +0 -7
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The package.json a deployment directory gets. `create` writes it at scaffold
|
|
2
|
+
// time and `migrate` rewrites an existing one onto it, so it lives here rather
|
|
3
|
+
// than in either command — two copies drifted once already, and the pm2 pin is
|
|
4
|
+
// exactly the field where a silent difference costs the most.
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* pm2 is pinned exactly, not caret-ranged.
|
|
12
|
+
*
|
|
13
|
+
* pm2's CLI and its long-running daemon must be the same version. A range lets
|
|
14
|
+
* two deployments installed a week apart resolve differently, and the newer one
|
|
15
|
+
* silently adopts the daemon the older one started — the skew `ensurePm2InSync`
|
|
16
|
+
* has to detect and, in the daemon-is-newer direction, cannot repair (#118).
|
|
17
|
+
*
|
|
18
|
+
* Read from this package's own devDependencies rather than written here as a
|
|
19
|
+
* literal. Dependabot scans manifests, not source, so a literal is invisible to
|
|
20
|
+
* it — this pin sat on 5.4.3 while upstream reached 7.x, which is precisely the
|
|
21
|
+
* drift operators point at when they argue for distro packages instead.
|
|
22
|
+
* `checkPinIsExact` keeps the manifest entry from being loosened to a range.
|
|
23
|
+
*/
|
|
24
|
+
const PM2_VERSION = readPinnedPm2Version();
|
|
25
|
+
|
|
26
|
+
function readPinnedPm2Version() {
|
|
27
|
+
const manifest = JSON.parse(
|
|
28
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')
|
|
29
|
+
);
|
|
30
|
+
const pinned = manifest.devDependencies?.pm2;
|
|
31
|
+
if (!pinned || !/^\d+\.\d+\.\d+$/.test(pinned)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`@selvajs/cli devDependencies.pm2 must be an exact version, got ${JSON.stringify(pinned)}. ` +
|
|
34
|
+
`Every deployment this CLI scaffolds inherits it, and a range there lets two ` +
|
|
35
|
+
`installs resolve different pm2s that then fight over one daemon.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return pinned;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SCRIPTS = {
|
|
42
|
+
start: 'selva start',
|
|
43
|
+
stop: 'selva stop',
|
|
44
|
+
restart: 'selva restart',
|
|
45
|
+
logs: 'selva logs',
|
|
46
|
+
doctor: 'selva doctor',
|
|
47
|
+
update: 'selva update'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Packages a deployment carries. Providers are bundled into `@selvajs/selva`;
|
|
52
|
+
* the CLI is a dependency so `npm run start` resolves the `selva` bin locally
|
|
53
|
+
* instead of relying on a global install.
|
|
54
|
+
*/
|
|
55
|
+
const DEPENDENCIES = {
|
|
56
|
+
'@selvajs/cli': 'latest',
|
|
57
|
+
'@selvajs/selva': 'latest',
|
|
58
|
+
pm2: PM2_VERSION
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* npm `overrides` forced onto every deployment — security patches for
|
|
63
|
+
* transitive deps whose direct parent hasn't picked up the fix yet. Each entry
|
|
64
|
+
* is a temporary shim: remove it once the parent ships the fixed version on
|
|
65
|
+
* its own (the override then matches what would install anyway, so removal is
|
|
66
|
+
* cleanup, never urgent).
|
|
67
|
+
*
|
|
68
|
+
* js-yaml: pm2 <= 7.0.3 pins 4.3.0, which carries the quadratic-complexity
|
|
69
|
+
* DoS advisories (GHSA-5p4m-2wfm-xmqj and friends); the fix landed in 4.3.1.
|
|
70
|
+
* Drop when pm2's own js-yaml dependency reaches >= 4.3.1.
|
|
71
|
+
*/
|
|
72
|
+
export const OVERRIDES = {
|
|
73
|
+
'js-yaml': '^4.3.1'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Legacy `@selvajs/*` packages that a deployment may still list. Each was
|
|
78
|
+
* either renamed or folded into `@selvajs/selva`; `migrate` drops them and
|
|
79
|
+
* `doctor` reports them as layout drift.
|
|
80
|
+
*/
|
|
81
|
+
export const LEGACY_DEPENDENCIES = {
|
|
82
|
+
'@selvajs/runtime': 'the old runtime package',
|
|
83
|
+
'@selvajs/create': 'the old CLI package',
|
|
84
|
+
'@selvajs/platform': 'now bundled into @selvajs/selva',
|
|
85
|
+
'@selvajs/local-provider': 'now bundled into @selvajs/selva',
|
|
86
|
+
'@selvajs/supabase-provider': 'now bundled into @selvajs/selva',
|
|
87
|
+
'@selvajs/header-auth-provider': 'now bundled into @selvajs/selva'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build the canonical deployment package.json.
|
|
92
|
+
*
|
|
93
|
+
* Dependencies are replaced wholesale rather than merged, so an operator's own
|
|
94
|
+
* additions are dropped. That is deliberate — a deployment directory is
|
|
95
|
+
* generated output, and anyone needing custom dependencies should depend on
|
|
96
|
+
* `@selvajs/selva` from their own project instead of editing this file.
|
|
97
|
+
*
|
|
98
|
+
* `engines` is the one exception, carried over when present: npm only enforces
|
|
99
|
+
* it under `engine-strict`, so an operator who pinned a Node floor did it
|
|
100
|
+
* deliberately, and dropping it removes a guard whose absence shows up only
|
|
101
|
+
* under real traffic (issue #176). Everything else a migration discards is
|
|
102
|
+
* listed by `diffPackageJson` before the operator confirms.
|
|
103
|
+
*/
|
|
104
|
+
export function buildDeploymentPackageJson({ name, version = '0.1.0', engines }) {
|
|
105
|
+
const pkg = {
|
|
106
|
+
name: sanitizePackageName(name),
|
|
107
|
+
version,
|
|
108
|
+
private: true,
|
|
109
|
+
type: 'module',
|
|
110
|
+
scripts: { ...SCRIPTS },
|
|
111
|
+
dependencies: { ...DEPENDENCIES },
|
|
112
|
+
overrides: { ...OVERRIDES }
|
|
113
|
+
};
|
|
114
|
+
if (engines && typeof engines === 'object') pkg.engines = { ...engines };
|
|
115
|
+
return pkg;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Top-level keys the canonical package.json defines. Anything outside this set
|
|
120
|
+
* that an operator added is dropped by `migrate` — `diffPackageJson` reports
|
|
121
|
+
* them so the loss is declared rather than discovered later.
|
|
122
|
+
*/
|
|
123
|
+
export const CANONICAL_FIELDS = new Set([
|
|
124
|
+
'name',
|
|
125
|
+
'version',
|
|
126
|
+
'private',
|
|
127
|
+
'type',
|
|
128
|
+
'scripts',
|
|
129
|
+
'dependencies',
|
|
130
|
+
'overrides',
|
|
131
|
+
'engines'
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
export function sanitizePackageName(name) {
|
|
135
|
+
return (
|
|
136
|
+
String(name ?? '')
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
139
|
+
.replace(/^[-._]+|[-._]+$/g, '') || 'selva-deployment'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { DEPENDENCIES, SCRIPTS, PM2_VERSION };
|
package/src/env.js
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Old env name → its replacement, for keys renamed without changing meaning.
|
|
6
|
+
// The server still reads each old name for one minor version and warns at
|
|
7
|
+
// boot; keep this in sync with the server's list and drop an entry once the
|
|
8
|
+
// server stops reading it.
|
|
9
|
+
export const RENAMED_ENV_VARS = {
|
|
10
|
+
COMPUTE_DEFINITION_BYTE_CACHE_MB: 'COMPUTE_DEFINITION_CACHE_MB',
|
|
11
|
+
COMPUTE_RESPONSE_CACHE_MB: 'COMPUTE_SOLVE_CACHE_MB',
|
|
12
|
+
DEFINITION_CACHE_TTL_MS: 'REMOTE_DEFINITION_CACHE_TTL_MS',
|
|
13
|
+
MAX_SOLVE_DURATION_MS: 'COMPUTE_SOLVE_DEADLINE_MS'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Deprecated vars whose replacement changes the VALUE too, so a simple key
|
|
17
|
+
// rename can't migrate them — `selva migrate` leaves these alone rather than guessing.
|
|
18
|
+
export const REPLACED_ENV_VARS = {
|
|
19
|
+
SELVA_FLAG_COMPUTE_DEBUG_VERBOSE: 'SELVA_FLAG_COMPUTE_DEBUG=verbose'
|
|
20
|
+
};
|
|
21
|
+
|
|
6
22
|
export function parseEnv(text) {
|
|
7
23
|
const out = {};
|
|
8
24
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
@@ -28,7 +44,6 @@ export function readEnvFile(path) {
|
|
|
28
44
|
return parseEnv(readFileSync(path, 'utf8'));
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
// Merge values into template: preserve structure, rewrite in-place, append new keys.
|
|
32
47
|
export function mergeEnv(template, values) {
|
|
33
48
|
const seen = new Set();
|
|
34
49
|
const lines = template.split(/\r?\n/);
|
|
@@ -69,7 +84,6 @@ export function mergeEnv(template, values) {
|
|
|
69
84
|
out.push(...appended);
|
|
70
85
|
}
|
|
71
86
|
|
|
72
|
-
// Ensure single trailing newline (shell append mishap guard).
|
|
73
87
|
while (out.length > 0 && out[out.length - 1] === '') out.pop();
|
|
74
88
|
return out.join('\n') + '\n';
|
|
75
89
|
}
|
|
@@ -78,6 +92,46 @@ export function writeEnvFile(path, template, values) {
|
|
|
78
92
|
writeFileSync(path, mergeEnv(template, values), 'utf8');
|
|
79
93
|
}
|
|
80
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Rewrite deprecated keys to their current names, in place. Only the key is
|
|
97
|
+
* touched — value, comments, ordering, and spacing survive.
|
|
98
|
+
*
|
|
99
|
+
* A key already present under its new name is left alone and its old line is
|
|
100
|
+
* dropped: the server resolves new-name-wins, so keeping both would preserve
|
|
101
|
+
* a line that does nothing.
|
|
102
|
+
*
|
|
103
|
+
* Returns `{ text, changes }`, `changes` being `[oldName, newName, 'renamed'
|
|
104
|
+
* | 'dropped']` per line acted on. Empty means the file was already current.
|
|
105
|
+
*/
|
|
106
|
+
export function renameEnvKeys(text, renames) {
|
|
107
|
+
const present = new Set(Object.keys(parseEnv(text)));
|
|
108
|
+
const changes = [];
|
|
109
|
+
const out = [];
|
|
110
|
+
|
|
111
|
+
for (const line of text.split(/\r?\n/)) {
|
|
112
|
+
const stripped = line.trim();
|
|
113
|
+
const eq = stripped.indexOf('=');
|
|
114
|
+
if (!stripped || stripped.startsWith('#') || eq === -1) {
|
|
115
|
+
out.push(line);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const key = stripped.slice(0, eq).trim();
|
|
119
|
+
const newName = renames[key];
|
|
120
|
+
if (!newName) {
|
|
121
|
+
out.push(line);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (present.has(newName)) {
|
|
125
|
+
changes.push([key, newName, 'dropped']);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
out.push(line.replace(key, newName));
|
|
129
|
+
changes.push([key, newName, 'renamed']);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { text: out.join('\n'), changes };
|
|
133
|
+
}
|
|
134
|
+
|
|
81
135
|
function quoteIfNeeded(value) {
|
|
82
136
|
const s = String(value);
|
|
83
137
|
if (s === '') return '""';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Node version-range matching, used by `selva doctor` to check an installed
|
|
2
|
+
// runtime and by the scaffolder to refuse a too-old shell before writing files.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Narrow `engines.node` range check — `>=X`, `^`, `~`, `||`, and bare/x-ranges.
|
|
6
|
+
* Returns null when unparseable so a misread never reports a false failure.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors `satisfiesRange` in @selvajs/server/ops. Duplicated because the CLI is
|
|
9
|
+
* dependency-free by design (it scaffolds the deployment that installs the
|
|
10
|
+
* runtime, so it cannot import from it).
|
|
11
|
+
*/
|
|
12
|
+
export function satisfiesNodeRange(version, range) {
|
|
13
|
+
const core = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(version).trim());
|
|
14
|
+
if (!core) return null;
|
|
15
|
+
const v = [Number(core[1]), Number(core[2]), Number(core[3])];
|
|
16
|
+
|
|
17
|
+
let anyParsed = false;
|
|
18
|
+
for (const alt of String(range).split('||')) {
|
|
19
|
+
const clauses = alt.trim().split(/\s+/).filter(Boolean);
|
|
20
|
+
if (!clauses.length) continue;
|
|
21
|
+
|
|
22
|
+
let all = true;
|
|
23
|
+
let parsed = true;
|
|
24
|
+
for (const clause of clauses) {
|
|
25
|
+
const m = /^(>=|<=|>|<|\^|~|=)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/.exec(clause);
|
|
26
|
+
if (!m) {
|
|
27
|
+
parsed = false;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
const op = m[1] ?? '=';
|
|
31
|
+
const num = (s) => (s == null || s === 'x' || s === '*' ? 0 : Number(s));
|
|
32
|
+
const t = [Number(m[2]), num(m[3]), num(m[4])];
|
|
33
|
+
let cmp = 0;
|
|
34
|
+
for (let i = 0; i < 3; i++) {
|
|
35
|
+
if (v[i] > t[i]) {
|
|
36
|
+
cmp = 1;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (v[i] < t[i]) {
|
|
40
|
+
cmp = -1;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let ok;
|
|
45
|
+
if (op === '>=') ok = cmp >= 0;
|
|
46
|
+
else if (op === '>') ok = cmp > 0;
|
|
47
|
+
else if (op === '<=') ok = cmp <= 0;
|
|
48
|
+
else if (op === '<') ok = cmp < 0;
|
|
49
|
+
else if (op === '^') ok = cmp >= 0 && v[0] === t[0];
|
|
50
|
+
else if (op === '~') ok = cmp >= 0 && v[0] === t[0] && v[1] === t[1];
|
|
51
|
+
else
|
|
52
|
+
ok =
|
|
53
|
+
v[0] === t[0] &&
|
|
54
|
+
(m[3] == null || m[3] === 'x' || m[3] === '*' || v[1] === t[1]) &&
|
|
55
|
+
(m[4] == null || m[4] === 'x' || m[4] === '*' || v[2] === t[2]);
|
|
56
|
+
if (!ok) {
|
|
57
|
+
all = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!parsed) continue;
|
|
62
|
+
anyParsed = true;
|
|
63
|
+
if (all) return true;
|
|
64
|
+
}
|
|
65
|
+
return anyParsed ? false : null;
|
|
66
|
+
}
|
package/src/paths.js
CHANGED
|
@@ -1,9 +1,72 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { resolve, join } from 'node:path';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The Node floor Selva requires, read from this CLI's own `engines.node`.
|
|
7
|
+
*
|
|
8
|
+
* Never hardcode it: the CLI, runtime, and server all declare the same range in
|
|
9
|
+
* package.json, and a copy in code silently keeps enforcing the old floor after
|
|
10
|
+
* a bump. Falls back to null (skip the check) rather than guessing a number —
|
|
11
|
+
* refusing to scaffold on a wrong guess is worse than not checking.
|
|
12
|
+
*/
|
|
13
|
+
export function requiredNodeRange() {
|
|
14
|
+
try {
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
17
|
+
const range = pkg.engines?.node;
|
|
18
|
+
return typeof range === 'string' ? range : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* This CLI's own version, written into `.selva-version` so a later `selva
|
|
26
|
+
* migrate` can tell which scaffold layout it is looking at.
|
|
27
|
+
*
|
|
28
|
+
* Read from package.json for the same reason as `requiredNodeRange`: a literal
|
|
29
|
+
* in code stops tracking the release the moment someone forgets to bump it, and
|
|
30
|
+
* the marker's whole job is to be accurate about which CLI wrote the directory.
|
|
31
|
+
*/
|
|
32
|
+
export function scaffoldVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
|
|
36
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
37
|
+
} catch {
|
|
38
|
+
return '0.0.0';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A scaffolding template shipped inside the installed runtime. The CLI cannot
|
|
44
|
+
* import from `@selvajs/selva` — it is what installs it — so it reads the files
|
|
45
|
+
* out of node_modules after install.
|
|
46
|
+
*/
|
|
47
|
+
export function runtimeTemplatePath(dir, name) {
|
|
48
|
+
return join(dir, 'node_modules', '@selvajs', 'selva', 'templates', name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `.env` template to merge operator values into: the runtime's `.env.example`
|
|
53
|
+
* when installed, otherwise the deployment's own `.env` so its comments and
|
|
54
|
+
* ordering survive a rewrite.
|
|
55
|
+
*
|
|
56
|
+
* Returns `''` when neither exists rather than throwing — `mergeEnv` appends
|
|
57
|
+
* every value to an empty template, so the operator gets a usable file instead
|
|
58
|
+
* of an ENOENT from a command that was only asked to change one key.
|
|
59
|
+
*/
|
|
60
|
+
export function readEnvTemplate(dir) {
|
|
61
|
+
const template = runtimeTemplatePath(dir, '.env.example');
|
|
62
|
+
if (existsSync(template)) return readFileSync(template, 'utf8');
|
|
63
|
+
const env = join(dir, '.env');
|
|
64
|
+
if (existsSync(env)) return readFileSync(env, 'utf8');
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Either .env or ecosystem.config.cjs is enough — selva.config.js and
|
|
69
|
+
// node_modules are expected too, but init should still fix a partial install.
|
|
7
70
|
export function isDeploymentDir(dir) {
|
|
8
71
|
return existsSync(join(dir, '.env')) || existsSync(join(dir, 'ecosystem.config.cjs'));
|
|
9
72
|
}
|
package/src/prompts.js
CHANGED
|
@@ -18,9 +18,7 @@ export function collectConfigFromEnv(env = process.env) {
|
|
|
18
18
|
'local',
|
|
19
19
|
'SELVA_AUTH_PROVIDER'
|
|
20
20
|
);
|
|
21
|
-
//
|
|
22
|
-
// for all three" default. Header-auth has no data layer, so it falls
|
|
23
|
-
// through to local unless explicitly overridden.
|
|
21
|
+
// Header-auth has no data layer, so it falls through to local unless overridden.
|
|
24
22
|
const dataDefault = auth === 'header' ? 'local' : auth;
|
|
25
23
|
const data = pick(
|
|
26
24
|
env.SELVA_DATA_PROVIDER,
|
|
@@ -72,7 +70,6 @@ export function collectConfigFromEnv(env = process.env) {
|
|
|
72
70
|
values.HEADER_AUTH_DISPLAY_NAME_HEADER = env.HEADER_AUTH_DISPLAY_NAME_HEADER;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
// ── Bootstrap admin ──────────────────────────────────────────────────
|
|
76
73
|
const adminRequired = auth === 'header' || tenancy === 'multi';
|
|
77
74
|
const adminEmail = env.BOOTSTRAP_INSTANCE_ADMIN_EMAIL || '';
|
|
78
75
|
if (adminRequired && !adminEmail) {
|
|
@@ -127,15 +124,11 @@ function requireEnv(env, name) {
|
|
|
127
124
|
return v;
|
|
128
125
|
}
|
|
129
126
|
|
|
130
|
-
// Run full interactive prompt sequence; return env vars. Caller decides merge vs fresh.
|
|
131
|
-
// `defaults` pre-populates prompts (cheap re-run).
|
|
132
127
|
export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
133
128
|
const isInit = mode === 'init';
|
|
134
129
|
|
|
135
130
|
p.intro(pc.bgCyan(pc.black(isInit ? ' selva init ' : ' Selva — new deployment ')));
|
|
136
131
|
|
|
137
|
-
// Brand prompts (SELVA_BRAND_NAME / COPYRIGHT_NAME / TAGLINE / DESCRIPTION)
|
|
138
|
-
|
|
139
132
|
const tenancy = await p.select({
|
|
140
133
|
message: 'Tenancy mode',
|
|
141
134
|
initialValue: defaults.SELVA_TENANCY ?? 'single',
|
|
@@ -169,7 +162,6 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
169
162
|
});
|
|
170
163
|
cancelOn(auth);
|
|
171
164
|
|
|
172
|
-
// Header-auth: auth-only; user must pick separate data/storage backend.
|
|
173
165
|
let data;
|
|
174
166
|
let storage;
|
|
175
167
|
if (auth === 'header') {
|
|
@@ -197,7 +189,6 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
197
189
|
cancelOn(storageChoice);
|
|
198
190
|
storage = storageChoice;
|
|
199
191
|
} else {
|
|
200
|
-
// Operators usually want same backend for all three; ask once, default the others.
|
|
201
192
|
const mixProviders = await p.confirm({
|
|
202
193
|
message: `Use ${pc.cyan(auth)} for data and storage too?`,
|
|
203
194
|
initialValue: pickSameProviderDefault(defaults, auth)
|
|
@@ -281,7 +272,6 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
281
272
|
}
|
|
282
273
|
|
|
283
274
|
if (auth === 'header') {
|
|
284
|
-
// HEADER_AUTH_DATA_DIR: ask only if data provider isn't local (fallback available).
|
|
285
275
|
if (data !== 'local') {
|
|
286
276
|
const dataDir = await p.text({
|
|
287
277
|
message: 'HEADER_AUTH_DATA_DIR — directory for header-allowlist.json',
|
|
@@ -330,7 +320,7 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
330
320
|
providerValues.HEADER_AUTH_DISPLAY_NAME_HEADER = stringValue(display);
|
|
331
321
|
}
|
|
332
322
|
|
|
333
|
-
//
|
|
323
|
+
// Not enforced — a Docker network can still route to the container on another host.
|
|
334
324
|
const bindLoopback = await p.confirm({
|
|
335
325
|
message: 'Bind the app to 127.0.0.1 only? (recommended for header-auth)',
|
|
336
326
|
initialValue: !defaults.HOST || defaults.HOST === '127.0.0.1' || defaults.HOST === 'localhost'
|
|
@@ -339,7 +329,6 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
339
329
|
providerValues.HOST = bindLoopback ? '127.0.0.1' : stringValue(defaults.HOST);
|
|
340
330
|
}
|
|
341
331
|
|
|
342
|
-
// Bootstrap admin (checked only while no admin exists; safe to leave set).
|
|
343
332
|
if (auth === 'header') {
|
|
344
333
|
p.note(
|
|
345
334
|
[
|
|
@@ -494,10 +483,11 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
494
483
|
return values;
|
|
495
484
|
}
|
|
496
485
|
|
|
497
|
-
// Init: default to "same provider" only if current .env already shows that.
|
|
498
486
|
function pickSameProviderDefault(defaults, auth) {
|
|
499
487
|
const data = defaults.SELVA_DATA_PROVIDER ?? auth;
|
|
500
488
|
const storage = defaults.SELVA_STORAGE_PROVIDER ?? auth;
|
|
489
|
+
// No existing defaults means a fresh `create`, not a re-prompted `init` — default to
|
|
490
|
+
// "same provider for all three" since there's nothing yet to disagree with auth.
|
|
501
491
|
if (!defaults.SELVA_DATA_PROVIDER && !defaults.SELVA_STORAGE_PROVIDER) return true;
|
|
502
492
|
return data === auth && storage === auth;
|
|
503
493
|
}
|
|
@@ -527,5 +517,3 @@ function headerAuthSecurityWarning() {
|
|
|
527
517
|
pc.dim('See packages/providers/header-auth/README.md for the full deployment checklist.')
|
|
528
518
|
].join('\n');
|
|
529
519
|
}
|
|
530
|
-
|
|
531
|
-
export { p as prompts, pc as colors };
|