@penvhq/cli 0.8.0 → 0.9.0
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/dist/bin.cjs +7006 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +8 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-ASO5CF5D.js +7252 -0
- package/dist/chunk-ASO5CF5D.js.map +1 -0
- package/dist/index.cjs +3094 -1490
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +503 -38
- package/dist/index.d.ts +503 -38
- package/dist/index.js +57 -5539
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import * as citty from 'citty';
|
|
2
|
-
import { ProjectionProvider, Provider, ParameterRef, Scope, AnyProvider, RotationMechanism, RotationState } from '@penvhq/core';
|
|
2
|
+
import { ProjectionProvider, Provider, PenvConfig, DotenvEntry, ParameterRef, Scope, DotenvDiagnostic, Resolution, AnyProvider, RotationMechanism, RotationState } from '@penvhq/core';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `penv artifact build` — the sealed deployment artifact CI hands a release.
|
|
7
|
+
*
|
|
8
|
+
* It is the third step of the sequence PRD §7 names: pull the target
|
|
9
|
+
* environment, validate it, build the artifact, mount it. Each step is a command
|
|
10
|
+
* with one job, and this one's is narrow on purpose — it does not re-reach a
|
|
11
|
+
* verdict `penv validate` already reaches, because two implementations of "is
|
|
12
|
+
* this configuration good" would eventually let a release be built that CI had
|
|
13
|
+
* already rejected.
|
|
14
|
+
*
|
|
15
|
+
* Two properties are the whole design:
|
|
16
|
+
*
|
|
17
|
+
* **It never decrypts.** A sealed value is copied ciphertext-and-address into
|
|
18
|
+
* the artifact exactly as the record holds it, so building needs no key at all —
|
|
19
|
+
* CI can produce a production artifact without ever being able to read one. The
|
|
20
|
+
* AAD is still the value file's full name, so the ciphertext stays bound to the
|
|
21
|
+
* scope it was sealed at (invariant 17).
|
|
22
|
+
*
|
|
23
|
+
* **It refuses to guess the environment.** `--env` is named explicitly and never
|
|
24
|
+
* defaulted, `--out` likewise. An artifact built for "whatever the default was"
|
|
25
|
+
* is a release that ships the wrong environment's credentials, and the default
|
|
26
|
+
* that made it is one config edit nobody reviewed.
|
|
27
|
+
*/
|
|
28
|
+
interface ArtifactBuildOptions {
|
|
29
|
+
readonly cwd: string;
|
|
30
|
+
/** Named explicitly. There is no default, and none is invented. */
|
|
31
|
+
readonly environment?: string;
|
|
32
|
+
/** Named explicitly. Where the release will mount it from. */
|
|
33
|
+
readonly out?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ArtifactBuildResult {
|
|
36
|
+
readonly file: string;
|
|
37
|
+
readonly environment: string;
|
|
38
|
+
readonly engineVersion: string;
|
|
39
|
+
readonly keySource: string;
|
|
40
|
+
/** Delivery mappings with a value. */
|
|
41
|
+
readonly values: number;
|
|
42
|
+
/** How many of those travelled as ciphertext. */
|
|
43
|
+
readonly sealed: number;
|
|
44
|
+
/** Declared mappings the environment has no non-local winner for. */
|
|
45
|
+
readonly absent: number;
|
|
46
|
+
/** True when the artifact was written inside the project — a `doctor` finding. */
|
|
47
|
+
readonly insideRepo: boolean;
|
|
48
|
+
}
|
|
49
|
+
declare function runArtifactBuild(options: ArtifactBuildOptions): Promise<ArtifactBuildResult>;
|
|
50
|
+
declare function renderArtifactBuild(result: ArtifactBuildResult, cwd: string): string[];
|
|
3
51
|
|
|
4
52
|
/**
|
|
5
53
|
* A check reports one of four verdicts. `unknown` — a check that ran but could
|
|
@@ -9,7 +57,7 @@ import { ProjectionProvider, Provider, ParameterRef, Scope, AnyProvider, Rotatio
|
|
|
9
57
|
* second kind.
|
|
10
58
|
*/
|
|
11
59
|
type DoctorSeverity = "pass" | "warning" | "failure" | "unknown";
|
|
12
|
-
type DoctorCheck = "schema" | "missing" | "declared" | "weak" | "unused" | "unscoped-fallback" | "plaintext-secret" | "public-secret" | "encryption" | "rotation-overdue" | "rotation-stuck" | "provider-value-drift" | "
|
|
60
|
+
type DoctorCheck = "schema" | "missing" | "declared" | "weak" | "unused" | "unscoped-fallback" | "plaintext-secret" | "public-secret" | "encryption" | "rotation-overdue" | "rotation-stuck" | "provider-value-drift" | "provider" | "projection-unreachable" | "projection-name-drift" | "projection-manual-edit" | "projection-value-drift" | "environment-flag-shadow" | "artifact-in-tree";
|
|
13
61
|
interface DoctorFinding {
|
|
14
62
|
readonly check: DoctorCheck;
|
|
15
63
|
readonly severity: DoctorSeverity;
|
|
@@ -47,6 +95,28 @@ interface DoctorOptions {
|
|
|
47
95
|
declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
|
|
48
96
|
declare function renderDoctor(report: DoctorReport): string[];
|
|
49
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Opening a penv project from a working directory, and the pieces every command
|
|
100
|
+
* needs once it is open: the config, the environment to act on, the provider
|
|
101
|
+
* rooted at the records tree, and the parameter a CLI key names.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
interface Project {
|
|
105
|
+
/** The directory holding `penv.config.ts`. */
|
|
106
|
+
readonly root: string;
|
|
107
|
+
readonly configFile: string;
|
|
108
|
+
readonly config: PenvConfig;
|
|
109
|
+
/** The parameter tree, absolute — `.penv/state/records/`. */
|
|
110
|
+
readonly recordsDir: string;
|
|
111
|
+
/**
|
|
112
|
+
* The project's provider, as the contract — never the concrete
|
|
113
|
+
* implementation. Shared commands speak the async interface and nothing more;
|
|
114
|
+
* the sync twins a command genuinely needs are reached through `localTree`,
|
|
115
|
+
* which is the one place the filesystem-only surface is named.
|
|
116
|
+
*/
|
|
117
|
+
readonly provider: Provider;
|
|
118
|
+
}
|
|
119
|
+
|
|
50
120
|
interface ScopeOptions {
|
|
51
121
|
/** The environment scope. Combined with `local`, the environment-scoped override. */
|
|
52
122
|
readonly environment?: string;
|
|
@@ -59,7 +129,7 @@ interface SetOptions extends ScopeOptions {
|
|
|
59
129
|
}
|
|
60
130
|
interface SetResult {
|
|
61
131
|
readonly parameter: string;
|
|
62
|
-
/** The value file written, relative to
|
|
132
|
+
/** The value file written, relative to the records tree. */
|
|
63
133
|
readonly location: string;
|
|
64
134
|
/** Whether meta's policy sealed it. Reported, so the marker is never a surprise. */
|
|
65
135
|
readonly encrypted: boolean;
|
|
@@ -144,7 +214,7 @@ interface FillResult {
|
|
|
144
214
|
/** The value files written, one per answered prompt. */
|
|
145
215
|
readonly written: ReadonlyArray<{
|
|
146
216
|
readonly parameter: string;
|
|
147
|
-
/** The value file written, relative to
|
|
217
|
+
/** The value file written, relative to the records tree. */
|
|
148
218
|
readonly location: string;
|
|
149
219
|
readonly encrypted: boolean;
|
|
150
220
|
}>;
|
|
@@ -249,8 +319,136 @@ declare function runGet(options: GetOptions): Promise<string>;
|
|
|
249
319
|
*/
|
|
250
320
|
declare function runExplain(options: GetOptions): Promise<GetExplanation>;
|
|
251
321
|
|
|
322
|
+
/**
|
|
323
|
+
* What the codebase already says about itself.
|
|
324
|
+
*
|
|
325
|
+
* `penv init` asks a human to confirm a plan, and a plan the human has to fill
|
|
326
|
+
* in from scratch is an interrogation. So penv reads the two facts it can
|
|
327
|
+
* observe — the framework in `package.json`, and whether a `src/` directory
|
|
328
|
+
* exists — and offers them as a suggestion.
|
|
329
|
+
*
|
|
330
|
+
* The line this module does not cross: a framework is an identity, never a
|
|
331
|
+
* config key. Nothing here is written to `penv.config.ts` as `framework: "next"`
|
|
332
|
+
* — the answers become concrete decisions (`schemaFile`, `publicPrefixes`) that
|
|
333
|
+
* mean the same thing in a year, when the project has been rewritten twice and
|
|
334
|
+
* penv would otherwise still be reinterpreting a name it read once.
|
|
335
|
+
*
|
|
336
|
+
* Everything here is a suggestion. The one thing that is never suggested is an
|
|
337
|
+
* environment: deployment topology is not in `package.json`, and invariant 10
|
|
338
|
+
* forbids inferring it.
|
|
339
|
+
*/
|
|
340
|
+
/** A framework penv recognised, and what it implies about a project's layout. */
|
|
341
|
+
interface Detected {
|
|
342
|
+
/** The framework's own name, as a human writes it — `"Next.js"`. */
|
|
343
|
+
readonly name: string;
|
|
344
|
+
/** Where this framework's projects keep their modules, relative to the root. */
|
|
345
|
+
readonly schemaFile: string;
|
|
346
|
+
/**
|
|
347
|
+
* The conventional path penv stepped aside from, because a module that is not
|
|
348
|
+
* penv's schema already lives there. Set only when it happened, so the plan can
|
|
349
|
+
* say why the schema is not where the convention would have put it.
|
|
350
|
+
*/
|
|
351
|
+
readonly displacedFrom?: string;
|
|
352
|
+
/** The prefixes this framework inlines into its client bundle. */
|
|
353
|
+
readonly publicPrefixes: readonly string[];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The dotenv files a project's framework reads, and what each one says.
|
|
358
|
+
*
|
|
359
|
+
* One module answers this for both ends of adoption: `penv init` offers these
|
|
360
|
+
* files for the cutover, and `penv run` refuses the ones that come back
|
|
361
|
+
* afterwards. Two readings of "which files are active" would eventually let init
|
|
362
|
+
* move a file run does not watch, or run refuse a file init never offered.
|
|
363
|
+
*
|
|
364
|
+
* The four scopes are invariant 4's, which are Next.js's and Vite's:
|
|
365
|
+
* `.env` > `.env.local` > `.env.<environment>` > `.env.<environment>.local`.
|
|
366
|
+
* Anything else with an `.env` prefix is documentation (`.env.example`), a
|
|
367
|
+
* leftover (`.env.backup`), or a filename penv has no reading of — none of them
|
|
368
|
+
* is configuration a framework loads, so none is offered or refused.
|
|
369
|
+
*
|
|
370
|
+
* Reading an environment out of a filename here is not inference: nothing
|
|
371
|
+
* reaches `penv.config.ts` until a human selects the file, and
|
|
372
|
+
* {@link activeDotenvFiles} judges the segment against the declared whitelist
|
|
373
|
+
* (invariant 10) rather than believing it.
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
/** Where a dotenv file sits in the cascade. */
|
|
377
|
+
type DotenvKind = "shared" | "local" | "environment" | "environment-local";
|
|
378
|
+
interface DotenvFile {
|
|
379
|
+
/** The filename exactly as it is on disk — what undo restores. */
|
|
380
|
+
readonly name: string;
|
|
381
|
+
readonly kind: DotenvKind;
|
|
382
|
+
/** The environment the filename names, for the two environment-scoped kinds. */
|
|
383
|
+
readonly environment?: string;
|
|
384
|
+
/** How the file is described in the selection table. */
|
|
385
|
+
readonly label: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The draft schema penv writes when it adopts a dotenv file.
|
|
390
|
+
*
|
|
391
|
+
* It is a draft and says so in the file it lands in: single-sample inference
|
|
392
|
+
* cannot know that a boolean seen as `true` must also accept `1`, or that a
|
|
393
|
+
* string is really a URL. penv scaffolds `penv.schema.ts` once and never
|
|
394
|
+
* regenerates it (invariant 2), so every correction the reader makes is safe.
|
|
395
|
+
*
|
|
396
|
+
* Requiredness across several files is the one judgement here, and it is
|
|
397
|
+
* deliberately coarse: a field observed in every adopted environment starts
|
|
398
|
+
* required, and a field missing from any of them starts optional. That is not
|
|
399
|
+
* per-environment requiredness — there is one schema, never one per environment
|
|
400
|
+
* (invariant 1) — it is the weakest shape that every adopted environment
|
|
401
|
+
* satisfies, which is what makes the first `penv run` after a cutover pass with
|
|
402
|
+
* no edits.
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
/** One schema field, rendered into `penv.schema.ts` as `<key>: <type>,`. */
|
|
406
|
+
interface SchemaField {
|
|
407
|
+
readonly key: string;
|
|
408
|
+
/** The Zod expression, e.g. `z.url()` — `.optional()` already applied when it belongs. */
|
|
409
|
+
readonly type: string;
|
|
410
|
+
}
|
|
411
|
+
/** A drafted field, plus the verdict a report prints. */
|
|
412
|
+
interface DraftField extends SchemaField {
|
|
413
|
+
readonly required: boolean;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The one runtime dependency an adopted project takes, and how it gets there.
|
|
418
|
+
*
|
|
419
|
+
* PRD §3: an adopted project depends on exactly `@penvhq/penv` at the engine's
|
|
420
|
+
* own version — the typed `@env` surface, not a CLI distribution. `penv init`
|
|
421
|
+
* installs it with the package manager the project already uses, and only after
|
|
422
|
+
* showing the exact `package.json` and lockfile change: an install is the one
|
|
423
|
+
* step of adoption that reaches outside the repository, so it is the one step
|
|
424
|
+
* that is shown before it happens rather than reported after.
|
|
425
|
+
*
|
|
426
|
+
* The install itself is a seam. It shells out to a package manager, which the
|
|
427
|
+
* tests must never do — and a fake here is not a weaker test, because what init
|
|
428
|
+
* has to get right is the plan, the consent, and the refusal when the install
|
|
429
|
+
* does not happen.
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
433
|
+
interface InstallPlan {
|
|
434
|
+
readonly root: string;
|
|
435
|
+
readonly manager: PackageManager;
|
|
436
|
+
readonly package: string;
|
|
437
|
+
readonly version: string;
|
|
438
|
+
/** The command, argv-shaped — what runs, and what a refusal tells the user to run. */
|
|
439
|
+
readonly command: readonly string[];
|
|
440
|
+
/** The lockfile the manager will rewrite, when the project has one. */
|
|
441
|
+
readonly lockfile?: string;
|
|
442
|
+
/** What `package.json` already says about the package, when it says anything. */
|
|
443
|
+
readonly declared?: string;
|
|
444
|
+
/** True when `package.json` already pins this exact version — nothing to install. */
|
|
445
|
+
readonly satisfied: boolean;
|
|
446
|
+
}
|
|
447
|
+
/** Runs an install plan, or throws. Replaced in tests; never spawns there. */
|
|
448
|
+
type InstallRuntime = (plan: InstallPlan) => Promise<void>;
|
|
449
|
+
|
|
252
450
|
/** What init touched, so a caller can report it and a test can assert it. */
|
|
253
|
-
type InitTarget = "penv-dir" | "schema" | "env" | "config" | "
|
|
451
|
+
type InitTarget = "penv-dir" | "schema" | "env" | "config" | "tsconfig" | "gitignore" | "seam";
|
|
254
452
|
/**
|
|
255
453
|
* `conflicted` is the one that is not a success. penv wanted to write something,
|
|
256
454
|
* found the user's file already saying something else about the same thing, and
|
|
@@ -279,6 +477,12 @@ interface InitStep {
|
|
|
279
477
|
interface InitDecisions {
|
|
280
478
|
/** The whitelist. Empty unless a human named them — penv never infers one. */
|
|
281
479
|
readonly environments: readonly string[];
|
|
480
|
+
/**
|
|
481
|
+
* The environment every command falls back to when `--env` is absent (seal 3).
|
|
482
|
+
* Written only when the cutover adopted one — a declared decision, so the
|
|
483
|
+
* whitelist rule is untouched, and CI keeps naming `--env` anyway.
|
|
484
|
+
*/
|
|
485
|
+
readonly defaultEnvironment?: string;
|
|
282
486
|
/** The schema module, relative to the project root, POSIX. */
|
|
283
487
|
readonly schemaFile: string;
|
|
284
488
|
/** The prefixes the framework inlines into its client bundle. */
|
|
@@ -313,6 +517,15 @@ interface InitOptions {
|
|
|
313
517
|
/** The detected framework name, passed by the command so the seam step need not re-detect it. */
|
|
314
518
|
readonly framework?: string;
|
|
315
519
|
}
|
|
520
|
+
interface InitPlan {
|
|
521
|
+
readonly detected: Detected | undefined;
|
|
522
|
+
/** What init writes unless a human edits it. */
|
|
523
|
+
readonly decisions: InitDecisions;
|
|
524
|
+
/** Environments the `.env*` files on disk are evidence for. Offered, never taken. */
|
|
525
|
+
readonly suggestedEnvironments: readonly string[];
|
|
526
|
+
/** Why each decision is what it is. Printed — a fallback penv takes silently is a guess. */
|
|
527
|
+
readonly notes: readonly string[];
|
|
528
|
+
}
|
|
316
529
|
interface AliasEdit {
|
|
317
530
|
readonly source: string;
|
|
318
531
|
readonly changed: boolean;
|
|
@@ -338,6 +551,85 @@ interface AliasEdit {
|
|
|
338
551
|
* `@env`, and this line is the only thing that has to know where that is.
|
|
339
552
|
*/
|
|
340
553
|
declare function insertEnvAlias(source: string, target?: string, name?: string): AliasEdit;
|
|
554
|
+
/** One adopted file: what it holds, and the scope its values are written at. */
|
|
555
|
+
interface Adopted {
|
|
556
|
+
readonly file: DotenvFile;
|
|
557
|
+
readonly entries: readonly DotenvEntry[];
|
|
558
|
+
readonly refs: readonly ParameterRef[];
|
|
559
|
+
readonly scope: Scope;
|
|
560
|
+
}
|
|
561
|
+
interface AdoptionPlan {
|
|
562
|
+
readonly root: string;
|
|
563
|
+
/** Every dotenv file penv found, in the order the list shows them. */
|
|
564
|
+
readonly found: readonly DotenvFile[];
|
|
565
|
+
/** Checked when the list is first shown: the development cascade, where it exists. */
|
|
566
|
+
readonly preselected: readonly string[];
|
|
567
|
+
}
|
|
568
|
+
/** What there is to adopt, and what penv proposes taking. */
|
|
569
|
+
declare function planAdoption(root: string): AdoptionPlan;
|
|
570
|
+
interface CutoverPlan {
|
|
571
|
+
readonly root: string;
|
|
572
|
+
readonly selected: readonly DotenvFile[];
|
|
573
|
+
readonly adopted: readonly Adopted[];
|
|
574
|
+
/** The whitelist after this cutover — what the config declares, or is about to. */
|
|
575
|
+
readonly environments: readonly string[];
|
|
576
|
+
/**
|
|
577
|
+
* The environments this cutover is *about*: the ones its files name. Narrower
|
|
578
|
+
* than the whitelist on a project that already declared more, and the ones the
|
|
579
|
+
* draft is judged against and the import is validated for — an environment
|
|
580
|
+
* this cutover did not touch must not fail it for a state it was already in.
|
|
581
|
+
*/
|
|
582
|
+
readonly adopting: readonly string[];
|
|
583
|
+
readonly decisions: InitDecisions;
|
|
584
|
+
readonly fields: readonly DraftField[];
|
|
585
|
+
readonly variables: number;
|
|
586
|
+
/** Values the parser read but that look like a mistake. Shown, never fixed. */
|
|
587
|
+
readonly diagnostics: readonly DotenvDiagnostic[];
|
|
588
|
+
readonly install: InstallPlan;
|
|
589
|
+
readonly framework: string | undefined;
|
|
590
|
+
/** True when `penv.config.ts` already existed, so init keeps every decision it records. */
|
|
591
|
+
readonly configured: boolean;
|
|
592
|
+
}
|
|
593
|
+
interface CutoverInput {
|
|
594
|
+
readonly root: string;
|
|
595
|
+
/** What init would scaffold anyway: detection, the schema's home, the alias. */
|
|
596
|
+
readonly base: InitPlan;
|
|
597
|
+
readonly selected: readonly DotenvFile[];
|
|
598
|
+
/** The environment named when the selection declares none — `.env` alone declares nothing. */
|
|
599
|
+
readonly environment?: string;
|
|
600
|
+
/** The `@penvhq/penv` version to pin. Defaults to this engine's own. */
|
|
601
|
+
readonly version?: string;
|
|
602
|
+
readonly inject?: boolean;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Everything a cutover needs, checked before anything is written.
|
|
606
|
+
*
|
|
607
|
+
* The order is the order the failures matter in: an unresolved bundle first
|
|
608
|
+
* (there is nothing to plan on top of it), then what the selection declares,
|
|
609
|
+
* then whether the selection is complete, then every variable name, and only
|
|
610
|
+
* then the schema and the install. Each one throws, so the caller has a plan or
|
|
611
|
+
* a refusal and never a half-answer.
|
|
612
|
+
*/
|
|
613
|
+
declare function planCutover(input: CutoverInput): CutoverPlan;
|
|
614
|
+
interface CutoverResult {
|
|
615
|
+
readonly plan: CutoverPlan;
|
|
616
|
+
readonly steps: readonly InitStep[];
|
|
617
|
+
/** The dotenv files moved into the bundle, by name. */
|
|
618
|
+
readonly moved: readonly string[];
|
|
619
|
+
/** The environments whose imported values were validated before the move. */
|
|
620
|
+
readonly validated: readonly string[];
|
|
621
|
+
}
|
|
622
|
+
interface CutoverOptions {
|
|
623
|
+
/** Injected in tests: how the runtime dependency is installed. Never spawns there. */
|
|
624
|
+
readonly install?: InstallRuntime;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Installs, scaffolds, imports, validates — and only then moves the dotenv
|
|
628
|
+
* files aside. The order is the guarantee: every step before the move leaves a
|
|
629
|
+
* project whose `.env` files are exactly where they were, so a refusal at any
|
|
630
|
+
* of them costs a re-run and nothing else.
|
|
631
|
+
*/
|
|
632
|
+
declare function applyCutover(plan: CutoverPlan, options?: CutoverOptions): Promise<CutoverResult>;
|
|
341
633
|
declare function runInit(options: InitOptions): InitResult;
|
|
342
634
|
|
|
343
635
|
/**
|
|
@@ -433,7 +725,26 @@ interface ValidateOptions {
|
|
|
433
725
|
/** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
|
|
434
726
|
readonly envFlags?: readonly string[];
|
|
435
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* One environment, checked — and everything the check produced on the way.
|
|
730
|
+
*
|
|
731
|
+
* The verdict is `result` and it is the only verdict penv has. The rest is what
|
|
732
|
+
* `penv run` needs to build a child environment, handed back rather than
|
|
733
|
+
* recomputed: a second walk of the same tree could disagree with the one the
|
|
734
|
+
* verdict was reached on, and then `run` would start a process `validate`
|
|
735
|
+
* refuses.
|
|
736
|
+
*/
|
|
737
|
+
interface EnvironmentCheck {
|
|
738
|
+
readonly result: ValidateResult;
|
|
739
|
+
/** Absent when the schema module could not be evaluated — `result.issues` says why. */
|
|
740
|
+
readonly schema?: z.ZodType;
|
|
741
|
+
/** Every parameter the tree holds, resolved for this environment. */
|
|
742
|
+
readonly resolutions: readonly Resolution[];
|
|
743
|
+
/** The schema-validated object. Present only when the verdict passed. */
|
|
744
|
+
readonly validated?: unknown;
|
|
745
|
+
}
|
|
436
746
|
declare function runValidate(options: ValidateOptions): Promise<ValidateResult>;
|
|
747
|
+
declare function checkEnvironment(project: Project, environment: string): Promise<EnvironmentCheck>;
|
|
437
748
|
|
|
438
749
|
interface ImportOptions {
|
|
439
750
|
readonly cwd: string;
|
|
@@ -518,7 +829,7 @@ interface ListEntry {
|
|
|
518
829
|
readonly variable: string;
|
|
519
830
|
/** `<env>.local`, `local`, an environment name, `default`, or `absent`. */
|
|
520
831
|
readonly scope: string;
|
|
521
|
-
/** The winning value file relative to
|
|
832
|
+
/** The winning value file relative to the records tree, or `undefined` when nothing wins. */
|
|
522
833
|
readonly location: string | undefined;
|
|
523
834
|
readonly encrypted: boolean;
|
|
524
835
|
readonly viaUnscopedFallback: boolean;
|
|
@@ -529,6 +840,66 @@ interface ListResult {
|
|
|
529
840
|
}
|
|
530
841
|
declare function runList(options: ListOptions): Promise<ListResult>;
|
|
531
842
|
|
|
843
|
+
/**
|
|
844
|
+
* `penv migrate` — move a project's records under `.penv/state/records/`.
|
|
845
|
+
*
|
|
846
|
+
* penv reads one layout, so this is the one command that knows two. It is a
|
|
847
|
+
* relocation and nothing else: records move byte for byte, keeping their names,
|
|
848
|
+
* so the grammar, the cascade, the meta and the AAD that binds a ciphertext to
|
|
849
|
+
* its address all mean afterwards exactly what they meant before.
|
|
850
|
+
* `penv.schema.ts`, `penv.config.ts` and `.penv/env.ts` are the project's, and
|
|
851
|
+
* are never touched.
|
|
852
|
+
*
|
|
853
|
+
* It previews before it moves, because the one thing a migration must not do is
|
|
854
|
+
* surprise the person who ran it — and it refuses a half-migrated tree rather
|
|
855
|
+
* than merging two, since which copy of a parameter is current is a question
|
|
856
|
+
* only the user can answer.
|
|
857
|
+
*/
|
|
858
|
+
/** One thing that moves, project-relative and POSIX. */
|
|
859
|
+
interface MigrateMove {
|
|
860
|
+
readonly from: string;
|
|
861
|
+
readonly to: string;
|
|
862
|
+
}
|
|
863
|
+
interface MigratePlan {
|
|
864
|
+
readonly root: string;
|
|
865
|
+
/** What moves, in the order it is reported. */
|
|
866
|
+
readonly moves: readonly MigrateMove[];
|
|
867
|
+
/** What penv writes that is not there yet — the tree root and the safety boundary. */
|
|
868
|
+
readonly creates: readonly string[];
|
|
869
|
+
/** What penv removes: the ignore file that described the old layout. */
|
|
870
|
+
readonly removes: readonly string[];
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* `previewed` is a plan nobody approved, so nothing was written; `current` is a
|
|
874
|
+
* project that was already on the new layout, which is what a second run says.
|
|
875
|
+
*/
|
|
876
|
+
type MigrateStatus = "migrated" | "current" | "previewed";
|
|
877
|
+
interface MigrateResult extends MigratePlan {
|
|
878
|
+
readonly status: MigrateStatus;
|
|
879
|
+
}
|
|
880
|
+
interface MigrateOptions {
|
|
881
|
+
readonly cwd: string;
|
|
882
|
+
/** Approve the plan. Without it `migrate` previews and writes nothing. */
|
|
883
|
+
readonly yes?: boolean;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* What a migration would do, without doing any of it.
|
|
887
|
+
*
|
|
888
|
+
* The move list is `oldLayoutEntries` — the same list every command's refusal is
|
|
889
|
+
* keyed off — so the preview can never describe a different migration from the
|
|
890
|
+
* one that runs.
|
|
891
|
+
*/
|
|
892
|
+
declare function planMigrate(cwd: string): MigratePlan;
|
|
893
|
+
/**
|
|
894
|
+
* Performs a plan. Separate from {@link planMigrate} so what runs is the plan the
|
|
895
|
+
* user approved, not a second reading of the disk between the question and the
|
|
896
|
+
* answer.
|
|
897
|
+
*/
|
|
898
|
+
declare function applyMigrate(plan: MigratePlan): MigrateResult;
|
|
899
|
+
/** Plans, and applies only when the move was approved. */
|
|
900
|
+
declare function runMigrate(options: MigrateOptions): MigrateResult;
|
|
901
|
+
declare function renderMigrate(result: MigrateResult): string[];
|
|
902
|
+
|
|
532
903
|
/**
|
|
533
904
|
* `penv mv <from> <to>` — rename a parameter, every scope at once.
|
|
534
905
|
*
|
|
@@ -664,7 +1035,7 @@ interface RemoveOptions extends ScopeOptions {
|
|
|
664
1035
|
}
|
|
665
1036
|
interface RemoveResult {
|
|
666
1037
|
readonly parameter: string;
|
|
667
|
-
/** The value files that existed and are now gone, relative to
|
|
1038
|
+
/** The value files that existed and are now gone, relative to the records tree. */
|
|
668
1039
|
readonly removed: readonly string[];
|
|
669
1040
|
/** Both files penv looked at, whether or not they were there. */
|
|
670
1041
|
readonly considered: readonly string[];
|
|
@@ -710,44 +1081,90 @@ declare function runRotate(options: RotateOptions): Promise<RotateResult>;
|
|
|
710
1081
|
declare function renderRotate(result: RotateResult): string[];
|
|
711
1082
|
|
|
712
1083
|
/**
|
|
713
|
-
*
|
|
714
|
-
* `load()` resolve in a bundled or serverless runtime where no `penv.config.ts`
|
|
715
|
-
* or `.penv/` tree is on disk. It embeds the evaluated config and every committed
|
|
716
|
-
* sealed value; the scaffolded `env.ts` imports it and passes it to `load`.
|
|
1084
|
+
* Starting someone else's command, opaquely.
|
|
717
1085
|
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
1086
|
+
* `penv run -- <command>` starts exactly what follows `--`: the argument
|
|
1087
|
+
* boundaries the shell already worked out are handed to the operating system
|
|
1088
|
+
* untouched, stdio is the parent's, and the child's exit code and terminating
|
|
1089
|
+
* signal come back out. penv never parses the command, never rebuilds a command
|
|
1090
|
+
* line from it, never wraps it in a shell — a shell would re-split what the user
|
|
1091
|
+
* already split, and `penv run -- node -e "console.log(1 > 2)"` would redirect to
|
|
1092
|
+
* a file called `2`.
|
|
723
1093
|
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
1094
|
+
* Windows is the one place where "hand it to the operating system" needs help.
|
|
1095
|
+
* `pnpm`, `next` and every other node-installed tool are `.cmd` shims there, and
|
|
1096
|
+
* Node refuses to execute one without a shell. So a `.cmd`/`.bat` target — and
|
|
1097
|
+
* only that — is started through `cmd.exe /d /s /c` with
|
|
1098
|
+
* `windowsVerbatimArguments`, building the one command line cmd will accept and
|
|
1099
|
+
* escaping every argument so that cmd hands the child the same bytes penv was
|
|
1100
|
+
* given. Everything else spawns directly, on every platform.
|
|
727
1101
|
*/
|
|
728
1102
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1103
|
+
/** How a child ended. Exactly one of these is meaningful, and both are forwarded. */
|
|
1104
|
+
interface ChildResult {
|
|
1105
|
+
/** The child's own exit code, or 1 when a signal ended it. */
|
|
1106
|
+
readonly exitCode: number;
|
|
1107
|
+
/** The signal that ended the child, when one did. */
|
|
1108
|
+
readonly signal: NodeJS.Signals | null;
|
|
732
1109
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
readonly
|
|
736
|
-
readonly
|
|
737
|
-
|
|
738
|
-
readonly importLine: string;
|
|
739
|
-
/** How to add `snapshot` to the load options — printed on `manual`. */
|
|
740
|
-
readonly loadHint: string;
|
|
1110
|
+
interface ChildInvocation {
|
|
1111
|
+
/** The command exactly as it followed `--`: the executable, then its arguments. */
|
|
1112
|
+
readonly command: readonly string[];
|
|
1113
|
+
readonly env: Record<string, string>;
|
|
1114
|
+
readonly cwd: string;
|
|
741
1115
|
}
|
|
742
|
-
|
|
743
|
-
interface
|
|
744
|
-
|
|
745
|
-
readonly
|
|
1116
|
+
/** A started child: how it ends, and the one thing a wrapper may do to it. */
|
|
1117
|
+
interface ChildHandle {
|
|
1118
|
+
/** Resolves when the child has ended, however it ended. */
|
|
1119
|
+
readonly ended: Promise<ChildResult>;
|
|
1120
|
+
/** Asks the child to stop — what `--watch` does before it starts the next one. */
|
|
1121
|
+
kill(signal?: NodeJS.Signals): void;
|
|
746
1122
|
}
|
|
747
|
-
|
|
1123
|
+
/** The seam `run` starts a child through — replaced in tests that assert what it was given. */
|
|
1124
|
+
type StartChild = (invocation: ChildInvocation) => ChildHandle;
|
|
1125
|
+
|
|
1126
|
+
/** Where a run reads its values from. `snapshot` is the sealed artifact. */
|
|
1127
|
+
type RunSource = "project" | "snapshot";
|
|
1128
|
+
interface RunOptions {
|
|
748
1129
|
readonly cwd: string;
|
|
749
|
-
|
|
750
|
-
declare
|
|
1130
|
+
readonly environment?: string;
|
|
1131
|
+
/** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
|
|
1132
|
+
readonly envFlags?: readonly string[];
|
|
1133
|
+
/** Defaults to `project`. */
|
|
1134
|
+
readonly source?: string;
|
|
1135
|
+
/** The one mode allowed to synchronise. Off by default. */
|
|
1136
|
+
readonly watch?: boolean;
|
|
1137
|
+
/** The command exactly as it followed `--`. */
|
|
1138
|
+
readonly command: readonly string[];
|
|
1139
|
+
/** The environment penv itself was started with. Defaults to `process.env`. */
|
|
1140
|
+
readonly host?: Readonly<Record<string, string | undefined>>;
|
|
1141
|
+
/** Injected in tests: how a child is started. */
|
|
1142
|
+
readonly start?: StartChild;
|
|
1143
|
+
/** Injected in tests: the sync `--watch` performs. */
|
|
1144
|
+
readonly pull?: (options: PullOptions) => Promise<PullResult>;
|
|
1145
|
+
/** Injected in tests: what tells `--watch` something changed. */
|
|
1146
|
+
readonly changes?: (onChange: () => void) => {
|
|
1147
|
+
close(): void;
|
|
1148
|
+
};
|
|
1149
|
+
/** How long a replaced child has to leave before `--watch` insists. Defaults to 5s. */
|
|
1150
|
+
readonly stopGraceMs?: number;
|
|
1151
|
+
}
|
|
1152
|
+
interface RunResult {
|
|
1153
|
+
readonly environment: string;
|
|
1154
|
+
readonly source: RunSource;
|
|
1155
|
+
readonly command: readonly string[];
|
|
1156
|
+
/** Declared variables written into the child. */
|
|
1157
|
+
readonly written: number;
|
|
1158
|
+
/** Declared-but-valueless variables deleted from the child. */
|
|
1159
|
+
readonly deleted: number;
|
|
1160
|
+
/** penv's own variables removed before the child saw them. */
|
|
1161
|
+
readonly stripped: readonly string[];
|
|
1162
|
+
readonly exitCode: number;
|
|
1163
|
+
readonly signal: NodeJS.Signals | null;
|
|
1164
|
+
/** How many times `--watch` replaced the child. */
|
|
1165
|
+
readonly restarts: number;
|
|
1166
|
+
}
|
|
1167
|
+
declare function runRun(options: RunOptions): Promise<RunResult>;
|
|
751
1168
|
|
|
752
1169
|
interface WatchOptions {
|
|
753
1170
|
readonly cwd: string;
|
|
@@ -786,6 +1203,54 @@ declare function runWatch(options: WatchOptions): WatchHandle;
|
|
|
786
1203
|
*/
|
|
787
1204
|
declare function renderWatch(result: ValidateResult): string[];
|
|
788
1205
|
|
|
1206
|
+
interface Cutover {
|
|
1207
|
+
readonly format: number;
|
|
1208
|
+
/** When the files were moved, ISO-8601 UTC. */
|
|
1209
|
+
readonly movedAt: string;
|
|
1210
|
+
/** The filenames as they were at the project root — what undo restores, exactly. */
|
|
1211
|
+
readonly files: readonly string[];
|
|
1212
|
+
/** The environments the cutover declared, so a report can name them without the config. */
|
|
1213
|
+
readonly environments: readonly string[];
|
|
1214
|
+
}
|
|
1215
|
+
interface UndoResult {
|
|
1216
|
+
readonly root: string;
|
|
1217
|
+
/** The files put back, in the order they were moved. */
|
|
1218
|
+
readonly restored: readonly string[];
|
|
1219
|
+
/** Recorded names already at the project root — what an interrupted undo had reached. */
|
|
1220
|
+
readonly alreadyBack: readonly string[];
|
|
1221
|
+
/** Recorded names in neither the bundle nor the project root. Nothing penv can restore. */
|
|
1222
|
+
readonly missing: readonly string[];
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Puts every bundled file back under its exact original name, then drops the
|
|
1226
|
+
* bundle and the state that named it.
|
|
1227
|
+
*
|
|
1228
|
+
* Undo is resumable, because the thing it recovers from is an interruption. A
|
|
1229
|
+
* name already at the project root and no longer in the bundle is a file an
|
|
1230
|
+
* earlier run put back, not a collision — only a name that is in both places at
|
|
1231
|
+
* once is, and that is the one case worth refusing over, since restoring would
|
|
1232
|
+
* write over whatever came back. A name in neither place is reported rather than
|
|
1233
|
+
* refused: the old refusal's remedy was `penv cleanup`, which would have deleted
|
|
1234
|
+
* every file that was still recoverable.
|
|
1235
|
+
*/
|
|
1236
|
+
declare function runUndo(options: {
|
|
1237
|
+
readonly cwd: string;
|
|
1238
|
+
}): UndoResult;
|
|
1239
|
+
interface CleanupResult {
|
|
1240
|
+
readonly root: string;
|
|
1241
|
+
/** The files the bundle held. Empty when there was nothing to clean up. */
|
|
1242
|
+
readonly removed: readonly string[];
|
|
1243
|
+
readonly cleaned: boolean;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Drops the rollback bundle and the cutover state, and nothing else. The records
|
|
1247
|
+
* tree, the schema, the config and the loader are the project's — cleanup is the
|
|
1248
|
+
* end of the migration, not the end of the adoption.
|
|
1249
|
+
*/
|
|
1250
|
+
declare function runCleanup(options: {
|
|
1251
|
+
readonly cwd: string;
|
|
1252
|
+
}): CleanupResult;
|
|
1253
|
+
|
|
789
1254
|
/**
|
|
790
1255
|
* penv's command line.
|
|
791
1256
|
*
|
|
@@ -797,4 +1262,4 @@ declare function renderWatch(result: ValidateResult): string[];
|
|
|
797
1262
|
declare const main: citty.CommandDef<citty.ArgsDef>;
|
|
798
1263
|
declare function runMain(): Promise<void>;
|
|
799
1264
|
|
|
800
|
-
export { type DoctorCheck, type DoctorFinding, type DoctorReport, type DoctorSeverity, type FillOptions, type FillPrompt, type FillResult, type GenerateResult, type GetExplanation, type ImportReport, type InitResult, type InitStep, LAST_PUSHED_KEY, type ListResult, type MoveResult, type PullOptions, type PullResult, type PushOptions, type PushResult, type RemoveResult, type ResealResult, type RotateOptions, type RotatePhase, type RotateResult, type SetResult, type
|
|
1265
|
+
export { type AdoptionPlan, type ArtifactBuildOptions, type ArtifactBuildResult, type CleanupResult, type Cutover, type CutoverPlan, type CutoverResult, type DoctorCheck, type DoctorFinding, type DoctorReport, type DoctorSeverity, type EnvironmentCheck, type FillOptions, type FillPrompt, type FillResult, type GenerateResult, type GetExplanation, type ImportReport, type InitResult, type InitStep, LAST_PUSHED_KEY, type ListResult, type MigrateMove, type MigratePlan, type MigrateResult, type MigrateStatus, type MoveResult, type PullOptions, type PullResult, type PushOptions, type PushResult, type RemoveResult, type ResealResult, type RotateOptions, type RotatePhase, type RotateResult, type RunOptions, type RunResult, type RunSource, type SetResult, type UndoResult, type ValidateIssue, type ValidateResult, type WatchHandle, type WatchOptions, applyCutover, applyMigrate, checkEnvironment, generateDotenv, importDotenv, insertEnvAlias, main, planAdoption, planCutover, planMigrate, renderArtifactBuild, renderDoctor, renderFill, renderMigrate, renderMove, renderPull, renderPush, renderRotate, renderWatch, runArtifactBuild, runCleanup, runDecrypt, runDoctor, runEncrypt, runExplain, runFill, runGenerate, runGet, runInit, runList, runMain, runMigrate, runMove, runPull, runPush, runRemove, runRotate, runRun, runSet, runUndo, runValidate, runWatch };
|