@selvajs/cli 2.0.1 → 2.0.2
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 +1 -1
- package/package.json +1 -1
- package/src/commands/create.js +9 -20
- package/src/commands/doctor.js +82 -12
- package/src/commands/migrate.js +95 -34
- package/src/commands/pm2.js +4 -11
- package/src/env.js +7 -1
- package/bin/cli.js +0 -7
- package/bin/selva.js +0 -7
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ CLI for white-label Selva deployments.
|
|
|
8
8
|
npx @selvajs/cli my-deployment
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Interactive scaffolder. Prompts for provider, tenancy, flags, brand name, admin email. Generates `SELVA_HMAC_KEY` + `SELVA_AT_REST_KEY`. Writes `.env`, `
|
|
11
|
+
Interactive scaffolder. Prompts for provider, tenancy, flags, brand name, admin email. Generates `SELVA_HMAC_KEY` + `SELVA_AT_REST_KEY`. Writes `.env`, `ecosystem.config.cjs`, `package.json`. Runs `npm install`.
|
|
12
12
|
|
|
13
13
|
## Operate an existing deployment
|
|
14
14
|
|
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
//
|
|
3
3
|
// What this writes into <dir>:
|
|
4
4
|
// .env merged from runtime's .env.example + prompt values
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// package.json depends on @selvajs/selva + providers
|
|
5
|
+
// ecosystem.config.cjs copied verbatim from runtime templates
|
|
6
|
+
// package.json depends on @selvajs/selva + @selvajs/cli + pm2
|
|
8
7
|
// .selva-version marker for future CLI migrations
|
|
9
8
|
// node_modules/ after `npm install`
|
|
10
9
|
//
|
|
11
10
|
// The runtime templates are the source of truth — we don't carry our own
|
|
12
11
|
// copies in @selvajs/cli. We install @selvajs/selva first, then copy
|
|
13
|
-
// from node_modules/@selvajs/selva/templates/.
|
|
12
|
+
// from node_modules/@selvajs/selva/templates/. Provider selection is
|
|
13
|
+
// env-driven (SELVA_AUTH_PROVIDER etc.) — no selva.config.js is generated.
|
|
14
14
|
|
|
15
15
|
import { writeFileSync, existsSync, mkdirSync, readFileSync, cpSync } from 'node:fs';
|
|
16
16
|
import { resolve, join, basename } from 'node:path';
|
|
@@ -81,7 +81,6 @@ export async function runCreate(argv) {
|
|
|
81
81
|
const envTemplate = readFileSync(join(runtimeTemplates, '.env.example'), 'utf8');
|
|
82
82
|
writeEnvFile(join(targetDir, '.env'), envTemplate, values);
|
|
83
83
|
|
|
84
|
-
cpSync(join(runtimeTemplates, 'selva.config.example.js'), join(targetDir, 'selva.config.js'));
|
|
85
84
|
cpSync(join(runtimeTemplates, 'ecosystem.config.cjs'), join(targetDir, 'ecosystem.config.cjs'));
|
|
86
85
|
|
|
87
86
|
writeFileSync(join(targetDir, '.selva-version'), CLI_VERSION + '\n', 'utf8');
|
|
@@ -257,16 +256,16 @@ function envBool(v) {
|
|
|
257
256
|
}
|
|
258
257
|
|
|
259
258
|
// The deployment's package.json depends on @selvajs/selva (prebuilt
|
|
260
|
-
// SvelteKit app
|
|
261
|
-
//
|
|
259
|
+
// SvelteKit app with all providers bundled in) plus @selvajs/cli (the
|
|
260
|
+
// operator-side tool). Provider selection is env-driven — no provider
|
|
261
|
+
// packages need to be listed here.
|
|
262
262
|
//
|
|
263
|
-
// We
|
|
263
|
+
// We list @selvajs/cli itself as a dep so the `selva` bin gets linked
|
|
264
264
|
// into node_modules/.bin/. Without this the operator's only way to run
|
|
265
265
|
// `selva doctor` / `selva start` is a global install of the CLI.
|
|
266
|
-
function buildPackageJson(name
|
|
266
|
+
function buildPackageJson(name /*, values */) {
|
|
267
267
|
const deps = {
|
|
268
268
|
'@selvajs/cli': 'latest',
|
|
269
|
-
'@selvajs/platform': 'latest',
|
|
270
269
|
'@selvajs/selva': 'latest',
|
|
271
270
|
// pm2 lives in the deployment's own node_modules so `selva start` can
|
|
272
271
|
// resolve it via node_modules/.bin/pm2 without a global install. The
|
|
@@ -274,16 +273,6 @@ function buildPackageJson(name, values) {
|
|
|
274
273
|
pm2: '^5.4.0'
|
|
275
274
|
};
|
|
276
275
|
|
|
277
|
-
const providers = new Set([
|
|
278
|
-
values.SELVA_AUTH_PROVIDER,
|
|
279
|
-
values.SELVA_DATA_PROVIDER,
|
|
280
|
-
values.SELVA_STORAGE_PROVIDER
|
|
281
|
-
]);
|
|
282
|
-
|
|
283
|
-
if (providers.has('local')) deps['@selvajs/local-provider'] = 'latest';
|
|
284
|
-
if (providers.has('supabase')) deps['@selvajs/supabase-provider'] = 'latest';
|
|
285
|
-
if (providers.has('header')) deps['@selvajs/header-auth-provider'] = 'latest';
|
|
286
|
-
|
|
287
276
|
// Use `selva` (resolved via node_modules/.bin) in the npm scripts. Running
|
|
288
277
|
// them as `npm run start` / `npm run doctor` works without remembering the
|
|
289
278
|
// ./node_modules/.bin/ prefix.
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// `selva doctor` — validate a deployment without starting it.
|
|
2
2
|
//
|
|
3
3
|
// Checks:
|
|
4
|
-
// • .env
|
|
4
|
+
// • .env and ecosystem.config.cjs exist
|
|
5
|
+
// • Layout drift (legacy provider packages, stale selva.config.js, etc.)
|
|
5
6
|
// • Secrets are present and look like 32-byte hex
|
|
6
7
|
// • DATA_PATH writable (when local provider is in use)
|
|
7
8
|
// • Supabase URL reachable (when supabase provider is in use)
|
|
8
|
-
// • @selvajs/selva
|
|
9
|
+
// • @selvajs/selva installed
|
|
9
10
|
// • Origin set when behind a reverse proxy looks set
|
|
10
11
|
//
|
|
11
12
|
// Exits 0 (green) or 1 (any red); yellow checks don't fail the run.
|
|
@@ -32,7 +33,6 @@ export async function runDoctor() {
|
|
|
32
33
|
|
|
33
34
|
// ── Files ──────────────────────────────────────────────────────────
|
|
34
35
|
checks.push(checkFile(join(dir, '.env'), '.env present'));
|
|
35
|
-
checks.push(checkFile(join(dir, 'selva.config.js'), 'selva.config.js present'));
|
|
36
36
|
checks.push(checkFile(join(dir, 'ecosystem.config.cjs'), 'ecosystem.config.cjs present'));
|
|
37
37
|
|
|
38
38
|
// ── Layout drift ───────────────────────────────────────────────────
|
|
@@ -47,7 +47,7 @@ export async function runDoctor() {
|
|
|
47
47
|
|
|
48
48
|
// ── Provider wiring ────────────────────────────────────────────────
|
|
49
49
|
// `header` is only valid for the auth slot — data/storage stay
|
|
50
|
-
// local|supabase. Mirror what
|
|
50
|
+
// local|supabase. Mirror what providers.server.ts enforces.
|
|
51
51
|
const providers = {
|
|
52
52
|
auth: (env.SELVA_AUTH_PROVIDER ?? 'local').toLowerCase(),
|
|
53
53
|
data: (env.SELVA_DATA_PROVIDER ?? 'local').toLowerCase(),
|
|
@@ -90,10 +90,9 @@ export async function runDoctor() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// ── Installed packages ─────────────────────────────────────────────
|
|
93
|
+
// Provider implementations are bundled into @selvajs/selva — only the
|
|
94
|
+
// runtime package needs to be on disk.
|
|
93
95
|
checks.push(checkPackage(dir, '@selvajs/selva'));
|
|
94
|
-
if (used.has('local')) checks.push(checkPackage(dir, '@selvajs/local-provider'));
|
|
95
|
-
if (used.has('supabase')) checks.push(checkPackage(dir, '@selvajs/supabase-provider'));
|
|
96
|
-
if (used.has('header')) checks.push(checkPackage(dir, '@selvajs/header-auth-provider'));
|
|
97
96
|
|
|
98
97
|
// ── Origin (best-effort) ───────────────────────────────────────────
|
|
99
98
|
if (env.ORIGIN) {
|
|
@@ -208,18 +207,29 @@ function checkLayoutDrift(dir) {
|
|
|
208
207
|
} catch {
|
|
209
208
|
return red('package.json is not valid JSON');
|
|
210
209
|
}
|
|
211
|
-
const reasons = detectDrift(pkg);
|
|
212
|
-
if (reasons.length === 0) return green('
|
|
210
|
+
const reasons = detectDrift(pkg, dir);
|
|
211
|
+
if (reasons.length === 0) return green('deployment layout is current');
|
|
213
212
|
return red(
|
|
214
|
-
`
|
|
213
|
+
`deployment layout is outdated — run \`selva migrate\`:\n ` +
|
|
215
214
|
reasons.map((r) => '· ' + r).join('\n ')
|
|
216
215
|
);
|
|
217
216
|
}
|
|
218
217
|
|
|
218
|
+
// Defaults must match HeaderAuthProvider.DEFAULT_HEADERS. Duplicated here so
|
|
219
|
+
// doctor doesn't have to load the runtime. If the provider's defaults ever
|
|
220
|
+
// change, update both places — there's a smoke test in providers/header-auth
|
|
221
|
+
// that pins them, so a divergence would surface in CI.
|
|
222
|
+
const DEFAULT_HEADER_NAMES = {
|
|
223
|
+
upn: 'SELVA-UserPrincipalName',
|
|
224
|
+
email: 'SELVA-Email',
|
|
225
|
+
displayName: 'SELVA-DisplayName'
|
|
226
|
+
};
|
|
227
|
+
|
|
219
228
|
// Header-auth-specific sanity checks. None of these catch the truly dangerous
|
|
220
229
|
// misconfigurations (header spoofing, missing proxy auth) — those are
|
|
221
230
|
// runtime invariants we can't verify from here. We DO check the things we can:
|
|
222
|
-
// allowlist file presence, HOST binding, ORIGIN, and
|
|
231
|
+
// allowlist file presence, HOST binding, ORIGIN, bootstrap admin, and the
|
|
232
|
+
// resolved header names so they can be diffed against the proxy config.
|
|
223
233
|
function checkHeaderAuth(dir, env, dataProvider) {
|
|
224
234
|
const out = [];
|
|
225
235
|
|
|
@@ -228,7 +238,8 @@ function checkHeaderAuth(dir, env, dataProvider) {
|
|
|
228
238
|
if (!allowlistDir) {
|
|
229
239
|
out.push(red('HEADER_AUTH_DATA_DIR (or DATA_PATH) unset — provider will fail to start'));
|
|
230
240
|
} else {
|
|
231
|
-
const
|
|
241
|
+
const allowlistAbsDir = resolve(dir, allowlistDir);
|
|
242
|
+
const allowlistPath = join(allowlistAbsDir, 'header-allowlist.json');
|
|
232
243
|
if (existsSync(allowlistPath)) {
|
|
233
244
|
out.push(green(`header-allowlist.json present (${allowlistDir}/header-allowlist.json)`));
|
|
234
245
|
} else {
|
|
@@ -240,6 +251,14 @@ function checkHeaderAuth(dir, env, dataProvider) {
|
|
|
240
251
|
`header-allowlist.json not found at ${allowlistDir}/ — no users will be allowed in until one is added`
|
|
241
252
|
)
|
|
242
253
|
);
|
|
254
|
+
|
|
255
|
+
// If we expect lazy creation, the dir (or its parent) needs to be
|
|
256
|
+
// writable. Only check when HEADER_AUTH_DATA_DIR is set explicitly
|
|
257
|
+
// — when falling back to DATA_PATH the `local` provider's own
|
|
258
|
+
// checkDataPath has already covered the same ground.
|
|
259
|
+
if (env.HEADER_AUTH_DATA_DIR) {
|
|
260
|
+
out.push(checkDirWritable(allowlistAbsDir, `HEADER_AUTH_DATA_DIR=${allowlistDir}`));
|
|
261
|
+
}
|
|
243
262
|
}
|
|
244
263
|
}
|
|
245
264
|
|
|
@@ -288,5 +307,56 @@ function checkHeaderAuth(dir, env, dataProvider) {
|
|
|
288
307
|
out.push(green(`BOOTSTRAP_INSTANCE_ADMIN_EMAIL=${env.BOOTSTRAP_INSTANCE_ADMIN_EMAIL}`));
|
|
289
308
|
}
|
|
290
309
|
|
|
310
|
+
// 6. Resolved header names. The most common header-auth boot symptom is
|
|
311
|
+
// `user:null` because the proxy sets one set of names and the provider
|
|
312
|
+
// reads another. Print what the provider WILL read so the operator can
|
|
313
|
+
// diff it against the Caddyfile / oauth2-proxy config. We don't fail on
|
|
314
|
+
// custom names — operators legitimately override these for non-Caddy
|
|
315
|
+
// proxies — but yellow-flag the case where one is overridden and the
|
|
316
|
+
// others aren't, since a partial override is almost always a typo.
|
|
317
|
+
const resolved = {
|
|
318
|
+
upn: env.HEADER_AUTH_UPN_HEADER || DEFAULT_HEADER_NAMES.upn,
|
|
319
|
+
email: env.HEADER_AUTH_EMAIL_HEADER || DEFAULT_HEADER_NAMES.email,
|
|
320
|
+
displayName: env.HEADER_AUTH_DISPLAY_NAME_HEADER || DEFAULT_HEADER_NAMES.displayName
|
|
321
|
+
};
|
|
322
|
+
const overrides = [
|
|
323
|
+
Boolean(env.HEADER_AUTH_UPN_HEADER),
|
|
324
|
+
Boolean(env.HEADER_AUTH_EMAIL_HEADER),
|
|
325
|
+
Boolean(env.HEADER_AUTH_DISPLAY_NAME_HEADER)
|
|
326
|
+
].filter(Boolean).length;
|
|
327
|
+
const headerList =
|
|
328
|
+
`UPN=${resolved.upn}, Email=${resolved.email}, DisplayName=${resolved.displayName}`;
|
|
329
|
+
if (overrides === 0) {
|
|
330
|
+
out.push(green(`header names (bundled defaults): ${headerList}`));
|
|
331
|
+
} else if (overrides === 3) {
|
|
332
|
+
out.push(green(`header names (all overridden): ${headerList}`));
|
|
333
|
+
} else {
|
|
334
|
+
out.push(
|
|
335
|
+
yellow(
|
|
336
|
+
`header names partially overridden (${overrides}/3 set): ${headerList} — ` +
|
|
337
|
+
`a partial override is usually a typo. Set all three or none.`
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
291
342
|
return out;
|
|
292
343
|
}
|
|
344
|
+
|
|
345
|
+
function checkDirWritable(absDir, label) {
|
|
346
|
+
try {
|
|
347
|
+
if (existsSync(absDir)) {
|
|
348
|
+
const stat = statSync(absDir);
|
|
349
|
+
if (!stat.isDirectory()) return red(`${label} exists but isn't a directory`);
|
|
350
|
+
accessSync(absDir, constants.W_OK);
|
|
351
|
+
return green(`${label} writable`);
|
|
352
|
+
}
|
|
353
|
+
const parent = resolve(absDir, '..');
|
|
354
|
+
if (!existsSync(parent)) {
|
|
355
|
+
return yellow(`${label} doesn't exist yet (parent missing: ${parent})`);
|
|
356
|
+
}
|
|
357
|
+
accessSync(parent, constants.W_OK);
|
|
358
|
+
return yellow(`${label} doesn't exist yet — will be created on first run`);
|
|
359
|
+
} catch {
|
|
360
|
+
return red(`${label} not writable`);
|
|
361
|
+
}
|
|
362
|
+
}
|
package/src/commands/migrate.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// `selva migrate` — bring an existing deployment
|
|
2
|
-
// current layout.
|
|
1
|
+
// `selva migrate` — bring an existing deployment onto the current layout.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
3
|
+
// Three historical migrations exist for Selva deployments:
|
|
5
4
|
// 1. `@selvajs/create` → `@selvajs/cli` (CLI bootstrap; can't be automated
|
|
6
5
|
// since the operator has no `selva` binary yet — they run two npm
|
|
7
6
|
// commands by hand from the Hotfix doc).
|
|
8
7
|
// 2. `@selvajs/runtime` → `@selvajs/selva` (runtime bundling: UI, schemas,
|
|
9
8
|
// ui-kit and the providers' workspace deps are now built into
|
|
10
|
-
// @selvajs/selva).
|
|
9
|
+
// @selvajs/selva).
|
|
10
|
+
// 3. selva.config.js → env-driven providers (the picker logic moved into
|
|
11
|
+
// the runtime; deployments no longer need a config file. Provider
|
|
12
|
+
// packages are bundled into @selvajs/selva.)
|
|
13
|
+
//
|
|
14
|
+
// Migrate automates 2 and 3 together: it rewrites package.json, drops the
|
|
15
|
+
// now-bundled provider packages, removes any stale selva.config.js, and
|
|
16
|
+
// rewrites ecosystem.config.cjs if it still points at @selvajs/runtime.
|
|
11
17
|
//
|
|
12
18
|
// Future package-layout shifts go here too — the command should remain
|
|
13
19
|
// idempotent. On an already-current deployment it prints "nothing to
|
|
@@ -17,12 +23,17 @@
|
|
|
17
23
|
// pm2 again with --update-env. Rollback restores package.json.bak on
|
|
18
24
|
// npm-install failure so the operator isn't left with a broken deployment.
|
|
19
25
|
|
|
20
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
existsSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
copyFileSync,
|
|
31
|
+
rmSync
|
|
32
|
+
} from 'node:fs';
|
|
21
33
|
import { join } from 'node:path';
|
|
22
34
|
import { spawnSync, execSync } from 'node:child_process';
|
|
23
35
|
import * as p from '@clack/prompts';
|
|
24
36
|
import pc from 'picocolors';
|
|
25
|
-
import { readEnvFile } from '../env.js';
|
|
26
37
|
import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
27
38
|
|
|
28
39
|
const APP_NAME = 'selva-compute';
|
|
@@ -35,10 +46,10 @@ const SELVA_DEPS = new Set([
|
|
|
35
46
|
'@selvajs/cli',
|
|
36
47
|
'@selvajs/selva',
|
|
37
48
|
'@selvajs/runtime', // legacy — removed during migrate
|
|
38
|
-
'@selvajs/platform',
|
|
39
|
-
'@selvajs/local-provider',
|
|
40
|
-
'@selvajs/supabase-provider',
|
|
41
|
-
'@selvajs/header-auth-provider',
|
|
49
|
+
'@selvajs/platform', // legacy — bundled into @selvajs/selva
|
|
50
|
+
'@selvajs/local-provider', // legacy — bundled into @selvajs/selva
|
|
51
|
+
'@selvajs/supabase-provider', // legacy — bundled into @selvajs/selva
|
|
52
|
+
'@selvajs/header-auth-provider', // legacy — bundled into @selvajs/selva
|
|
42
53
|
'@selvajs/create' // legacy CLI — removed during migrate
|
|
43
54
|
]);
|
|
44
55
|
|
|
@@ -64,18 +75,30 @@ export async function runMigrate() {
|
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
const before = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
67
|
-
const
|
|
78
|
+
const target = buildTargetPackageJson(before);
|
|
79
|
+
const pkgDiff = diffPackageJson(before, target);
|
|
80
|
+
|
|
81
|
+
// Side-files: selva.config.js (now unused) and ecosystem.config.cjs
|
|
82
|
+
// (must point at @selvajs/selva, not @selvajs/runtime).
|
|
83
|
+
const configPath = join(dir, 'selva.config.js');
|
|
84
|
+
const hasStaleConfig = existsSync(configPath);
|
|
85
|
+
|
|
86
|
+
const ecoPath = join(dir, 'ecosystem.config.cjs');
|
|
87
|
+
const ecoHasStaleRuntime =
|
|
88
|
+
existsSync(ecoPath) && readFileSync(ecoPath, 'utf8').includes('@selvajs/runtime');
|
|
68
89
|
|
|
69
|
-
const
|
|
70
|
-
|
|
90
|
+
const sideFileChanges = [];
|
|
91
|
+
if (hasStaleConfig) sideFileChanges.push(`${pc.red('-')} selva.config.js ${pc.dim('(no longer needed; providers are env-driven)')}`);
|
|
92
|
+
if (ecoHasStaleRuntime) sideFileChanges.push(`${pc.yellow('~')} ecosystem.config.cjs ${pc.dim('(rewrite: @selvajs/runtime → @selvajs/selva)')}`);
|
|
71
93
|
|
|
72
|
-
if (
|
|
94
|
+
if (pkgDiff.length === 0 && sideFileChanges.length === 0) {
|
|
73
95
|
p.outro(pc.green('Already on the current layout — nothing to migrate.'));
|
|
74
96
|
return;
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
p.log.info('Changes to apply:');
|
|
78
|
-
for (const line of
|
|
100
|
+
for (const line of pkgDiff) console.log(' ' + line);
|
|
101
|
+
for (const line of sideFileChanges) console.log(' ' + line);
|
|
79
102
|
|
|
80
103
|
const confirmed = await p.confirm({
|
|
81
104
|
message: 'Apply these changes, reinstall, and restart?',
|
|
@@ -94,12 +117,29 @@ export async function runMigrate() {
|
|
|
94
117
|
p.log.warn('pm2 stop did not succeed — selva-compute may not be running. Continuing.');
|
|
95
118
|
}
|
|
96
119
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
120
|
+
// Back up everything we're about to mutate. Restored on npm-install failure
|
|
121
|
+
// so the operator isn't left with a half-migrated deployment.
|
|
99
122
|
const bakPath = pkgPath + '.bak';
|
|
100
123
|
copyFileSync(pkgPath, bakPath);
|
|
101
124
|
writeFileSync(pkgPath, JSON.stringify(target, null, 2) + '\n', 'utf8');
|
|
102
125
|
|
|
126
|
+
if (hasStaleConfig) {
|
|
127
|
+
copyFileSync(configPath, configPath + '.bak');
|
|
128
|
+
rmSync(configPath, { force: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (ecoHasStaleRuntime) {
|
|
132
|
+
copyFileSync(ecoPath, ecoPath + '.bak');
|
|
133
|
+
// Single-line rewrite: only @selvajs/runtime → @selvajs/selva. We don't
|
|
134
|
+
// regenerate from the canonical template here because the operator may
|
|
135
|
+
// have customized port/memory/cluster settings; preserve their edits.
|
|
136
|
+
const ecoContent = readFileSync(ecoPath, 'utf8').replace(
|
|
137
|
+
/@selvajs\/runtime/g,
|
|
138
|
+
'@selvajs/selva'
|
|
139
|
+
);
|
|
140
|
+
writeFileSync(ecoPath, ecoContent, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
|
|
103
143
|
// Nuke node_modules + lockfile. A simple `npm install` won't always
|
|
104
144
|
// resolve correctly when major version ranges change (e.g. runtime 0.10
|
|
105
145
|
// → selva 2.0) — the lockfile pins old transitive deps that no longer
|
|
@@ -120,6 +160,12 @@ export async function runMigrate() {
|
|
|
120
160
|
} catch (err) {
|
|
121
161
|
s.stop(pc.red('npm install failed — rolling back package.json'));
|
|
122
162
|
copyFileSync(bakPath, pkgPath);
|
|
163
|
+
if (hasStaleConfig && existsSync(configPath + '.bak')) {
|
|
164
|
+
copyFileSync(configPath + '.bak', configPath);
|
|
165
|
+
}
|
|
166
|
+
if (ecoHasStaleRuntime && existsSync(ecoPath + '.bak')) {
|
|
167
|
+
copyFileSync(ecoPath + '.bak', ecoPath);
|
|
168
|
+
}
|
|
123
169
|
// Try to bring the old process back up so we don't leave the operator
|
|
124
170
|
// with downtime. If node_modules was wiped this won't help, but at
|
|
125
171
|
// least package.json matches what's on disk.
|
|
@@ -129,11 +175,15 @@ export async function runMigrate() {
|
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
const status = runPm2(dir, ['start', APP_NAME, '--update-env'], { inherit: false });
|
|
178
|
+
const backupHints = ['package.json.bak'];
|
|
179
|
+
if (hasStaleConfig) backupHints.push('selva.config.js.bak');
|
|
180
|
+
if (ecoHasStaleRuntime) backupHints.push('ecosystem.config.cjs.bak');
|
|
181
|
+
|
|
132
182
|
if (status === 0) {
|
|
133
183
|
p.outro(
|
|
134
184
|
[
|
|
135
185
|
pc.green('Migration complete.'),
|
|
136
|
-
pc.dim('
|
|
186
|
+
pc.dim('Backups saved as ') + pc.cyan(backupHints.join(', ')),
|
|
137
187
|
pc.dim('Run ') + pc.cyan('selva doctor') + pc.dim(' to verify.')
|
|
138
188
|
].join('\n')
|
|
139
189
|
);
|
|
@@ -142,30 +192,22 @@ export async function runMigrate() {
|
|
|
142
192
|
}
|
|
143
193
|
}
|
|
144
194
|
|
|
145
|
-
// Compute what package.json should look like given the current contents
|
|
146
|
-
// the operator's .env (which tells us which providers are in use).
|
|
195
|
+
// Compute what package.json should look like given the current contents.
|
|
147
196
|
//
|
|
148
197
|
// Wholesale-replace semantics: any @selvajs/* or pm2 entry not in our
|
|
149
198
|
// canonical set is dropped. Non-selva deps the operator added are also
|
|
150
199
|
// dropped — that's the design choice we made up-front.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
].filter(Boolean).map((v) => v.toLowerCase())
|
|
158
|
-
);
|
|
159
|
-
|
|
200
|
+
//
|
|
201
|
+
// Provider packages (@selvajs/local-provider etc.) are NOT preserved even
|
|
202
|
+
// when the operator's .env selects them: they're bundled into @selvajs/selva
|
|
203
|
+
// in v2.1+ and the standalone packages are legacy. Providers are picked at
|
|
204
|
+
// runtime from SELVA_*_PROVIDER env vars.
|
|
205
|
+
function buildTargetPackageJson(current) {
|
|
160
206
|
const deps = {
|
|
161
207
|
'@selvajs/cli': 'latest',
|
|
162
208
|
'@selvajs/selva': 'latest',
|
|
163
|
-
'@selvajs/platform': 'latest',
|
|
164
209
|
pm2: '^5.4.0'
|
|
165
210
|
};
|
|
166
|
-
if (providers.has('local')) deps['@selvajs/local-provider'] = 'latest';
|
|
167
|
-
if (providers.has('supabase')) deps['@selvajs/supabase-provider'] = 'latest';
|
|
168
|
-
if (providers.has('header')) deps['@selvajs/header-auth-provider'] = 'latest';
|
|
169
211
|
|
|
170
212
|
return {
|
|
171
213
|
name: current.name ?? 'selva-deployment',
|
|
@@ -227,14 +269,33 @@ function runPm2(dir, args, { inherit = true } = {}) {
|
|
|
227
269
|
|
|
228
270
|
// Exported for use by `selva doctor` so it can warn about layout drift
|
|
229
271
|
// without duplicating the detection logic.
|
|
230
|
-
export function detectDrift(pkgJson) {
|
|
272
|
+
export function detectDrift(pkgJson, dir) {
|
|
231
273
|
const deps = pkgJson?.dependencies ?? {};
|
|
232
274
|
const reasons = [];
|
|
233
275
|
if (deps['@selvajs/runtime']) reasons.push('@selvajs/runtime is the old runtime package');
|
|
234
276
|
if (deps['@selvajs/create']) reasons.push('@selvajs/create is the old CLI package');
|
|
277
|
+
if (deps['@selvajs/platform']) reasons.push('@selvajs/platform is now bundled into @selvajs/selva');
|
|
278
|
+
if (deps['@selvajs/local-provider'])
|
|
279
|
+
reasons.push('@selvajs/local-provider is now bundled into @selvajs/selva');
|
|
280
|
+
if (deps['@selvajs/supabase-provider'])
|
|
281
|
+
reasons.push('@selvajs/supabase-provider is now bundled into @selvajs/selva');
|
|
282
|
+
if (deps['@selvajs/header-auth-provider'])
|
|
283
|
+
reasons.push('@selvajs/header-auth-provider is now bundled into @selvajs/selva');
|
|
235
284
|
if (!deps['@selvajs/selva']) reasons.push('@selvajs/selva is missing');
|
|
236
285
|
if (!deps['@selvajs/cli']) reasons.push('@selvajs/cli is missing');
|
|
237
286
|
if (!deps['pm2']) reasons.push('pm2 is not in dependencies');
|
|
287
|
+
|
|
288
|
+
if (dir) {
|
|
289
|
+
const configPath = join(dir, 'selva.config.js');
|
|
290
|
+
if (existsSync(configPath)) {
|
|
291
|
+
reasons.push('selva.config.js is no longer needed (providers are env-driven)');
|
|
292
|
+
}
|
|
293
|
+
const ecoPath = join(dir, 'ecosystem.config.cjs');
|
|
294
|
+
if (existsSync(ecoPath) && readFileSync(ecoPath, 'utf8').includes('@selvajs/runtime')) {
|
|
295
|
+
reasons.push('ecosystem.config.cjs still references @selvajs/runtime');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
238
299
|
return reasons;
|
|
239
300
|
}
|
|
240
301
|
|
package/src/commands/pm2.js
CHANGED
|
@@ -78,17 +78,10 @@ export async function runUpdate() {
|
|
|
78
78
|
const before = readRuntimeVersion(dir);
|
|
79
79
|
p.log.info(`Current @selvajs/selva: ${before ?? 'unknown'}`);
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
const packages = [
|
|
85
|
-
'@selvajs/cli',
|
|
86
|
-
'@selvajs/selva',
|
|
87
|
-
'@selvajs/platform',
|
|
88
|
-
'@selvajs/local-provider',
|
|
89
|
-
'@selvajs/supabase-provider',
|
|
90
|
-
'@selvajs/header-auth-provider'
|
|
91
|
-
];
|
|
81
|
+
// Providers are bundled into @selvajs/selva — the only @selvajs/* packages
|
|
82
|
+
// an operator install carries are the runtime and the CLI. The admin-center
|
|
83
|
+
// "Run update" button runs the same list — keep them in sync if you edit.
|
|
84
|
+
const packages = ['@selvajs/cli', '@selvajs/selva'];
|
|
92
85
|
|
|
93
86
|
const confirmed = await p.confirm({
|
|
94
87
|
message: 'Refresh all @selvajs/* packages and restart the app?',
|
package/src/env.js
CHANGED
|
@@ -78,7 +78,13 @@ export function mergeEnv(template, values) {
|
|
|
78
78
|
out.push(...appended);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
// Always end with a single trailing newline. Without it, a later
|
|
82
|
+
// `echo VAR=value >> .env` concatenates onto the last line, producing
|
|
83
|
+
// `HOST=127.0.0.1HEADER_AUTH_DATA_DIR=...` and a `getaddrinfo ENOTFOUND`
|
|
84
|
+
// boot crash. Strip any existing trailing blanks first so we don't grow
|
|
85
|
+
// the file by a line on each rewrite.
|
|
86
|
+
while (out.length > 0 && out[out.length - 1] === '') out.pop();
|
|
87
|
+
return out.join('\n') + '\n';
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export function writeEnvFile(path, template, values) {
|
package/bin/cli.js
DELETED