@penvhq/cli 0.16.1 → 1.0.0-alpha.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/dist/index.d.cts DELETED
@@ -1,1240 +0,0 @@
1
- import * as citty from 'citty';
2
- import { ProjectionProvider, Provider, PenvConfig, DotenvEntry, ParameterRef, Scope, DotenvDiagnostic, Resolution, AnyProvider, RotationMechanism, RotationState } from '@penvhq/core';
3
- import { InstallPlan, InstallRuntime } from './install.cjs';
4
- import { z } from 'zod';
5
-
6
- /**
7
- * `penv artifact build` — the sealed deployment artifact CI hands a release.
8
- *
9
- * It is the third step of the sequence PRD §7 names: pull the target
10
- * environment, validate it, build the artifact, mount it. Each step is a command
11
- * with one job, and this one's is narrow on purpose — it does not re-reach a
12
- * verdict `penv validate` already reaches, because two implementations of "is
13
- * this configuration good" would eventually let a release be built that CI had
14
- * already rejected.
15
- *
16
- * Two properties are the whole design:
17
- *
18
- * **It never decrypts.** A sealed value is copied ciphertext-and-address into
19
- * the artifact exactly as the record holds it, so building needs no key at all —
20
- * CI can produce a production artifact without ever being able to read one. The
21
- * AAD is still the value file's full name, so the ciphertext stays bound to the
22
- * scope it was sealed at (invariant 17).
23
- *
24
- * **It refuses to guess the environment.** `--env` is named explicitly and never
25
- * defaulted, `--out` likewise. An artifact built for "whatever the default was"
26
- * is a release that ships the wrong environment's credentials, and the default
27
- * that made it is one config edit nobody reviewed.
28
- */
29
- interface ArtifactBuildOptions {
30
- readonly cwd: string;
31
- /** Named explicitly. There is no default, and none is invented. */
32
- readonly environment?: string;
33
- /** Named explicitly. Where the release will mount it from. */
34
- readonly out?: string;
35
- }
36
- interface ArtifactBuildResult {
37
- readonly file: string;
38
- readonly environment: string;
39
- readonly engineVersion: string;
40
- readonly keySource: string;
41
- /** Delivery mappings with a value. */
42
- readonly values: number;
43
- /** How many of those travelled as ciphertext. */
44
- readonly sealed: number;
45
- /** Declared mappings the environment has no non-local winner for. */
46
- readonly absent: number;
47
- /** True when the artifact was written inside the project — a `doctor` finding. */
48
- readonly insideRepo: boolean;
49
- }
50
- declare function runArtifactBuild(options: ArtifactBuildOptions): Promise<ArtifactBuildResult>;
51
- declare function renderArtifactBuild(result: ArtifactBuildResult, cwd: string): string[];
52
-
53
- /**
54
- * A check reports one of four verdicts. `unknown` — a check that ran but could
55
- * not reach a verdict — is never rendered as a pass: "I looked and found nothing
56
- * wrong" and "I could not look" are opposite situations with opposite remedies,
57
- * and a value-withholding destination makes most of what doctor can say the
58
- * second kind.
59
- */
60
- type DoctorSeverity = "pass" | "warning" | "failure" | "unknown";
61
- type DoctorCheck = "schema" | "missing" | "declared" | "weak" | "unused" | "unscoped-fallback" | "secrecy-undeclared" | "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" | "local-extension" | "artifact-in-tree";
62
- interface DoctorFinding {
63
- readonly check: DoctorCheck;
64
- readonly severity: DoctorSeverity;
65
- readonly label: string;
66
- readonly subject?: string;
67
- readonly detail?: string;
68
- /** A line the reader can act on — the `penv set` to paste, where there is one. */
69
- readonly remedy?: string;
70
- }
71
- interface DoctorReport {
72
- readonly environment: string;
73
- readonly findings: readonly DoctorFinding[];
74
- /** False when any finding is a failure. Warnings and unknowns do not fail the run. */
75
- readonly ok: boolean;
76
- }
77
- interface DoctorOptions {
78
- readonly cwd: string;
79
- readonly environment?: string;
80
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
81
- readonly envFlags?: readonly string[];
82
- /** Injected in tests: the projection-holding destination to check against. Defaults to the one the config declares. */
83
- readonly projection?: ProjectionProvider;
84
- /**
85
- * Injected in tests: the source-of-truth provider to compare the local tree
86
- * against. Defaults to the one the config declares (`sourceProviderFor`).
87
- * Mirrors `projection`, for the same reason — the drift checks stay driveable
88
- * without a live backend.
89
- */
90
- readonly source?: Provider;
91
- /** Injected in tests: the wall-clock reading the rotation clocks are read against. Defaults to now. */
92
- readonly now?: string;
93
- /** Injected in tests: how long a `dual-valid` window may stay open before it reads as stuck. Defaults to 24h. */
94
- readonly stuckThresholdMs?: number;
95
- }
96
- declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
97
- declare function renderDoctor(report: DoctorReport): string[];
98
-
99
- /**
100
- * Opening a penv project from a working directory, and the pieces every command
101
- * needs once it is open: the config, the environment to act on, the provider
102
- * rooted at the records tree, and the parameter a CLI key names.
103
- */
104
-
105
- interface Project {
106
- /** The directory holding `penv.config.ts`. */
107
- readonly root: string;
108
- readonly configFile: string;
109
- readonly config: PenvConfig;
110
- /** The parameter tree, absolute — `.penv/state/records/`. */
111
- readonly recordsDir: string;
112
- /**
113
- * The project's provider, as the contract — never the concrete
114
- * implementation. Shared commands speak the async interface and nothing more;
115
- * the sync twins a command genuinely needs are reached through `localTree`,
116
- * which is the one place the filesystem-only surface is named.
117
- */
118
- readonly provider: Provider;
119
- }
120
-
121
- interface ScopeOptions {
122
- /** The environment scope. Combined with `local`, the environment-scoped override. */
123
- readonly environment?: string;
124
- readonly local?: boolean;
125
- }
126
- interface SetOptions extends ScopeOptions {
127
- readonly cwd: string;
128
- readonly key: string;
129
- readonly value: string;
130
- }
131
- interface SetResult {
132
- readonly parameter: string;
133
- /** The value file written, relative to the records tree. */
134
- readonly location: string;
135
- /** Whether meta's policy sealed it. Reported, so the marker is never a surprise. */
136
- readonly encrypted: boolean;
137
- }
138
- /**
139
- * Writes one value file, sealing it when meta says the parameter is a secret.
140
- *
141
- * The scope is chosen from the flags, then the seal-and-twin write is the shared
142
- * {@link sealAwareWrite}, against the local tree — the store `set` always edits.
143
- */
144
- declare function runSet(options: SetOptions): Promise<SetResult>;
145
-
146
- interface ResealOptions extends ScopeOptions {
147
- readonly cwd: string;
148
- readonly key: string;
149
- }
150
- interface ResealResult {
151
- readonly parameter: string;
152
- /** The file that now holds the value. */
153
- readonly location: string;
154
- /** The file that no longer exists, because its twin replaced it. */
155
- readonly removed: string;
156
- }
157
- declare function runEncrypt(options: ResealOptions): Promise<ResealResult>;
158
- declare function runDecrypt(options: ResealOptions): Promise<ResealResult>;
159
-
160
- /**
161
- * `penv fill` — walk the schema's required-but-missing parameters and ask for
162
- * each one, deriving the value file's name so the user never has to.
163
- *
164
- * The schema-first flow writes `.penv/env.ts` before any value exists, and there
165
- * the user hits a translation they should not have to make: `databaseUrl` in the
166
- * schema is `database-url` on disk, and typing the wrong one writes a file the
167
- * schema still cannot see. `fill` reads the same declared drift `validate`
168
- * computes, and for each missing parameter asks for a value and writes it through
169
- * the one writer — `runSet` — deriving the kebab filename from the schema key.
170
- *
171
- * A value is never invented: a blank answer skips the parameter, because the
172
- * silent value reaching runtime is the failure penv exists to delete, and a
173
- * placeholder written here is exactly that value by a friendlier route.
174
- *
175
- * Optional parameters — `.optional()`, `.default()` — are asked too, after the
176
- * required gaps, tagged so the reader knows an answer is an override and Enter
177
- * keeps what the schema declared. Skipping them silently was the old behavior,
178
- * and it hid a real choice: a schema default reaching runtime is legal, but the
179
- * user who never heard the question never chose it.
180
- */
181
- /** One question `fill` puts to the user: which parameter, in which environment. */
182
- interface FillPrompt {
183
- /** The value file's key, kebab and slash-separated — the name the user need never derive. */
184
- readonly parameter: string;
185
- readonly environment: string;
186
- /**
187
- * Whether meta says this is a secret. Carried so a wrapper can mute the echo;
188
- * v1 does not, and the drift carries no meta, so this is `false` today.
189
- */
190
- readonly secret: boolean;
191
- /**
192
- * Whether the schema excuses absence — `.optional()`, `.default()`. An answer
193
- * writes an override; a blank one leaves the schema's own behavior in place,
194
- * which is a kept default rather than a lingering gap.
195
- */
196
- readonly optional: boolean;
197
- /** What the schema falls back to, rendered for display, when it declares one penv can read. */
198
- readonly defaultValue?: string;
199
- readonly description?: string;
200
- }
201
- interface FillOptions {
202
- readonly cwd: string;
203
- readonly environment?: string;
204
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
205
- readonly envFlags?: readonly string[];
206
- /**
207
- * How a value is obtained for one prompt. `undefined` or an empty answer skips
208
- * the parameter — the readline half lives only in the wrapper, so `runFill`
209
- * stays pure and unit-testable.
210
- */
211
- readonly ask: (prompt: FillPrompt) => Promise<string | undefined>;
212
- }
213
- interface FillResult {
214
- readonly environment: string;
215
- /** The value files written, one per answered prompt. */
216
- readonly written: ReadonlyArray<{
217
- readonly parameter: string;
218
- /** The value file written, relative to the records tree. */
219
- readonly location: string;
220
- readonly encrypted: boolean;
221
- }>;
222
- /** The parameters a blank answer left for later — never written as an empty value. */
223
- readonly skipped: readonly string[];
224
- /**
225
- * The optional parameters a blank answer left to the schema. Not `skipped`:
226
- * a skipped parameter is still a gap, and one of these is a decision — the
227
- * schema's default (or declared absence) is the value, on purpose.
228
- */
229
- readonly kept: readonly string[];
230
- /**
231
- * The declared keys no filename reaches (`apiURL`, a reserved token). `fill`
232
- * cannot ask for a value it could never write, so it carries the rename remedy
233
- * out rather than prompting for a file that would error.
234
- */
235
- readonly unreachable: ReadonlyArray<{
236
- readonly subject: string;
237
- readonly remedy: string;
238
- }>;
239
- }
240
- /**
241
- * Asks for every declared-but-missing parameter, and writes the ones answered.
242
- *
243
- * The drift is `validate`'s, not a second reading of the schema: `runValidate`
244
- * already computes exactly the required-but-absent set, so `fill` and `validate`
245
- * can never disagree about what is missing. The writing is `runSet`'s, so a
246
- * filled secret is sealed exactly as a `set` one is — `fill` owns neither the
247
- * resolution nor the write, only the prompting between them.
248
- */
249
- declare function runFill(options: FillOptions): Promise<FillResult>;
250
- declare function renderFill(result: FillResult): string[];
251
-
252
- interface GenerateOptions {
253
- readonly cwd: string;
254
- readonly environment?: string;
255
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
256
- readonly envFlags?: readonly string[];
257
- /** Where to write, absolute or relative to `cwd`. Defaults to `.env` at the project root. */
258
- readonly out?: string;
259
- /** Permits sealed values to be written into the artifact as plaintext. */
260
- readonly allowDecrypt?: boolean;
261
- }
262
- interface GenerateResult {
263
- readonly file: string;
264
- readonly environment: string;
265
- readonly entries: number;
266
- /** How many of them were sealed and are now plaintext in the artifact. */
267
- readonly decrypted: number;
268
- }
269
- /** The `.env` text for one environment — what `penv generate` writes. */
270
- declare function generateDotenv(options: Omit<GenerateOptions, "out">): string;
271
- declare function runGenerate(options: GenerateOptions): GenerateResult;
272
-
273
- /**
274
- * `penv get <key>` — read a parameter, or explain which file wins and why.
275
- *
276
- * Fallback is never silent, and neither is precedence: `--explain` prints every
277
- * candidate in the order the cascade considered them, so a value quietly coming
278
- * from a shared default has nowhere to hide.
279
- */
280
- interface GetOptions {
281
- readonly cwd: string;
282
- readonly key: string;
283
- readonly environment?: string;
284
- }
285
- interface GetExplanation {
286
- readonly parameter: string;
287
- readonly environment: string;
288
- /** `undefined` when no candidate was present. */
289
- readonly location: string | undefined;
290
- /**
291
- * Why the winning file did not open, when it is `.enc` and did not.
292
- *
293
- * A winner that cannot be decrypted is not a skipped candidate — it won, and
294
- * the cascade is over. Reporting it as a skip would say a lower scope should
295
- * have been reached, which is the scope-widening answer the cascade refuses.
296
- */
297
- readonly undecryptable?: string;
298
- readonly candidates: readonly GetCandidate[];
299
- }
300
- interface GetCandidate {
301
- readonly location: string;
302
- readonly present: boolean;
303
- readonly wins: boolean;
304
- /** Why a present candidate did not win, or why it was never considered. */
305
- readonly skipped: string | undefined;
306
- }
307
- /**
308
- * The value, or a named error.
309
- *
310
- * `requireValue` answers first, so a winner that exists but did not decrypt is
311
- * reported as undecryptable rather than as absent. Only a genuine absence — no
312
- * candidate at any scope — reaches the refusal below, which is what keeps `penv
313
- * set` from being offered as the fix for a secret the user still has.
314
- */
315
- declare function runGet(options: GetOptions): Promise<string>;
316
- /**
317
- * Which file wins, and why — never a value, so this must not be stopped by the
318
- * winner being unreadable. Core describes an `.enc` winner rather than refusing
319
- * it, so `--explain` is the same walk every other command does.
320
- */
321
- declare function runExplain(options: GetOptions): Promise<GetExplanation>;
322
-
323
- /**
324
- * What the codebase already says about itself.
325
- *
326
- * `penv init` asks a human to confirm a plan, and a plan the human has to fill
327
- * in from scratch is an interrogation. So penv reads the two facts it can
328
- * observe — the framework in `package.json`, and whether a `src/` directory
329
- * exists — and offers them as a suggestion.
330
- *
331
- * The line this module does not cross: a framework is an identity, never a
332
- * config key. Nothing here is written to `penv.config.ts` as `framework: "next"`
333
- * — the answers become concrete decisions (`schemaFile`, `publicPrefixes`) that
334
- * mean the same thing in a year, when the project has been rewritten twice and
335
- * penv would otherwise still be reinterpreting a name it read once.
336
- *
337
- * Everything here is a suggestion. The one thing that is never suggested is an
338
- * environment: deployment topology is not in `package.json`, and invariant 10
339
- * forbids inferring it.
340
- */
341
- /** A framework penv recognised, and what it implies about a project's layout. */
342
- interface Detected {
343
- /** The framework's own name, as a human writes it — `"Next.js"`. */
344
- readonly name: string;
345
- /** Where this framework's projects keep their modules, relative to the root. */
346
- readonly schemaFile: string;
347
- /**
348
- * The conventional path penv stepped aside from, because a module that is not
349
- * penv's schema already lives there. Set only when it happened, so the plan can
350
- * say why the schema is not where the convention would have put it.
351
- */
352
- readonly displacedFrom?: string;
353
- /** The prefixes this framework inlines into its client bundle. */
354
- readonly publicPrefixes: readonly string[];
355
- }
356
-
357
- /**
358
- * The dotenv files a project's framework reads, and what each one says.
359
- *
360
- * One module answers this for both ends of adoption: `penv init` offers these
361
- * files for the cutover, and `penv run` refuses the ones that come back
362
- * afterwards. Two readings of "which files are active" would eventually let init
363
- * move a file run does not watch, or run refuse a file init never offered.
364
- *
365
- * The four scopes are invariant 4's, which are Next.js's and Vite's:
366
- * `.env` > `.env.local` > `.env.<environment>` > `.env.<environment>.local`.
367
- * Anything else with an `.env` prefix is documentation (`.env.example`), a
368
- * leftover (`.env.backup`), or a filename penv has no reading of — none of them
369
- * is configuration a framework loads, so none is offered or refused.
370
- *
371
- * Reading an environment out of a filename here is not inference: nothing
372
- * reaches `penv.config.ts` until a human selects the file, and
373
- * {@link activeDotenvFiles} judges the segment against the declared whitelist
374
- * (invariant 10) rather than believing it.
375
- */
376
-
377
- /** Where a dotenv file sits in the cascade. */
378
- type DotenvKind = "shared" | "local" | "environment" | "environment-local";
379
- interface DotenvFile {
380
- /** The filename exactly as it is on disk — what undo restores. */
381
- readonly name: string;
382
- readonly kind: DotenvKind;
383
- /** The environment the filename names, for the two environment-scoped kinds. */
384
- readonly environment?: string;
385
- /** How the file is described in the selection table. */
386
- readonly label: string;
387
- }
388
-
389
- /**
390
- * The draft schema penv writes when it adopts a dotenv file.
391
- *
392
- * It is a draft and says so in the file it lands in: single-sample inference
393
- * cannot know that a boolean seen as `true` must also accept `1`, or that a
394
- * string is really a URL. penv scaffolds `penv.schema.ts` once and never
395
- * regenerates it (invariant 2), so every correction the reader makes is safe.
396
- *
397
- * Requiredness across several files is the one judgement here, and it is
398
- * deliberately coarse: a field observed in every adopted environment starts
399
- * required, and a field missing from any of them starts optional. That is not
400
- * per-environment requiredness — there is one schema, never one per environment
401
- * (invariant 1) — it is the weakest shape that every adopted environment
402
- * satisfies, which is what makes the first `penv run` after a cutover pass with
403
- * no edits.
404
- */
405
-
406
- /** One schema field, rendered into `penv.schema.ts` as `<key>: <type>,`. */
407
- interface SchemaField {
408
- readonly key: string;
409
- /** The Zod expression, e.g. `z.url()` — `.optional()` already applied when it belongs. */
410
- readonly type: string;
411
- }
412
- /** A drafted field, plus the verdict a report prints. */
413
- interface DraftField extends SchemaField {
414
- readonly required: boolean;
415
- }
416
-
417
- /** What init touched, so a caller can report it and a test can assert it. */
418
- type InitTarget = "penv-dir" | "schema" | "env" | "config" | "tsconfig" | "gitignore" | "seam";
419
- /**
420
- * `conflicted` is the one that is not a success. penv wanted to write something,
421
- * found the user's file already saying something else about the same thing, and
422
- * left it alone — so the step is reported with a warning rather than a ✓, and the
423
- * text says what will not work until the user decides.
424
- *
425
- * `info` is a step penv did not perform automatically — a manual instruction (the
426
- * injection seam for a framework penv cannot scaffold), reported so the user
427
- * knows the one thing left to do.
428
- */
429
- type InitAction = "created" | "kept" | "updated" | "conflicted" | "info";
430
- interface InitStep {
431
- readonly target: InitTarget;
432
- readonly action: InitAction;
433
- /** The reported line, in the docs' voice. */
434
- readonly text: string;
435
- readonly note?: string;
436
- }
437
- /**
438
- * The answers init writes down. Every one of these is a decision a human either
439
- * made or consented to — never an identity penv recorded to reinterpret later.
440
- * There is deliberately no `framework` here: `schemaFile` and `publicPrefixes`
441
- * still mean exactly what they say after the project is rewritten in something
442
- * else, and `framework: "next"` would not.
443
- */
444
- interface InitDecisions {
445
- /** The whitelist. Empty unless a human named them — penv never infers one. */
446
- readonly environments: readonly string[];
447
- /**
448
- * The environment every command falls back to when `--env` is absent (seal 3).
449
- * Written only when the cutover adopted one — a declared decision, so the
450
- * whitelist rule is untouched, and CI keeps naming `--env` anyway.
451
- */
452
- readonly defaultEnvironment?: string;
453
- /** The schema module, relative to the project root, POSIX. */
454
- readonly schemaFile: string;
455
- /** The prefixes the framework inlines into its client bundle. */
456
- readonly publicPrefixes: readonly string[];
457
- /**
458
- * How the user's code names the schema module — `@env` or `#env`.
459
- *
460
- * Two forms, resolved by two different things: `@env` is a tsconfig `paths`
461
- * entry that a bundler resolves and plain Node does not, and `#env` is a
462
- * package.json `imports` entry that Node resolves itself. Which one a project
463
- * wants is a fact about the project, so penv reads what it already does and
464
- * offers that.
465
- */
466
- readonly alias: string;
467
- /**
468
- * Whether to inject the validated config into `process.env` for libraries that
469
- * read it directly, so `env.ts` loads with `{ inject: true }` and penv places
470
- * the framework's pre-app seam. Off by default and only ever turned on by an
471
- * explicit yes — a project that reads config only through `@env` gets none.
472
- */
473
- readonly inject: boolean;
474
- }
475
- interface InitResult {
476
- readonly root: string;
477
- readonly decisions: InitDecisions;
478
- readonly steps: readonly InitStep[];
479
- }
480
- interface InitOptions {
481
- readonly cwd: string;
482
- /** What to write. Omitted means the plan's defaults, as `--yes` takes them. */
483
- readonly decisions?: InitDecisions;
484
- /** The detected framework name, passed by the command so the seam step need not re-detect it. */
485
- readonly framework?: string;
486
- }
487
- interface InitPlan {
488
- readonly detected: Detected | undefined;
489
- /** What init writes unless a human edits it. */
490
- readonly decisions: InitDecisions;
491
- /** Environments the `.env*` files on disk are evidence for. Offered, never taken. */
492
- readonly suggestedEnvironments: readonly string[];
493
- /** Why each decision is what it is. Printed — a fallback penv takes silently is a guess. */
494
- readonly notes: readonly string[];
495
- }
496
- interface AliasEdit {
497
- readonly source: string;
498
- readonly changed: boolean;
499
- /**
500
- * What the alias already points at, when that is not penv's schema.
501
- *
502
- * The alias is how the user's code reaches penv, so an alias that resolves
503
- * somewhere else is not a small problem: `import { env } from "@env"` compiles,
504
- * runs, and hands back another module's export. Reporting "kept the alias"
505
- * because the *key* was present is how penv would say that was fine — a silent
506
- * seam, in the scaffolder of the tool whose subject is silent seams.
507
- *
508
- * Left as the user's, never rewritten: penv cannot tell a stale mapping from a
509
- * deliberate one, and the file is theirs.
510
- */
511
- readonly conflict?: string;
512
- }
513
- /**
514
- * `tsconfig.json` with the `@env` alias present, everything else untouched.
515
- * Already-aliased input comes back unchanged rather than gaining a duplicate.
516
- *
517
- * The alias is why the schema can live anywhere: application code imports
518
- * `@env`, and this line is the only thing that has to know where that is.
519
- */
520
- declare function insertEnvAlias(source: string, target?: string, name?: string): AliasEdit;
521
- /** One adopted file: what it holds, and the scope its values are written at. */
522
- interface Adopted {
523
- readonly file: DotenvFile;
524
- readonly entries: readonly DotenvEntry[];
525
- readonly refs: readonly ParameterRef[];
526
- readonly scope: Scope;
527
- }
528
- interface AdoptionPlan {
529
- readonly root: string;
530
- /** Every dotenv file penv found, in the order the list shows them. */
531
- readonly found: readonly DotenvFile[];
532
- /** Checked when the list is first shown: the development cascade, where it exists. */
533
- readonly preselected: readonly string[];
534
- }
535
- /** What there is to adopt, and what penv proposes taking. */
536
- declare function planAdoption(root: string): AdoptionPlan;
537
- interface CutoverPlan {
538
- readonly root: string;
539
- readonly selected: readonly DotenvFile[];
540
- readonly adopted: readonly Adopted[];
541
- /** The whitelist after this cutover — what the config declares, or is about to. */
542
- readonly environments: readonly string[];
543
- /**
544
- * The environments this cutover is *about*: the ones its files name. Narrower
545
- * than the whitelist on a project that already declared more, and the ones the
546
- * draft is judged against and the import is validated for — an environment
547
- * this cutover did not touch must not fail it for a state it was already in.
548
- */
549
- readonly adopting: readonly string[];
550
- readonly decisions: InitDecisions;
551
- readonly fields: readonly DraftField[];
552
- readonly variables: number;
553
- /** Values the parser read but that look like a mistake. Shown, never fixed. */
554
- readonly diagnostics: readonly DotenvDiagnostic[];
555
- readonly install: InstallPlan;
556
- readonly framework: string | undefined;
557
- /** True when `penv.config.ts` already existed, so init keeps every decision it records. */
558
- readonly configured: boolean;
559
- }
560
- interface CutoverInput {
561
- readonly root: string;
562
- /** What init would scaffold anyway: detection, the schema's home, the alias. */
563
- readonly base: InitPlan;
564
- readonly selected: readonly DotenvFile[];
565
- /** The environment named when the selection declares none — `.env` alone declares nothing. */
566
- readonly environment?: string;
567
- /** The `@penvhq/penv` version to pin. Defaults to this engine's own. */
568
- readonly version?: string;
569
- readonly inject?: boolean;
570
- }
571
- /**
572
- * Everything a cutover needs, checked before anything is written.
573
- *
574
- * The order is the order the failures matter in: an unresolved bundle first
575
- * (there is nothing to plan on top of it), then what the selection declares,
576
- * then whether the selection is complete, then every variable name, and only
577
- * then the schema and the install. Each one throws, so the caller has a plan or
578
- * a refusal and never a half-answer.
579
- */
580
- declare function planCutover(input: CutoverInput): CutoverPlan;
581
- interface CutoverResult {
582
- readonly plan: CutoverPlan;
583
- readonly steps: readonly InitStep[];
584
- /** The dotenv files moved into the bundle, by name. */
585
- readonly moved: readonly string[];
586
- /** The environments whose imported values were validated before the move. */
587
- readonly validated: readonly string[];
588
- }
589
- interface CutoverOptions {
590
- /** Injected in tests: how the runtime dependency is installed. Never spawns there. */
591
- readonly install?: InstallRuntime;
592
- }
593
- /**
594
- * Installs, scaffolds, imports, validates — and only then moves the dotenv
595
- * files aside. The order is the guarantee: every step before the move leaves a
596
- * project whose `.env` files are exactly where they were, so a refusal at any
597
- * of them costs a re-run and nothing else.
598
- *
599
- * The scaffold is snapshotted first, and rolled back when anything after it
600
- * refuses. Without that, "a refusal costs a re-run" was only true of the dotenv
601
- * files: a failed run still left a config, a draft schema, an edited tsconfig
602
- * and an imported records tree behind, and the next run kept every one of them
603
- * rather than starting clean. The install is not rolled back — it is the one
604
- * step the developer consented to by itself, and a re-run finds it satisfied.
605
- */
606
- declare function applyCutover(plan: CutoverPlan, options?: CutoverOptions): Promise<CutoverResult>;
607
- declare function runInit(options: InitOptions): InitResult;
608
-
609
- /**
610
- * Reading the user's schema, and the distance between it and the parameter tree.
611
- *
612
- * The schema declares what must exist and the tree holds what does. The gap
613
- * between them is the signal `penv validate` exists to raise; this module makes
614
- * it legible without closing it. Nothing here writes or deletes a value file —
615
- * a declaration has no value, so materialising one could only invent it, and an
616
- * invented value is the silent-value-reaching-runtime failure penv exists to
617
- * delete. `penv set` stays the only writer.
618
- *
619
- * The introspection below lives here, not in `doctor`, because `doctor` and
620
- * `watch` both report drift and two readers of the same schema would be two
621
- * answers to one question.
622
- *
623
- * Every helper answers "I cannot tell" rather than guessing. A report is only
624
- * worth reading if every line in it is true, so a field this module cannot
625
- * understand produces no line at all.
626
- */
627
-
628
- /** A parameter the schema declares that the tree has no value for. */
629
- interface DeclaredDrift {
630
- /** The parameter id, or the dotted schema path when no filename could reach it. */
631
- readonly subject: string;
632
- /** Absent when no filename reaches this key, which is drift `penv set` cannot close. */
633
- readonly ref?: ParameterRef;
634
- /** The line to paste: the `penv set` that closes this, or the rename that must precede it. */
635
- readonly remedy: string;
636
- readonly detail: string;
637
- }
638
- /** A parameter the tree holds a value for that the schema does not declare. */
639
- interface UndeclaredDrift {
640
- readonly ref: ParameterRef;
641
- /** The generated variable, which is the name the application would have read. */
642
- readonly variable: string;
643
- }
644
- /**
645
- * A parameter the schema declares but does not require — `.optional()`,
646
- * `.default()`, and their kin — that the tree has no value for. Not drift in the
647
- * verdict sense: absence here is a state the schema itself blessed, so `doctor`
648
- * and `watch` say nothing about it. It is measured for `fill`, whose reader is
649
- * deciding what to write, and for whom "the schema would take an override here"
650
- * is exactly the kind of fact a silent skip would hide.
651
- */
652
- interface OptionalDrift {
653
- /** The parameter id, or the dotted schema path when no filename could reach it. */
654
- readonly subject: string;
655
- /** Absent when no filename reaches this key — an override `penv set` cannot write. */
656
- readonly ref?: ParameterRef;
657
- /** What the schema falls back to, rendered for display, when it declares one this module can read. */
658
- readonly defaultValue?: string;
659
- /** The rename that must precede any override, for the key no filename reaches. */
660
- readonly remedy: string;
661
- }
662
- /**
663
- * The distance between the schema and the tree, in both directions. Named
664
- * `declared`/`undeclared` for the side that has it, not for a verdict: neither
665
- * direction is by itself an error, and only `validate` decides that. `optional`
666
- * is the deliberately verdict-free third list — see {@link OptionalDrift}.
667
- */
668
- interface DriftReport {
669
- readonly declared: readonly DeclaredDrift[];
670
- readonly undeclared: readonly UndeclaredDrift[];
671
- readonly optional: readonly OptionalDrift[];
672
- }
673
-
674
- type ValidateIssueKind = "config" | "reserved" | "collision" | "schema" | "undecryptable";
675
- interface ValidateIssue {
676
- readonly kind: ValidateIssueKind;
677
- /** What the line is about: a parameter, a variable, a token, or a file. */
678
- readonly subject: string;
679
- readonly message: string;
680
- readonly remedy?: string;
681
- }
682
- interface ValidateResult {
683
- readonly ok: boolean;
684
- readonly environment: string;
685
- readonly parameters: number;
686
- readonly issues: readonly ValidateIssue[];
687
- /**
688
- * The distance between the schema and the tree, carried for the callers
689
- * that report it (`watch`). Never folded into `ok` and never rendered by
690
- * `renderValidate`: drift is a report, and CI's verdict must not move because
691
- * a parameter the schema tolerates is absent. Empty when the schema did not
692
- * load, since there is nothing to measure against.
693
- */
694
- readonly drift: DriftReport;
695
- }
696
- interface ValidateOptions {
697
- readonly cwd: string;
698
- readonly environment?: string;
699
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
700
- readonly envFlags?: readonly string[];
701
- }
702
- /**
703
- * One environment, checked — and everything the check produced on the way.
704
- *
705
- * The verdict is `result` and it is the only verdict penv has. The rest is what
706
- * `penv run` needs to build a child environment, handed back rather than
707
- * recomputed: a second walk of the same tree could disagree with the one the
708
- * verdict was reached on, and then `run` would start a process `validate`
709
- * refuses.
710
- */
711
- interface EnvironmentCheck {
712
- readonly result: ValidateResult;
713
- /** Absent when the schema module could not be evaluated — `result.issues` says why. */
714
- readonly schema?: z.ZodType;
715
- /** Every parameter the tree holds, resolved for this environment. */
716
- readonly resolutions: readonly Resolution[];
717
- /** The schema-validated object. Present only when the verdict passed. */
718
- readonly validated?: unknown;
719
- }
720
- declare function runValidate(options: ValidateOptions): Promise<ValidateResult>;
721
- declare function checkEnvironment(project: Project, environment: string): Promise<EnvironmentCheck>;
722
-
723
- interface ImportOptions {
724
- readonly cwd: string;
725
- /** The dotenv file to adopt, absolute or relative to `cwd`. */
726
- readonly file: string;
727
- /**
728
- * `--env`. It reads as "these are <environment>'s values", so for a file whose
729
- * name carries no environment it names the *scope* as well as the environment
730
- * to run against: `penv import prod-secrets.txt --env production` writes
731
- * `<name>.production`, and `--env production` on `.env.local` writes
732
- * `<name>.production.local`. The filename supplies both when it carries an
733
- * environment, so the flag is needed only for a file that does not — and
734
- * contradicting the filename is an error rather than a silent choice between
735
- * the two.
736
- */
737
- readonly environment?: string;
738
- }
739
- interface ImportReport {
740
- readonly root: string;
741
- readonly file: string;
742
- readonly backup: string;
743
- /** The scope the source named — filename, `--env`, or both — and the scope every value was written at. */
744
- readonly scope: Scope;
745
- /**
746
- * The environment the import ran against, or `undefined` when none is set.
747
- *
748
- * Undefined only ever accompanies the unscoped default: any other scope names
749
- * an environment, so it always has one. It means the values were written and
750
- * the closing validation was skipped, which the output states.
751
- */
752
- readonly environment: string | undefined;
753
- /** The declared environments, so a skipped validation can name one to pass. */
754
- readonly environments: readonly string[];
755
- readonly variables: number;
756
- /**
757
- * Comment blocks that belonged to no variable. Reported rather than discarded
758
- * silently: a file header has no parameter to describe, but that is not a
759
- * reason to pretend it was never there.
760
- */
761
- readonly orphanComments: number;
762
- readonly steps: readonly InitStep[];
763
- }
764
- /**
765
- * Adopts the file: parses it, scaffolds the project, writes one value file per
766
- * variable and each attached comment into that parameter's meta, and backs the
767
- * source up. Validation is the caller's next step rather than part of adoption —
768
- * an inferred schema is a draft, and a draft that needs correcting has still
769
- * imported every value correctly.
770
- *
771
- * Adoption is all or nothing. Every name is checked against the config, and any
772
- * environment the source names resolved, before the tree is scaffolded or a
773
- * value written. The two names that fail here fail *destructively*: a reserved
774
- * name bricks every later command, and a lossy name renames the user's variable
775
- * behind their back. What the source names is resolved here rather than left to
776
- * the closing `validate` because a command that writes a tree and *then*
777
- * discovers it cannot name an environment has already half-adopted the project
778
- * it just refused. A half-imported tree would be the drift penv exists to
779
- * remove, introduced by penv itself.
780
- *
781
- * An environment nothing names is a different case, and not an error: an
782
- * unscoped import writes at the unscoped default, which needs no environment.
783
- * Only the validation that follows needs one, so it is skipped and said to be
784
- * skipped. Requiring one here would fail `penv import .env` on a greenfield
785
- * project — the first command the quickstart gives, where no environment could
786
- * plausibly be set yet — to satisfy a step that is the caller's next one.
787
- */
788
- declare function importDotenv(options: ImportOptions): ImportReport;
789
-
790
- /**
791
- * `penv list` — every parameter, and the scope that wins for one environment.
792
- *
793
- * The winning scope is the point: `production` and `default` are both "it
794
- * resolves", and only one of them means the value was written for production.
795
- */
796
- interface ListOptions {
797
- readonly cwd: string;
798
- readonly environment?: string;
799
- }
800
- interface ListEntry {
801
- readonly parameter: string;
802
- /** The generated `.env` variable, so the two names are legible side by side. */
803
- readonly variable: string;
804
- /** `<env>.local`, `local`, an environment name, `default`, or `absent`. */
805
- readonly scope: string;
806
- /** The winning value file relative to the records tree, or `undefined` when nothing wins. */
807
- readonly location: string | undefined;
808
- readonly encrypted: boolean;
809
- readonly viaUnscopedFallback: boolean;
810
- }
811
- interface ListResult {
812
- readonly environment: string;
813
- readonly parameters: readonly ListEntry[];
814
- }
815
- declare function runList(options: ListOptions): Promise<ListResult>;
816
-
817
- /**
818
- * `penv migrate` — move a project's records under `.penv/state/records/`.
819
- *
820
- * penv reads one layout, so this is the one command that knows two. It is a
821
- * relocation and nothing else: records move byte for byte, keeping their names,
822
- * so the grammar, the cascade, the meta and the AAD that binds a ciphertext to
823
- * its address all mean afterwards exactly what they meant before.
824
- * `penv.schema.ts`, `penv.config.ts` and `.penv/env.ts` are the project's, and
825
- * are never touched.
826
- *
827
- * It previews before it moves, because the one thing a migration must not do is
828
- * surprise the person who ran it — and it refuses a half-migrated tree rather
829
- * than merging two, since which copy of a parameter is current is a question
830
- * only the user can answer.
831
- */
832
- /** One thing that moves, project-relative and POSIX. */
833
- interface MigrateMove {
834
- readonly from: string;
835
- readonly to: string;
836
- }
837
- interface MigratePlan {
838
- readonly root: string;
839
- /** What moves, in the order it is reported. */
840
- readonly moves: readonly MigrateMove[];
841
- /** What penv writes that is not there yet — the tree root and the safety boundary. */
842
- readonly creates: readonly string[];
843
- /** What penv removes: the ignore file that described the old layout. */
844
- readonly removes: readonly string[];
845
- }
846
- /**
847
- * `previewed` is a plan nobody approved, so nothing was written; `current` is a
848
- * project that was already on the new layout, which is what a second run says.
849
- */
850
- type MigrateStatus = "migrated" | "current" | "previewed";
851
- interface MigrateResult extends MigratePlan {
852
- readonly status: MigrateStatus;
853
- }
854
- interface MigrateOptions {
855
- readonly cwd: string;
856
- /** Approve the plan. Without it `migrate` previews and writes nothing. */
857
- readonly yes?: boolean;
858
- }
859
- /**
860
- * What a migration would do, without doing any of it.
861
- *
862
- * The move list is `oldLayoutEntries` — the same list every command's refusal is
863
- * keyed off — so the preview can never describe a different migration from the
864
- * one that runs.
865
- */
866
- declare function planMigrate(cwd: string): MigratePlan;
867
- /**
868
- * Performs a plan. Separate from {@link planMigrate} so what runs is the plan the
869
- * user approved, not a second reading of the disk between the question and the
870
- * answer.
871
- */
872
- declare function applyMigrate(plan: MigratePlan): MigrateResult;
873
- /** Plans, and applies only when the move was approved. */
874
- declare function runMigrate(options: MigrateOptions): MigrateResult;
875
- declare function renderMigrate(result: MigrateResult): string[];
876
-
877
- /**
878
- * `penv mv <from> <to>` — rename a parameter, every scope at once.
879
- *
880
- * A parameter is not one file. It is up to eight — four cascade levels, each
881
- * with a plaintext and an encrypted address — plus its meta, and a rename that
882
- * moved some of them would split one parameter into two. So this moves all of
883
- * them or none of them, and the whole plan is checked before a single byte is
884
- * written.
885
- *
886
- * **This is the only correct way to move an encrypted value.** A ciphertext is
887
- * sealed against the address it lives at, so `mv redis-password.production.enc
888
- * redis/password.production.enc` at the shell produces a file that will never
889
- * open again — the value is not moved, it is destroyed, and the shell reports
890
- * success. Re-sealing at the new address is the whole reason this command
891
- * exists: penv asked for namespacing to be "a deliberate refactor afterwards"
892
- * and then, once values could be encrypted, made doing it by hand a way to lose
893
- * them.
894
- *
895
- * It moves the tree and never the schema. `.penv/env.ts` is yours (invariant 2),
896
- * so renaming `database-url` to `database/url` leaves it declaring the old access
897
- * path — and the drift report is what says so. penv names the distance; you close
898
- * it. This command's report says which line to change rather than changing it.
899
- */
900
- interface MoveOptions {
901
- readonly cwd: string;
902
- readonly from: string;
903
- readonly to: string;
904
- }
905
- interface MovedFile {
906
- readonly from: string;
907
- readonly to: string;
908
- /** True when the value was opened and sealed again for its new address. */
909
- readonly resealed: boolean;
910
- }
911
- interface MoveResult {
912
- readonly from: string;
913
- readonly to: string;
914
- readonly files: readonly MovedFile[];
915
- /** The meta file's new location, or `undefined` when the parameter had none. */
916
- readonly meta: string | undefined;
917
- /** The access path the schema still declares, and the one it should now. */
918
- readonly schema: {
919
- readonly was: string;
920
- readonly now: string;
921
- };
922
- /** The file that holds the shape to rename — cohort-aware, so the tip names one that exists. */
923
- readonly schemaFile: string;
924
- }
925
- declare function runMove(options: MoveOptions): Promise<MoveResult>;
926
- declare function renderMove(result: MoveResult): string[];
927
-
928
- interface PullOptions {
929
- readonly cwd: string;
930
- readonly environment?: string;
931
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
932
- readonly envFlags?: readonly string[];
933
- /** Injected in tests: the source provider. Defaults to the one the config declares. */
934
- readonly source?: AnyProvider;
935
- }
936
- interface PullResult {
937
- readonly environment: string;
938
- /** The source provider's type — `filesystem` when the environment declares no separate backend. */
939
- readonly source: string;
940
- /**
941
- * True when the source *is* the local tree, so there was nothing to pull. The
942
- * caller distinguishes "pulled nothing because the backend was empty" from
943
- * "there is no backend to pull from" — opposite situations.
944
- */
945
- readonly localSource: boolean;
946
- /** Value files written into the local tree. */
947
- readonly values: number;
948
- /** Meta files written into the local tree. */
949
- readonly meta: number;
950
- /** Distinct parameters the pull touched, at any scope. */
951
- readonly refs: number;
952
- /**
953
- * True when the source declares `readsValues: false`: names and meta came
954
- * down, values stayed absent — the destination never returns one, and the
955
- * pull says so rather than dressing emptiness as freshness.
956
- */
957
- readonly valuesUnreadable?: boolean;
958
- }
959
- declare function runPull(options: PullOptions): Promise<PullResult>;
960
- declare function renderPull(result: PullResult): string[];
961
-
962
- /** The per-environment meta field recording penv's last push, compared against the destination's `updatedAt`. */
963
- declare const LAST_PUSHED_KEY = "lastPushedAt";
964
- interface PushOptions {
965
- readonly cwd: string;
966
- readonly environment?: string;
967
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
968
- readonly envFlags?: readonly string[];
969
- /** Permits sealed values to be decrypted locally and pushed as plaintext for the destination to re-seal. */
970
- readonly allowDecrypt?: boolean;
971
- /** Pre-approves creating a missing destination-side target (`--yes`). */
972
- readonly yes?: boolean;
973
- /** Injected in tests: the destination provider. Defaults to the one the environment's entry declares. */
974
- readonly provider?: AnyProvider;
975
- /** Injected in tests: answers the create-target question. Defaults to a terminal prompt. */
976
- readonly confirm?: (question: string) => Promise<boolean>;
977
- /** Injected in tests: the wall-clock reading recorded in meta. Defaults to now. */
978
- readonly now?: string;
979
- }
980
- interface PushResult {
981
- readonly environment: string;
982
- /** The destination provider's type — its package name. */
983
- readonly destination: string;
984
- /** What the destination holds, which decided what crossed. */
985
- readonly mode: "records" | "projection";
986
- /** Values sent — resolved secrets for a projection, value files for records. */
987
- readonly pushed: number;
988
- /** Meta records mirrored. Records mode only. */
989
- readonly meta: number;
990
- readonly repositorySecrets: number;
991
- readonly environmentSecrets: number;
992
- /** How many were sealed and crossed as plaintext for the destination to re-seal. */
993
- readonly decrypted: number;
994
- /** True when the destination-side target was created by this push, on approval. */
995
- readonly createdTarget: boolean;
996
- }
997
- declare function runPush(options: PushOptions): Promise<PushResult>;
998
- declare function renderPush(result: PushResult): string[];
999
-
1000
- interface RemoveOptions extends ScopeOptions {
1001
- readonly cwd: string;
1002
- readonly key: string;
1003
- }
1004
- interface RemoveResult {
1005
- readonly parameter: string;
1006
- /** The value files that existed and are now gone, relative to the records tree. */
1007
- readonly removed: readonly string[];
1008
- /** Both files penv looked at, whether or not they were there. */
1009
- readonly considered: readonly string[];
1010
- }
1011
- declare function runRemove(options: RemoveOptions): Promise<RemoveResult>;
1012
-
1013
- interface RotateOptions {
1014
- readonly cwd: string;
1015
- readonly key: string;
1016
- readonly environment?: string;
1017
- /** Open a `dual-valid` window: write the new value while the old is still retained. */
1018
- readonly begin?: boolean;
1019
- /** Close a `dual-valid` window: return to `active`, stamp the completion. */
1020
- readonly complete?: boolean;
1021
- /**
1022
- * The new value. A `begin` and an `atomic-cutover` flip write it; a `complete`
1023
- * does not touch the value at all, so it needs none. Injected in tests; on the
1024
- * CLI it is the positional argument or stdin, the same source `set` reads.
1025
- */
1026
- readonly value?: string;
1027
- /** Injected in tests: the wall-clock reading recorded in meta. Defaults to now. */
1028
- readonly now?: string;
1029
- }
1030
- /** The single step a run performed — the three the two mechanisms decompose into. */
1031
- type RotatePhase = "begin" | "complete" | "cutover";
1032
- interface RotateResult {
1033
- readonly parameter: string;
1034
- readonly environment: string;
1035
- readonly mechanism: RotationMechanism;
1036
- readonly phase: RotatePhase;
1037
- /** The source provider's type — where the value and its meta were written. */
1038
- readonly source: string;
1039
- /** True when this run wrote a new value. `begin` and `cutover` do; `complete` does not. */
1040
- readonly wroteValue: boolean;
1041
- /** The rotation state after this run — `rotating` after a begin, `active` otherwise. */
1042
- readonly state: RotationState;
1043
- /** When the current window opened, ISO. Set only after a `begin`, else `null`. */
1044
- readonly rotatingSince: string | null;
1045
- /** When a rotation last completed, ISO. Set after a `complete` or a `cutover`. */
1046
- readonly lastRotated: string | null;
1047
- }
1048
- declare function runRotate(options: RotateOptions): Promise<RotateResult>;
1049
- declare function renderRotate(result: RotateResult): string[];
1050
-
1051
- /**
1052
- * Starting someone else's command, opaquely.
1053
- *
1054
- * `penv run -- <command>` starts exactly what follows `--`: the argument
1055
- * boundaries the shell already worked out are handed to the operating system
1056
- * untouched, stdio is the parent's, and the child's exit code and terminating
1057
- * signal come back out. penv never parses the command, never rebuilds a command
1058
- * line from it, never wraps it in a shell — a shell would re-split what the user
1059
- * already split, and `penv run -- node -e "console.log(1 > 2)"` would redirect to
1060
- * a file called `2`.
1061
- *
1062
- * Windows is the one place where "hand it to the operating system" needs help.
1063
- * `pnpm`, `next` and every other node-installed tool are `.cmd` shims there, and
1064
- * Node refuses to execute one without a shell. So a `.cmd`/`.bat` target — and
1065
- * only that — is started through `cmd.exe /d /s /c` with
1066
- * `windowsVerbatimArguments`, building the one command line cmd will accept and
1067
- * escaping every argument so that cmd hands the child the same bytes penv was
1068
- * given. Everything else spawns directly, on every platform.
1069
- */
1070
-
1071
- /** How a child ended. Exactly one of these is meaningful, and both are forwarded. */
1072
- interface ChildResult {
1073
- /** The child's own exit code, or 1 when a signal ended it. */
1074
- readonly exitCode: number;
1075
- /** The signal that ended the child, when one did. */
1076
- readonly signal: NodeJS.Signals | null;
1077
- }
1078
- interface ChildInvocation {
1079
- /** The command exactly as it followed `--`: the executable, then its arguments. */
1080
- readonly command: readonly string[];
1081
- readonly env: Record<string, string>;
1082
- readonly cwd: string;
1083
- /**
1084
- * What penv is starting this on its own behalf to do — `init`'s dependency
1085
- * install. Absent means the command is the user's, from after `--`, and the
1086
- * two failures have opposite remedies: one is about what they typed, the other
1087
- * about a program penv chose to run.
1088
- */
1089
- readonly purpose?: string;
1090
- }
1091
- /** A started child: how it ends, and the one thing a wrapper may do to it. */
1092
- interface ChildHandle {
1093
- /** Resolves when the child has ended, however it ended. */
1094
- readonly ended: Promise<ChildResult>;
1095
- /** Asks the child to stop — what `--watch` does before it starts the next one. */
1096
- kill(signal?: NodeJS.Signals): void;
1097
- }
1098
- /** The seam `run` starts a child through — replaced in tests that assert what it was given. */
1099
- type StartChild = (invocation: ChildInvocation) => ChildHandle;
1100
-
1101
- /** Where a run reads its values from. `snapshot` is the sealed artifact. */
1102
- type RunSource = "project" | "snapshot";
1103
- interface RunOptions {
1104
- readonly cwd: string;
1105
- readonly environment?: string;
1106
- /** Bare flags the command did not declare — environment shorthands, judged against the whitelist. */
1107
- readonly envFlags?: readonly string[];
1108
- /** Defaults to `project`. */
1109
- readonly source?: string;
1110
- /** The one mode allowed to synchronise. Off by default. */
1111
- readonly watch?: boolean;
1112
- /** The command exactly as it followed `--`. */
1113
- readonly command: readonly string[];
1114
- /** The environment penv itself was started with. Defaults to `process.env`. */
1115
- readonly host?: Readonly<Record<string, string | undefined>>;
1116
- /** Injected in tests: how a child is started. */
1117
- readonly start?: StartChild;
1118
- /** Injected in tests: the sync `--watch` performs. */
1119
- readonly pull?: (options: PullOptions) => Promise<PullResult>;
1120
- /** Injected in tests: what tells `--watch` something changed. */
1121
- readonly changes?: (onChange: () => void) => {
1122
- close(): void;
1123
- };
1124
- /** How long a replaced child has to leave before `--watch` insists. Defaults to 5s. */
1125
- readonly stopGraceMs?: number;
1126
- }
1127
- interface RunResult {
1128
- readonly environment: string;
1129
- readonly source: RunSource;
1130
- readonly command: readonly string[];
1131
- /** Declared variables written into the child. */
1132
- readonly written: number;
1133
- /** Declared-but-valueless variables deleted from the child. */
1134
- readonly deleted: number;
1135
- /** penv's own variables removed before the child saw them. */
1136
- readonly stripped: readonly string[];
1137
- readonly exitCode: number;
1138
- readonly signal: NodeJS.Signals | null;
1139
- /** How many times `--watch` replaced the child. */
1140
- readonly restarts: number;
1141
- }
1142
- declare function runRun(options: RunOptions): Promise<RunResult>;
1143
-
1144
- interface WatchOptions {
1145
- readonly cwd: string;
1146
- readonly environment?: string;
1147
- /** Defaults to {@link DEBOUNCE_MS}. */
1148
- readonly debounceMs?: number;
1149
- /** Called with every completed validation, starting with the initial one. */
1150
- readonly onResult?: (result: ValidateResult) => void;
1151
- /**
1152
- * Called when a cycle could not produce a result at all — an unreadable
1153
- * config, a watcher the platform dropped. Never called for a *failing*
1154
- * validation: that is a result, and it goes to `onResult`.
1155
- */
1156
- readonly onError?: (error: unknown) => void;
1157
- }
1158
- interface WatchHandle {
1159
- /** Stops watching. Idempotent, and safe to call from inside a callback. */
1160
- close(): void;
1161
- }
1162
- /**
1163
- * Watches, and re-validates on change.
1164
- *
1165
- * Returns a handle rather than blocking, so the loop is a plain object a test
1166
- * can drive and close instead of a live process it would have to spawn. The
1167
- * command below is the only thing that turns it into a process that waits.
1168
- */
1169
- declare function runWatch(options: WatchOptions): WatchHandle;
1170
- /**
1171
- * One cycle's report: `penv validate`'s, with a rule above it — on a loop, the
1172
- * reader's first question is where the last run ended — and the drift below it.
1173
- *
1174
- * Drift comes last because it is the part that is not a verdict. The rows above
1175
- * say whether the configuration is valid; these say what the schema and the tree
1176
- * disagree about, which is often *why*, and is worth reading even on a run that
1177
- * passed.
1178
- */
1179
- declare function renderWatch(result: ValidateResult): string[];
1180
-
1181
- interface Cutover {
1182
- readonly format: number;
1183
- /** When the files were moved, ISO-8601 UTC. */
1184
- readonly movedAt: string;
1185
- /** The filenames as they were at the project root — what undo restores, exactly. */
1186
- readonly files: readonly string[];
1187
- /** The environments the cutover declared, so a report can name them without the config. */
1188
- readonly environments: readonly string[];
1189
- }
1190
- interface UndoResult {
1191
- readonly root: string;
1192
- /** The files put back, in the order they were moved. */
1193
- readonly restored: readonly string[];
1194
- /** Recorded names already at the project root — what an interrupted undo had reached. */
1195
- readonly alreadyBack: readonly string[];
1196
- /** Recorded names in neither the bundle nor the project root. Nothing penv can restore. */
1197
- readonly missing: readonly string[];
1198
- }
1199
- /**
1200
- * Puts every bundled file back under its exact original name, then drops the
1201
- * bundle and the state that named it.
1202
- *
1203
- * Undo is resumable, because the thing it recovers from is an interruption. A
1204
- * name already at the project root and no longer in the bundle is a file an
1205
- * earlier run put back, not a collision — only a name that is in both places at
1206
- * once is, and that is the one case worth refusing over, since restoring would
1207
- * write over whatever came back. A name in neither place is reported rather than
1208
- * refused: the old refusal's remedy was `penv cleanup`, which would have deleted
1209
- * every file that was still recoverable.
1210
- */
1211
- declare function runUndo(options: {
1212
- readonly cwd: string;
1213
- }): UndoResult;
1214
- interface CleanupResult {
1215
- readonly root: string;
1216
- /** The files the bundle held. Empty when there was nothing to clean up. */
1217
- readonly removed: readonly string[];
1218
- readonly cleaned: boolean;
1219
- }
1220
- /**
1221
- * Drops the rollback bundle and the cutover state, and nothing else. The records
1222
- * tree, the schema, the config and the loader are the project's — cleanup is the
1223
- * end of the migration, not the end of the adoption.
1224
- */
1225
- declare function runCleanup(options: {
1226
- readonly cwd: string;
1227
- }): CleanupResult;
1228
-
1229
- /**
1230
- * penv's command line.
1231
- *
1232
- * The wiring here is deliberately thin: every command's real work is a plain
1233
- * exported function that takes a `cwd` and returns a result, and citty only
1234
- * parses arguments, calls it, and prints what it returned. That is what lets the
1235
- * tests call the commands rather than spawn them.
1236
- */
1237
- declare const main: citty.CommandDef<citty.ArgsDef>;
1238
- declare function runMain(): Promise<void>;
1239
-
1240
- 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 };