@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.
@@ -1,22 +1,42 @@
1
- // Scaffold a fresh deployment: write .env (merged from template + prompts),
2
- // ecosystem.config.cjs, package.json, and bootstrap node_modules. Runtime
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 { isEmptyOrMissing } from '../paths.js';
14
-
15
- const CLI_VERSION = '0.1.0';
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
- // Write package.json, then install (need templates from @selvajs/selva).
43
- const pkgJson = buildPackageJson(deployName, values);
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
- // Copy templates from installed runtime.
55
- const runtimeTemplates = join(targetDir, 'node_modules', '@selvajs', 'selva', 'templates');
56
- if (skipInstall || !existsSync(runtimeTemplates)) {
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 ${runtimeTemplates}. ` +
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'), CLI_VERSION + '\n', 'utf8');
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
- const envTemplate = readFileSync(join(runtimeTemplates, '.env.example'), 'utf8');
67
- writeEnvFile(join(targetDir, '.env'), envTemplate, values);
85
+ writeEnvFile(join(targetDir, '.env'), readFileSync(envTemplatePath, 'utf8'), values);
68
86
 
69
- cpSync(join(runtimeTemplates, 'ecosystem.config.cjs'), join(targetDir, 'ecosystem.config.cjs'));
87
+ cpSync(ecosystemTemplatePath, join(targetDir, 'ecosystem.config.cjs'));
70
88
 
71
- writeFileSync(join(targetDir, '.selva-version'), CLI_VERSION + '\n', 'utf8');
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
- // Spawn with --loglevel=info for reify: progress; pipe stdout/stderr for live updates.
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
- // Show what npm actually said. Without this the operator has to
165
- // dig through ~/.npm/_logs/*-debug-0.log to find a single line.
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
- "workspace:* / catalog: specs) and has been unpublished. Your local npm\n" +
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;