@selvajs/cli 4.8.1 → 4.8.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/package.json +1 -1
- package/src/commands/create.js +8 -2
- package/src/commands/doctor.js +3 -2
- package/src/commands/migrate.js +29 -4
- package/src/deployment-package.js +49 -7
- package/src/prompts.js +63 -38
package/package.json
CHANGED
package/src/commands/create.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../paths.js';
|
|
18
18
|
import {
|
|
19
19
|
buildDeploymentPackageJson,
|
|
20
|
+
needsSupabaseProvider,
|
|
20
21
|
npmDistTagVersion,
|
|
21
22
|
resolveSelvaPins
|
|
22
23
|
} from '../deployment-package.js';
|
|
@@ -65,12 +66,17 @@ export async function runCreate(argv) {
|
|
|
65
66
|
// `"latest"` re-resolves on every later `npm install`, so the deployment
|
|
66
67
|
// would follow the tag instead of the version it was scaffolded against —
|
|
67
68
|
// and `selva doctor` reports a floating pin as drift.
|
|
68
|
-
const
|
|
69
|
+
const supabase = needsSupabaseProvider(values);
|
|
70
|
+
const { pins } = resolveSelvaPins(
|
|
71
|
+
{},
|
|
72
|
+
npmDistTagVersion,
|
|
73
|
+
supabase ? ['@selvajs/supabase-provider'] : []
|
|
74
|
+
);
|
|
69
75
|
|
|
70
76
|
// package.json first: npm install needs it, and the runtime templates below
|
|
71
77
|
// only exist once @selvajs/selva is installed into node_modules.
|
|
72
78
|
const pkgJson = JSON.stringify(
|
|
73
|
-
buildDeploymentPackageJson({ name: deployName, dependencies: pins }),
|
|
79
|
+
buildDeploymentPackageJson({ name: deployName, dependencies: pins, supabase }),
|
|
74
80
|
null,
|
|
75
81
|
2
|
|
76
82
|
);
|
package/src/commands/doctor.js
CHANGED
|
@@ -229,8 +229,9 @@ async function checkSupabaseMigrations(dir, env) {
|
|
|
229
229
|
// Provider package not installed in this deployment.
|
|
230
230
|
}
|
|
231
231
|
if (!expected) {
|
|
232
|
-
return
|
|
233
|
-
'@selvajs/supabase-provider
|
|
232
|
+
return red(
|
|
233
|
+
'@selvajs/supabase-provider is not installed, so the migration SQL is missing. ' +
|
|
234
|
+
'Run `selva migrate` to add it, then `npm install`.'
|
|
234
235
|
);
|
|
235
236
|
}
|
|
236
237
|
|
package/src/commands/migrate.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DEPENDENCIES,
|
|
16
16
|
isFloatingPin,
|
|
17
17
|
LEGACY_DEPENDENCIES,
|
|
18
|
+
needsSupabaseProvider,
|
|
18
19
|
npmDistTagVersion,
|
|
19
20
|
OVERRIDES,
|
|
20
21
|
resolveSelvaPins,
|
|
@@ -122,7 +123,12 @@ export async function runMigrate(
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
const before = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
125
|
-
const
|
|
126
|
+
const deploymentEnv = readDeploymentEnv(join(dir, '.env'));
|
|
127
|
+
const { pkg: target, notes: pinNotes } = buildTargetPackageJson(
|
|
128
|
+
before,
|
|
129
|
+
resolveVersion,
|
|
130
|
+
deploymentEnv
|
|
131
|
+
);
|
|
126
132
|
const pkgDiff = diffPackageJson(before, target);
|
|
127
133
|
|
|
128
134
|
const configPath = join(dir, 'selva.config.js');
|
|
@@ -339,13 +345,32 @@ function restartAfterRollback(dir, pm2 = runPm2) {
|
|
|
339
345
|
}
|
|
340
346
|
}
|
|
341
347
|
|
|
342
|
-
|
|
343
|
-
|
|
348
|
+
// Only the provider keys matter here, so this reads `KEY=value` lines rather
|
|
349
|
+
// than pulling in a dotenv parser: no quoting, expansion, or multi-line values
|
|
350
|
+
// to honour.
|
|
351
|
+
function readDeploymentEnv(envPath) {
|
|
352
|
+
if (!existsSync(envPath)) return {};
|
|
353
|
+
const env = {};
|
|
354
|
+
for (const line of readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
|
355
|
+
const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line);
|
|
356
|
+
if (match) env[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
|
|
357
|
+
}
|
|
358
|
+
return env;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function buildTargetPackageJson(current, resolveVersion = npmDistTagVersion, env = {}) {
|
|
362
|
+
const supabase = needsSupabaseProvider(env);
|
|
363
|
+
const { pins, notes } = resolveSelvaPins(
|
|
364
|
+
current.dependencies,
|
|
365
|
+
resolveVersion,
|
|
366
|
+
supabase ? ['@selvajs/supabase-provider'] : []
|
|
367
|
+
);
|
|
344
368
|
const pkg = buildDeploymentPackageJson({
|
|
345
369
|
name: current.name ?? 'selva-deployment',
|
|
346
370
|
version: current.version ?? '0.1.0',
|
|
347
371
|
engines: current.engines,
|
|
348
|
-
dependencies: pins
|
|
372
|
+
dependencies: pins,
|
|
373
|
+
supabase
|
|
349
374
|
});
|
|
350
375
|
return { pkg, notes };
|
|
351
376
|
}
|
|
@@ -64,7 +64,35 @@ const DEPENDENCIES = {
|
|
|
64
64
|
pm2: PM2_VERSION
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Installed on top of `DEPENDENCIES` when a Supabase provider is selected.
|
|
69
|
+
*
|
|
70
|
+
* The provider's *code* is bundled into `@selvajs/selva`, so the app runs
|
|
71
|
+
* without this. Its migration SQL is not: `@selvajs/selva` publishes only
|
|
72
|
+
* `build` + `templates`, while the SQL and the `selva-supabase` bin ship in
|
|
73
|
+
* this package's own tarball. Without it, a deployment has no way to reach the
|
|
74
|
+
* schema it needs — `npx selva-supabase sync-migrations` resolves nothing and
|
|
75
|
+
* `doctor`'s migration-head check finds no directory to compare against.
|
|
76
|
+
*/
|
|
77
|
+
export const SUPABASE_DEPENDENCIES = {
|
|
78
|
+
'@selvajs/supabase-provider': 'latest'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** True when any provider slot is set to supabase. */
|
|
82
|
+
export function needsSupabaseProvider(env) {
|
|
83
|
+
return ['SELVA_AUTH_PROVIDER', 'SELVA_DATA_PROVIDER', 'SELVA_STORAGE_PROVIDER'].some(
|
|
84
|
+
(key) => env?.[key] === 'supabase'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The `@selvajs/*` packages whose pin belongs to the operator, not the scaffold.
|
|
90
|
+
*
|
|
91
|
+
* `@selvajs/supabase-provider` is deliberately absent: it's conditional, and
|
|
92
|
+
* `resolveSelvaPins` runs before anything knows which providers a deployment
|
|
93
|
+
* uses. Its pin is resolved alongside these only when it's actually installed
|
|
94
|
+
* (see `resolveSelvaPins`' `extra` argument).
|
|
95
|
+
*/
|
|
68
96
|
export const SELVA_PACKAGES = ['@selvajs/cli', '@selvajs/selva'];
|
|
69
97
|
|
|
70
98
|
/**
|
|
@@ -117,7 +145,6 @@ export const LEGACY_DEPENDENCIES = {
|
|
|
117
145
|
'@selvajs/create': 'the old CLI package',
|
|
118
146
|
'@selvajs/platform': 'now bundled into @selvajs/selva',
|
|
119
147
|
'@selvajs/local-provider': 'now bundled into @selvajs/selva',
|
|
120
|
-
'@selvajs/supabase-provider': 'now bundled into @selvajs/selva',
|
|
121
148
|
'@selvajs/header-auth-provider': 'now bundled into @selvajs/selva'
|
|
122
149
|
};
|
|
123
150
|
|
|
@@ -139,14 +166,29 @@ export const LEGACY_DEPENDENCIES = {
|
|
|
139
166
|
* an operator choice too. `migrate` passes the resolved `@selvajs/*` pins in
|
|
140
167
|
* here; a fresh scaffold passes none and inherits the defaults.
|
|
141
168
|
*/
|
|
142
|
-
export function buildDeploymentPackageJson({
|
|
169
|
+
export function buildDeploymentPackageJson({
|
|
170
|
+
name,
|
|
171
|
+
version = '0.1.0',
|
|
172
|
+
engines,
|
|
173
|
+
dependencies,
|
|
174
|
+
supabase = false
|
|
175
|
+
}) {
|
|
176
|
+
const base = supabase ? { ...DEPENDENCIES, ...SUPABASE_DEPENDENCIES } : { ...DEPENDENCIES };
|
|
177
|
+
// A caller-supplied pin wins, but only for a package the deployment actually
|
|
178
|
+
// carries: an operator's stale supabase pin must not reinstate the dependency
|
|
179
|
+
// on a deployment that has since moved off supabase.
|
|
180
|
+
const resolved = { ...base };
|
|
181
|
+
for (const [pkgName, pin] of Object.entries(dependencies ?? {})) {
|
|
182
|
+
if (pkgName in base) resolved[pkgName] = pin;
|
|
183
|
+
}
|
|
184
|
+
|
|
143
185
|
const pkg = {
|
|
144
186
|
name: sanitizePackageName(name),
|
|
145
187
|
version,
|
|
146
188
|
private: true,
|
|
147
189
|
type: 'module',
|
|
148
190
|
scripts: { ...SCRIPTS },
|
|
149
|
-
dependencies:
|
|
191
|
+
dependencies: resolved,
|
|
150
192
|
overrides: { ...OVERRIDES }
|
|
151
193
|
};
|
|
152
194
|
if (engines && typeof engines === 'object') pkg.engines = { ...engines };
|
|
@@ -186,11 +228,11 @@ export function npmDistTagVersion(name, tag) {
|
|
|
186
228
|
* so migrate can report it rather than inventing a version or aborting a
|
|
187
229
|
* migration whose other half (layout, overrides, env renames) is still valid.
|
|
188
230
|
*/
|
|
189
|
-
export function resolveSelvaPins(currentDeps, resolveVersion) {
|
|
231
|
+
export function resolveSelvaPins(currentDeps, resolveVersion, extra = []) {
|
|
190
232
|
const pins = {};
|
|
191
233
|
const notes = [];
|
|
192
234
|
|
|
193
|
-
for (const name of SELVA_PACKAGES) {
|
|
235
|
+
for (const name of [...SELVA_PACKAGES, ...extra]) {
|
|
194
236
|
const current = currentDeps?.[name];
|
|
195
237
|
|
|
196
238
|
if (isPrereleasePin(current)) {
|
|
@@ -214,7 +256,7 @@ export function resolveSelvaPins(currentDeps, resolveVersion) {
|
|
|
214
256
|
// Nothing resolvable and nothing worth keeping — fall back to the tag so
|
|
215
257
|
// the install still succeeds. Rare: offline *and* a floating pin.
|
|
216
258
|
if (isFloatingPin(current)) {
|
|
217
|
-
pins[name] = DEPENDENCIES[name];
|
|
259
|
+
pins[name] = DEPENDENCIES[name] ?? SUPABASE_DEPENDENCIES[name];
|
|
218
260
|
if (!notes.some((n) => n.name === name)) {
|
|
219
261
|
notes.push({ name, kind: 'unresolved', pin: current, reason: 'no version returned' });
|
|
220
262
|
}
|
package/src/prompts.js
CHANGED
|
@@ -5,6 +5,52 @@ import pc from 'picocolors';
|
|
|
5
5
|
|
|
6
6
|
const TRUTHY = new Set(['true', '1', 'yes']);
|
|
7
7
|
|
|
8
|
+
// Mirrors FLAG_KEYS in @selvajs/server's create-selva-providers.ts. Every key
|
|
9
|
+
// here is written to .env; `promptable: false` only hides it from the wizard.
|
|
10
|
+
const FLAG_OPTIONS = [
|
|
11
|
+
{
|
|
12
|
+
value: 'ALLOW_ORG_CREATION',
|
|
13
|
+
label: 'Allow non-admin users to create their own orgs',
|
|
14
|
+
hint: 'multi-tenant self-service',
|
|
15
|
+
multiOnly: true,
|
|
16
|
+
// No route consults this flag yet — self-service org creation has not
|
|
17
|
+
// shipped. Offering it would promise behaviour nothing implements.
|
|
18
|
+
promptable: false
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: 'ALLOW_CROSS_ORG_PUBLIC',
|
|
22
|
+
label: 'Public projects visible across all orgs',
|
|
23
|
+
hint: 'instance-wide discovery',
|
|
24
|
+
multiOnly: true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: 'ALLOW_ORG_COMPUTE_OVERRIDE',
|
|
28
|
+
label: 'Orgs can configure their own Rhino.Compute server',
|
|
29
|
+
hint: 'BYO compute',
|
|
30
|
+
multiOnly: true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
value: 'ENABLE_PLATFORM_PROJECTS',
|
|
34
|
+
label: 'Platform projects (admin-owned, granted to orgs/users)',
|
|
35
|
+
hint: 'cross-org sharing without membership'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: 'ENABLE_SHARING',
|
|
39
|
+
label: 'Per-definition share links (anonymous external access)',
|
|
40
|
+
hint: 'tokenized URLs'
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const FLAG_NAMES = FLAG_OPTIONS.map((o) => o.value);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Flags worth offering for a given tenancy. Cross-org flags are hidden under
|
|
48
|
+
* `single`, where one org makes both settings describe the same set.
|
|
49
|
+
*/
|
|
50
|
+
export function promptableFlags(tenancy) {
|
|
51
|
+
return FLAG_OPTIONS.filter((o) => o.promptable !== false && (tenancy === 'multi' || !o.multiOnly));
|
|
52
|
+
}
|
|
53
|
+
|
|
8
54
|
function envBool(v) {
|
|
9
55
|
if (v === undefined) return false;
|
|
10
56
|
return TRUTHY.has(String(v).toLowerCase());
|
|
@@ -95,14 +141,10 @@ export function collectConfigFromEnv(env = process.env) {
|
|
|
95
141
|
}
|
|
96
142
|
values.ORIGIN = origin;
|
|
97
143
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'ENABLE_PLATFORM_PROJECTS',
|
|
103
|
-
'ENABLE_SHARING'
|
|
104
|
-
];
|
|
105
|
-
for (const f of flagNames) {
|
|
144
|
+
// An explicitly-set flag is preserved verbatim even when tenancy makes it
|
|
145
|
+
// inert: rewriting it to '' would hide the operator's misconfiguration
|
|
146
|
+
// rather than fix it, and the server already reads every key as opt-in.
|
|
147
|
+
for (const f of FLAG_NAMES) {
|
|
106
148
|
const key = `SELVA_FLAG_${f}`;
|
|
107
149
|
values[key] = envBool(env[key]) ? 'true' : '';
|
|
108
150
|
}
|
|
@@ -425,33 +467,7 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
425
467
|
}
|
|
426
468
|
}
|
|
427
469
|
|
|
428
|
-
const flagOptions =
|
|
429
|
-
{
|
|
430
|
-
value: 'ALLOW_ORG_CREATION',
|
|
431
|
-
label: 'Allow non-admin users to create their own orgs',
|
|
432
|
-
hint: 'multi-tenant self-service'
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
value: 'ALLOW_CROSS_ORG_PUBLIC',
|
|
436
|
-
label: 'Public projects visible across all orgs',
|
|
437
|
-
hint: 'instance-wide discovery'
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
value: 'ALLOW_ORG_COMPUTE_OVERRIDE',
|
|
441
|
-
label: 'Orgs can configure their own Rhino.Compute server',
|
|
442
|
-
hint: 'BYO compute'
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
value: 'ENABLE_PLATFORM_PROJECTS',
|
|
446
|
-
label: 'Platform projects (admin-owned, granted to orgs/users)',
|
|
447
|
-
hint: 'cross-org sharing without membership'
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
value: 'ENABLE_SHARING',
|
|
451
|
-
label: 'Per-definition share links (anonymous external access)',
|
|
452
|
-
hint: 'tokenized URLs'
|
|
453
|
-
}
|
|
454
|
-
];
|
|
470
|
+
const flagOptions = promptableFlags(tenancy);
|
|
455
471
|
|
|
456
472
|
const flagDefaults = flagOptions
|
|
457
473
|
.map((o) => o.value)
|
|
@@ -475,9 +491,18 @@ export async function collectConfig({ defaults = {}, mode = 'create' } = {}) {
|
|
|
475
491
|
...providerValues
|
|
476
492
|
};
|
|
477
493
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
494
|
+
// Every key is written, including the ones the prompt hid — a missing key
|
|
495
|
+
// reads as "unset" downstream, which is indistinguishable from "removed".
|
|
496
|
+
// A hidden flag keeps whatever the existing .env had; only offered flags
|
|
497
|
+
// are decided by the multiselect, so `init` can't silently clear one.
|
|
498
|
+
const offered = new Set(flagOptions.map((o) => o.value));
|
|
499
|
+
for (const name of FLAG_NAMES) {
|
|
500
|
+
const key = `SELVA_FLAG_${name}`;
|
|
501
|
+
if (!offered.has(name)) {
|
|
502
|
+
values[key] = envBool(defaults[key]) ? 'true' : '';
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
values[key] = Array.isArray(flags) && flags.includes(name) ? 'true' : '';
|
|
481
506
|
}
|
|
482
507
|
|
|
483
508
|
return values;
|