@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
package/src/commands/create.js
CHANGED
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
//
|
|
2
|
-
// ecosystem.config.cjs
|
|
3
|
-
// templates are the source of truth; providers are env-driven.
|
|
1
|
+
// Scaffolds a fresh deployment: package.json, node_modules, .env (merged from
|
|
2
|
+
// template + prompts), and ecosystem.config.cjs.
|
|
4
3
|
|
|
5
4
|
import { writeFileSync, existsSync, mkdirSync, readFileSync, cpSync } from 'node:fs';
|
|
6
|
-
import { resolve, join, basename } from 'node:path';
|
|
5
|
+
import { resolve, join, basename, dirname } from 'node:path';
|
|
7
6
|
import { spawn } from 'node:child_process';
|
|
8
7
|
import * as p from '@clack/prompts';
|
|
9
8
|
import pc from 'picocolors';
|
|
10
9
|
import { collectConfig, collectConfigFromEnv } from '../prompts.js';
|
|
11
10
|
import { generateKey } from '../secrets.js';
|
|
12
11
|
import { writeEnvFile } from '../env.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
import {
|
|
13
|
+
isEmptyOrMissing,
|
|
14
|
+
requiredNodeRange,
|
|
15
|
+
runtimeTemplatePath,
|
|
16
|
+
scaffoldVersion
|
|
17
|
+
} from '../paths.js';
|
|
18
|
+
import { buildDeploymentPackageJson } from '../deployment-package.js';
|
|
19
|
+
import { satisfiesNodeRange } from '../node-range.js';
|
|
16
20
|
|
|
17
21
|
export async function runCreate(argv) {
|
|
18
22
|
const { dir: rawDir, force, skipInstall, yes } = parseArgs(argv);
|
|
19
23
|
|
|
24
|
+
// Scaffolding under too old a Node installs and passes the health check,
|
|
25
|
+
// then fails only under real traffic — npm treats an engine mismatch as a
|
|
26
|
+
// warning, not an error, unless engine-strict is set, which no deployment
|
|
27
|
+
// sets. Refuse now, while the operator is still choosing their environment.
|
|
28
|
+
const required = requiredNodeRange();
|
|
29
|
+
if (required && satisfiesNodeRange(process.versions.node, required) === false) {
|
|
30
|
+
console.error(
|
|
31
|
+
`${pc.red('✗')} Selva requires Node ${required}, but this shell runs ` +
|
|
32
|
+
`v${process.versions.node}.\n` +
|
|
33
|
+
` A deployment scaffolded here would install and start, then fail once real\n` +
|
|
34
|
+
` requests hit newer Node APIs. Upgrade Node first (nvm, fnm, or your package\n` +
|
|
35
|
+
` manager), then re-run this command.`
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
const targetDir = resolve(rawDir);
|
|
21
41
|
if (!isEmptyOrMissing(targetDir) && !force) {
|
|
22
42
|
console.error(
|
|
@@ -27,20 +47,19 @@ export async function runCreate(argv) {
|
|
|
27
47
|
}
|
|
28
48
|
mkdirSync(targetDir, { recursive: true });
|
|
29
49
|
|
|
30
|
-
// --yes or CI=1 skips prompts (unattended bootstrap).
|
|
31
50
|
const nonInteractive = yes || envBool(process.env.CI);
|
|
32
51
|
const values = nonInteractive
|
|
33
52
|
? collectConfigFromEnv(process.env)
|
|
34
53
|
: await collectConfig({ defaults: {}, mode: 'create' });
|
|
35
54
|
|
|
36
|
-
// Generate fresh secrets (stable across restarts; init refuses to regen).
|
|
37
55
|
values.SELVA_HMAC_KEY = generateKey();
|
|
38
56
|
values.SELVA_AT_REST_KEY = generateKey();
|
|
39
57
|
|
|
40
58
|
const deployName = basename(targetDir);
|
|
41
59
|
|
|
42
|
-
//
|
|
43
|
-
|
|
60
|
+
// package.json first: npm install needs it, and the runtime templates below
|
|
61
|
+
// only exist once @selvajs/selva is installed into node_modules.
|
|
62
|
+
const pkgJson = JSON.stringify(buildDeploymentPackageJson({ name: deployName }), null, 2);
|
|
44
63
|
writeFileSync(join(targetDir, 'package.json'), pkgJson + '\n', 'utf8');
|
|
45
64
|
|
|
46
65
|
if (!skipInstall) {
|
|
@@ -51,24 +70,23 @@ export async function runCreate(argv) {
|
|
|
51
70
|
);
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
if (skipInstall || !existsSync(
|
|
73
|
+
const envTemplatePath = runtimeTemplatePath(targetDir, '.env.example');
|
|
74
|
+
const ecosystemTemplatePath = runtimeTemplatePath(targetDir, 'ecosystem.config.cjs');
|
|
75
|
+
if (skipInstall || !existsSync(envTemplatePath)) {
|
|
57
76
|
p.log.warn(
|
|
58
|
-
`Couldn't read runtime templates from ${
|
|
77
|
+
`Couldn't read runtime templates from ${dirname(envTemplatePath)}. ` +
|
|
59
78
|
`Run \`npm install\`, then \`selva init\` to finish setup.`
|
|
60
79
|
);
|
|
61
|
-
writeFileSync(join(targetDir, '.selva-version'),
|
|
80
|
+
writeFileSync(join(targetDir, '.selva-version'), scaffoldVersion() + '\n', 'utf8');
|
|
62
81
|
p.outro(`Partial scaffold at ${pc.cyan(targetDir)}.`);
|
|
63
82
|
return;
|
|
64
83
|
}
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
writeEnvFile(join(targetDir, '.env'), envTemplate, values);
|
|
85
|
+
writeEnvFile(join(targetDir, '.env'), readFileSync(envTemplatePath, 'utf8'), values);
|
|
68
86
|
|
|
69
|
-
cpSync(
|
|
87
|
+
cpSync(ecosystemTemplatePath, join(targetDir, 'ecosystem.config.cjs'));
|
|
70
88
|
|
|
71
|
-
writeFileSync(join(targetDir, '.selva-version'),
|
|
89
|
+
writeFileSync(join(targetDir, '.selva-version'), scaffoldVersion() + '\n', 'utf8');
|
|
72
90
|
writeGitignore(targetDir);
|
|
73
91
|
|
|
74
92
|
p.outro(
|
|
@@ -87,21 +105,17 @@ export async function runCreate(argv) {
|
|
|
87
105
|
);
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
// Stream npm output with live progress; on failure, show tail for debugging.
|
|
91
|
-
// Cache-bust hint: surface broken versions like @selvajs/selva@0.10.2.
|
|
92
108
|
function runNpmInstall(cwd) {
|
|
93
109
|
return new Promise((resolveP, rejectP) => {
|
|
94
110
|
const s = p.spinner();
|
|
95
111
|
s.start('Installing dependencies (this can take a minute)');
|
|
96
112
|
|
|
97
|
-
// Keep last 80 lines for failure output.
|
|
98
113
|
const tail = [];
|
|
99
114
|
const remember = (line) => {
|
|
100
115
|
tail.push(line);
|
|
101
116
|
if (tail.length > 80) tail.shift();
|
|
102
117
|
};
|
|
103
118
|
|
|
104
|
-
// Extract progress milestones (reify: lines, package count).
|
|
105
119
|
const updateProgress = (line) => {
|
|
106
120
|
const trimmed = line.trim();
|
|
107
121
|
if (!trimmed) return;
|
|
@@ -117,7 +131,7 @@ function runNpmInstall(cwd) {
|
|
|
117
131
|
if (/^npm (WARN|error|notice)/.test(trimmed)) return;
|
|
118
132
|
};
|
|
119
133
|
|
|
120
|
-
//
|
|
134
|
+
// --loglevel=info is what makes npm print the reify: lines updateProgress matches on.
|
|
121
135
|
const child = spawn('npm', ['install', '--loglevel=info'], {
|
|
122
136
|
cwd,
|
|
123
137
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -161,8 +175,8 @@ function runNpmInstall(cwd) {
|
|
|
161
175
|
}
|
|
162
176
|
s.stop(pc.red(`npm install failed (exit ${code})`));
|
|
163
177
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
178
|
+
// Without this the operator has to dig through ~/.npm/_logs/*-debug-0.log
|
|
179
|
+
// to find out what actually went wrong.
|
|
166
180
|
console.error('');
|
|
167
181
|
console.error(pc.dim('── last lines of npm output ──'));
|
|
168
182
|
for (const line of tail) console.error(line);
|
|
@@ -173,7 +187,7 @@ function runNpmInstall(cwd) {
|
|
|
173
187
|
console.error(
|
|
174
188
|
pc.yellow(
|
|
175
189
|
'npm resolved @selvajs/selva@0.10.2 — that version is broken (unresolved\n' +
|
|
176
|
-
|
|
190
|
+
'workspace:* / catalog: specs) and has been unpublished. Your local npm\n' +
|
|
177
191
|
'cache is stale. Clear it and retry:\n\n' +
|
|
178
192
|
' npm cache clean --force\n' +
|
|
179
193
|
' rm -rf node_modules package-lock.json\n' +
|
|
@@ -205,9 +219,7 @@ function parseArgs(argv) {
|
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
if (!dir) {
|
|
208
|
-
throw new Error(
|
|
209
|
-
'Usage: npx @selvajs/cli <directory> [--force] [--skip-install] [--yes]'
|
|
210
|
-
);
|
|
222
|
+
throw new Error('Usage: npx @selvajs/cli <directory> [--force] [--skip-install] [--yes]');
|
|
211
223
|
}
|
|
212
224
|
return { dir, force, skipInstall, yes };
|
|
213
225
|
}
|
|
@@ -217,44 +229,6 @@ function envBool(v) {
|
|
|
217
229
|
return ['1', 'true', 'yes'].includes(String(v).toLowerCase());
|
|
218
230
|
}
|
|
219
231
|
|
|
220
|
-
// Depends on @selvajs/selva (prebuilt) + @selvajs/cli (operator tool).
|
|
221
|
-
// @selvajs/cli links the `selva` bin; without it, only global CLI works.
|
|
222
|
-
function buildPackageJson(name /*, values */) {
|
|
223
|
-
const deps = {
|
|
224
|
-
'@selvajs/cli': 'latest',
|
|
225
|
-
'@selvajs/selva': 'latest',
|
|
226
|
-
// Deployment-local pm2 (pinned exact to prevent daemon version skew).
|
|
227
|
-
pm2: '5.4.3'
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const pkg = {
|
|
231
|
-
name: sanitizePackageName(name),
|
|
232
|
-
version: '0.1.0',
|
|
233
|
-
private: true,
|
|
234
|
-
type: 'module',
|
|
235
|
-
scripts: {
|
|
236
|
-
start: 'selva start',
|
|
237
|
-
stop: 'selva stop',
|
|
238
|
-
restart: 'selva restart',
|
|
239
|
-
logs: 'selva logs',
|
|
240
|
-
doctor: 'selva doctor',
|
|
241
|
-
update: 'selva update'
|
|
242
|
-
},
|
|
243
|
-
dependencies: deps
|
|
244
|
-
};
|
|
245
|
-
return JSON.stringify(pkg, null, 2);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function sanitizePackageName(name) {
|
|
249
|
-
// npm package names: lowercase, no spaces, limited punctuation.
|
|
250
|
-
return (
|
|
251
|
-
name
|
|
252
|
-
.toLowerCase()
|
|
253
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
254
|
-
.replace(/^[-._]+|[-._]+$/g, '') || 'selva-deployment'
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
232
|
function writeGitignore(dir) {
|
|
259
233
|
const path = join(dir, '.gitignore');
|
|
260
234
|
if (existsSync(path)) return;
|