neonctl 2.31.0 → 2.32.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/README.md +30 -1
- package/dist/commands/config.js +69 -64
- package/dist/commands/diff.js +222 -0
- package/dist/commands/index.js +2 -0
- package/dist/index.js +3 -0
- package/dist/utils/config_diff.js +179 -0
- package/dist/utils/git_diff.js +90 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -347,6 +347,34 @@ $ cat .neon
|
|
|
347
347
|
|
|
348
348
|
After pinning the branch, `checkout` also runs [`env pull`](#env-pull) by default, so the branch's Neon env vars are written to your local `.env` and you can start building right away — the branch-first loop is just `link` + `checkout`. Pass `--no-env-pull` to skip it (for example when env is injected at runtime via `neon-env run` / `neon dev`, or to keep secrets out of the working tree). A pull failure never undoes the checkout: the branch stays pinned and the failure is surfaced as a warning pointing you at `neon env pull` (or `neon deploy` if a `neon.ts`-declared service is missing).
|
|
349
349
|
|
|
350
|
+
### diff
|
|
351
|
+
|
|
352
|
+
`diff [compare-branch]` prints a **git-style** schema diff between the branch you're on and another branch — the top-level companion to `checkout`. It reads the branch pinned in `.neon` as the side under review (the `+++` side) and compares it against the branch you name (the `---`, reference side), so `+` lines are what your current branch adds on top of the reference:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# On feature/add-comments (pinned in .neon), see how it differs from main:
|
|
356
|
+
$ neon diff main
|
|
357
|
+
→ Comparing schema main → feature/add-comments
|
|
358
|
+
diff --neon database neondb
|
|
359
|
+
--- main (br-crimson-snow-12345678)
|
|
360
|
+
+++ feature/add-comments (br-dry-salad-87654321)
|
|
361
|
+
@@ -63,7 +64,8 @@
|
|
362
|
+
CREATE TABLE public.users (
|
|
363
|
+
id integer NOT NULL,
|
|
364
|
+
email text NOT NULL,
|
|
365
|
+
- created_at timestamp with time zone DEFAULT now()
|
|
366
|
+
+ created_at timestamp with time zone DEFAULT now(),
|
|
367
|
+
+ display_name text
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- **`compare-branch`** is optional. Omit it to compare the current branch against its **parent** (`neon diff` answers "what did I change since branching?"). It accepts a branch **name** or `br-…` **id**.
|
|
372
|
+
- **`--branch, -b <name|id>`** overrides the side under review instead of reading `.neon` — e.g. `neon diff main --branch feature/checkout` diffs an explicit branch against `main`.
|
|
373
|
+
- **`--database, --db <name>`** limits the diff to one database; by default every database on the current branch is compared (each rendered as its own `diff --neon database <name>` block). A database missing on the reference side shows as fully added.
|
|
374
|
+
- **`--output json|yaml`** emits a structured result per database (`{ database, base_branch, compare_branch, has_changes, diff }`) for scripting; the default renders the colorized git-style diff (respecting `--no-color` and non-TTY pipes).
|
|
375
|
+
|
|
376
|
+
The human-readable summary line goes to stderr and the diff body to stdout, so `neon diff main > changes.patch` captures just the diff. When the schemas match, `diff` prints `No schema differences …` and writes nothing to stdout. For history-aware comparisons (a branch against its own past state at a timestamp or LSN), use [`branches schema-diff`](https://neon.com/docs/reference/cli-branches#schema-diff).
|
|
377
|
+
|
|
350
378
|
### env pull
|
|
351
379
|
|
|
352
380
|
`env pull` writes the linked branch's Neon environment variables into a local dotenv file: an existing `.env` if you have one, otherwise `.env.local` (override with `--file <path>`). Only Neon-managed keys (`DATABASE_URL`, `DATABASE_URL_UNPOOLED`, and the Neon Auth / Data API URLs when those services are enabled) are written; any other lines in the file are preserved. The branch comes from the closest `.neon` file, so no `--branch` is needed (pass `--branch <id|name>` to target another branch).
|
|
@@ -420,7 +448,7 @@ The branch is chosen with `--branch <id|name>`; without it the project's default
|
|
|
420
448
|
- `--update-existing` — auto-confirm overriding existing remote settings on the branch. Without it, drift on settings already present remotely (compute, TTL, `protected`) is reported as a **conflict** and `apply` makes no changes until you resolve it or pass this flag.
|
|
421
449
|
- `--allow-protected` — auto-confirm applying to a branch Neon marks as protected. Without it, `apply` refuses to touch a protected branch.
|
|
422
450
|
|
|
423
|
-
**Output**: `status` prints the project, branch, and reverse-engineered config
|
|
451
|
+
**Output**: `status` prints the project, branch, and reverse-engineered config. `plan` / `apply` render a **`git diff`-style report** (matching [`neon diff`](#diff)): service changes (Neon Auth, Data API, buckets, functions) list as green `+` additions, while **branch setting changes** (TTL, `protected`, compute) show grouped under a `~ <branch>` header, one sorted `field → value` line each. A bare `apply` that hits drift on settings already present remotely prints those as a sorted **before→after** diff (`current → desired`, old in red / new in green) and exits non-zero until you pass `--update-existing`. Pass `--output json` (or `--output yaml`) to emit the full machine-readable result (`PushResult`) instead, for piping into other tools or CI.
|
|
424
452
|
|
|
425
453
|
**`config status --current-branch`** (alias `neon status --current-branch`) prints _only_ the branch pinned in the local `.neon` file — no network, no auth, no analytics — and exits non-zero when none is pinned. This behavior lets it safely drive a shell prompt. Example [starship](https://starship.rs) segment:
|
|
426
454
|
|
|
@@ -495,6 +523,7 @@ The target directory must be empty unless you pass `--force` (a lone `.git` is i
|
|
|
495
523
|
| set-context | | Deprecated; use `link` |
|
|
496
524
|
| env | `pull` | Manage a branch's env vars |
|
|
497
525
|
| checkout | | Pin a branch in `.neon` |
|
|
526
|
+
| diff | | Git-style schema diff vs a branch |
|
|
498
527
|
| [link](https://neon.com/docs/reference/cli-link) | | Link a directory to a project |
|
|
499
528
|
| config | `status`, `plan`, `apply` | Drive a branch from `neon.ts` |
|
|
500
529
|
| deploy | | Alias for `config apply` |
|
package/dist/commands/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolveConfig } from "@neon/config";
|
|
4
|
-
import { apply, createBranch as createBranchFromPolicy, inspect, loadConfigFromFile, plan, } from "@neon/config-runtime";
|
|
4
|
+
import { apply, createBranch as createBranchFromPolicy, inspect, loadConfigFromFile, PushConflictError, plan, } from "@neon/config-runtime";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { toNeonConfigView } from "../config_format.js";
|
|
7
7
|
import { contextBranch, readContextFile } from "../context.js";
|
|
@@ -9,6 +9,7 @@ import { isCi } from "../env.js";
|
|
|
9
9
|
import { loadEnvFileIntoProcess } from "../env_file.js";
|
|
10
10
|
import { log } from "../log.js";
|
|
11
11
|
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
12
|
+
import { renderAppliedChanges, renderBranchSettingConflicts, } from "../utils/config_diff.js";
|
|
12
13
|
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
13
14
|
import { bundleEntry } from "../utils/esbuild.js";
|
|
14
15
|
import { addDependenciesArgs, resolvePackageManager, runCommand, } from "../utils/package_manager.js";
|
|
@@ -23,19 +24,6 @@ import { autoPullEnvAfterPin } from "./env.js";
|
|
|
23
24
|
*/
|
|
24
25
|
const neonctlBundler = async (fn) => zipBundle(await bundleEntry(fn.source));
|
|
25
26
|
const INSPECT_FIELDS = ["project", "branch", "config"];
|
|
26
|
-
// Deliberately minimal: action/kind/identifier are short and fixed-ish, so the table can
|
|
27
|
-
// never overflow. Per-change `details` (a function's long invocationUrl in particular) are
|
|
28
|
-
// intentionally NOT a column — they used to be JSON-stringified into a cell and blew the
|
|
29
|
-
// table past 190 cols. Function URLs are printed below as a plain list (see reportPushResult),
|
|
30
|
-
// and the full details are still available via `--output json`.
|
|
31
|
-
const APPLIED_FIELDS = ["action", "kind", "identifier"];
|
|
32
|
-
const CONFLICT_FIELDS = [
|
|
33
|
-
"identifier",
|
|
34
|
-
"field",
|
|
35
|
-
"current",
|
|
36
|
-
"desired",
|
|
37
|
-
"reason",
|
|
38
|
-
];
|
|
39
27
|
/**
|
|
40
28
|
* Shared `--env` flag for `config plan|apply` and `deploy`. Loads a `.env` into
|
|
41
29
|
* `process.env` before the policy is evaluated.
|
|
@@ -330,16 +318,29 @@ export const applyCmd = async (props) => {
|
|
|
330
318
|
const branch = await resolveBranchRef(props);
|
|
331
319
|
announceTargetBranch(props, branch, "Applying to branch");
|
|
332
320
|
const branchId = branch.branchId;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
321
|
+
let result;
|
|
322
|
+
try {
|
|
323
|
+
result = await apply(config, {
|
|
324
|
+
projectId: props.projectId,
|
|
325
|
+
branchId,
|
|
326
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
327
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
328
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
329
|
+
...(props.updateExisting ? { updateExisting: true } : {}),
|
|
330
|
+
...(props.allowProtected ? { allowProtectedBranch: true } : {}),
|
|
331
|
+
bundleFunction: neonctlBundler,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
// Drift without `--update-existing` throws with the conflicting fields attached.
|
|
336
|
+
// Render them as the same git-style before→after diff, then fail with a concise
|
|
337
|
+
// message (the detailed diff above replaces the library's long multi-line text).
|
|
338
|
+
if (err instanceof PushConflictError) {
|
|
339
|
+
reportConflicts(props, err.conflicts);
|
|
340
|
+
throw new Error("Branch settings conflict with the policy. Re-run with --update-existing to apply the changes shown above.");
|
|
341
|
+
}
|
|
342
|
+
throw err;
|
|
343
|
+
}
|
|
343
344
|
reportPushResult(props, result, "apply", utilizedServices(config));
|
|
344
345
|
// After a successful apply/deploy, write the branch's Neon env vars to a local .env —
|
|
345
346
|
// the same bundled convenience as `link` / `checkout`, so the branch is immediately
|
|
@@ -388,56 +389,44 @@ const utilizedServices = (config) => {
|
|
|
388
389
|
/**
|
|
389
390
|
* Render a {@link PushResult}. JSON/YAML output emits the raw result (plus a `services`
|
|
390
391
|
* summary) verbatim so it can be piped; the human-readable path renders the actual changes
|
|
391
|
-
* (dropping noops) and any blocking conflicts as
|
|
392
|
-
* are empty — and always closes with the list of services the policy
|
|
393
|
-
* that produces no plan step (Postgres, or the credential-gated AI
|
|
394
|
-
* for being missing from the plan above.
|
|
392
|
+
* (dropping noops) and any blocking conflicts as a `git diff`-style report, or a "nothing to
|
|
393
|
+
* do" line when both are empty — and always closes with the list of services the policy
|
|
394
|
+
* utilizes so a service that produces no plan step (Postgres, or the credential-gated AI
|
|
395
|
+
* Gateway) isn't mistaken for being missing from the plan above.
|
|
396
|
+
*
|
|
397
|
+
* The diff is asymmetric on purpose (see the CLI's `neon diff`): **service** changes are
|
|
398
|
+
* additions with no "before", so they list as `+`/`~` lines; **branch setting** changes have
|
|
399
|
+
* a natural before→after, so conflicts render as a sorted `current → desired` diff. Planned
|
|
400
|
+
* branch updates (under `--update-existing`) carry only the new value, so they render
|
|
401
|
+
* desired-only for now (the previous value isn't threaded through the runtime yet).
|
|
395
402
|
*/
|
|
396
403
|
const reportPushResult = (props, result, mode, services) => {
|
|
397
404
|
if (props.output === "json" || props.output === "yaml") {
|
|
398
405
|
writer(props).end({ ...result, services }, { fields: [] });
|
|
399
406
|
return;
|
|
400
407
|
}
|
|
401
|
-
const
|
|
402
|
-
.filter((change) => change.action !== "noop")
|
|
403
|
-
.map((change) => ({
|
|
404
|
-
action: change.action,
|
|
405
|
-
kind: change.kind,
|
|
406
|
-
identifier: change.identifier,
|
|
407
|
-
}));
|
|
408
|
-
const conflicts = result.conflicts.map((conflict) => ({
|
|
409
|
-
identifier: conflict.identifier,
|
|
410
|
-
field: conflict.field,
|
|
411
|
-
current: stringify(conflict.current),
|
|
412
|
-
desired: stringify(conflict.desired),
|
|
413
|
-
reason: conflict.reason,
|
|
414
|
-
}));
|
|
408
|
+
const appliedChanges = result.applied.filter((change) => change.action !== "noop");
|
|
415
409
|
// Deployed functions carry their invocation URL in the change details — collect them so
|
|
416
410
|
// we can list where to call each function without digging through the raw details blob.
|
|
417
411
|
// Keyed by slug so a function never shows twice.
|
|
418
412
|
const functionUrlBySlug = new Map();
|
|
419
|
-
for (const change of
|
|
420
|
-
if (change.action === "noop")
|
|
421
|
-
continue;
|
|
413
|
+
for (const change of appliedChanges) {
|
|
422
414
|
const slug = change.details?.slug;
|
|
423
415
|
const invocationUrl = change.details?.invocationUrl;
|
|
424
416
|
if (typeof slug === "string" && typeof invocationUrl === "string") {
|
|
425
417
|
functionUrlBySlug.set(slug, invocationUrl);
|
|
426
418
|
}
|
|
427
419
|
}
|
|
420
|
+
// chalk self-detects TTY/NO_COLOR; `--no-color` (props.color === false) forces plain.
|
|
421
|
+
const color = props.color !== false;
|
|
428
422
|
const out = writer(props);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (conflicts.length > 0) {
|
|
437
|
-
out.write(conflicts, { fields: CONFLICT_FIELDS, title: "Conflicts" });
|
|
438
|
-
}
|
|
439
|
-
// Flush any tables, then append the lists/summary so they read directly below them.
|
|
440
|
-
out.end();
|
|
423
|
+
// Conflicts never reach here in the CLI: `plan` runs with updateExisting on, and a bare
|
|
424
|
+
// `apply` throws PushConflictError (rendered by reportConflicts). So an empty applied set
|
|
425
|
+
// is the whole story here.
|
|
426
|
+
const noChanges = appliedChanges.length === 0;
|
|
427
|
+
const appliedText = renderAppliedChanges(appliedChanges, mode === "plan" ? "Planned changes" : "Applied changes", { color });
|
|
428
|
+
if (appliedText)
|
|
429
|
+
out.text(`${appliedText}\n`);
|
|
441
430
|
// Function URLs are a plain list rather than a table: an invocation URL can be 70+ chars,
|
|
442
431
|
// which makes any bordered table overflow and wrap awkwardly in a normal terminal. A list
|
|
443
432
|
// lets each URL reflow on its own line, and stays copy-pasteable.
|
|
@@ -452,15 +441,31 @@ const reportPushResult = (props, result, mode, services) => {
|
|
|
452
441
|
log.info(`No changes — branch ${result.branchName} already matches the policy.`);
|
|
453
442
|
}
|
|
454
443
|
out.text(`\nUtilized services: ${services.join(", ")}\n`);
|
|
455
|
-
|
|
456
|
-
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Render the branch-setting {@link ConflictReport}s a bare `apply` refused to override (drift
|
|
447
|
+
* without `--update-existing`) as the git-style before→after diff. JSON/YAML output emits the
|
|
448
|
+
* structured conflicts so it can be piped; the human path prints the sorted diff followed by
|
|
449
|
+
* any conflict whose fix is *not* `--update-existing` (e.g. an immutable "no endpoint" case),
|
|
450
|
+
* so nothing the library's error message carried is lost.
|
|
451
|
+
*/
|
|
452
|
+
const reportConflicts = (props, conflicts) => {
|
|
453
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
454
|
+
writer(props).end({ conflicts }, { fields: [] });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const out = writer(props);
|
|
458
|
+
const text = renderBranchSettingConflicts([...conflicts], {
|
|
459
|
+
color: props.color !== false,
|
|
460
|
+
});
|
|
461
|
+
if (text)
|
|
462
|
+
out.text(`${text}\n`);
|
|
463
|
+
for (const conflict of conflicts) {
|
|
464
|
+
if (!/updateExisting/i.test(conflict.reason)) {
|
|
465
|
+
out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
|
|
466
|
+
}
|
|
457
467
|
}
|
|
458
468
|
};
|
|
459
|
-
const stringify = (value) => value === undefined
|
|
460
|
-
? ""
|
|
461
|
-
: typeof value === "string"
|
|
462
|
-
? value
|
|
463
|
-
: JSON.stringify(value);
|
|
464
469
|
/**
|
|
465
470
|
* Apply a `neon.ts` policy to a **freshly created** branch (used by `neonctl checkout`
|
|
466
471
|
* when it creates a branch). No-op when there is no `neon.ts` on the path from cwd up to
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { isNeonApiError } from "../api.js";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
import { fillSingleProject } from "../utils/enrichers.js";
|
|
5
|
+
import { looksLikeBranchId } from "../utils/formats.js";
|
|
6
|
+
import { renderDatabaseSchemaDiff, renderSchemaDiffReport, } from "../utils/git_diff.js";
|
|
7
|
+
import { writer } from "../writer.js";
|
|
8
|
+
// A top-level shortcut for `branches schema-diff`, framed like `git diff`: it
|
|
9
|
+
// compares the branch you're on (pinned in `.neon`, or `--branch`) against the
|
|
10
|
+
// branch you name, and prints a git-style unified schema diff. Because it has a
|
|
11
|
+
// handler but no subcommands, `diff` is also listed in `NO_SUBCOMMANDS_VERBS`
|
|
12
|
+
// (see index.ts) so a bare `neon diff main` isn't intercepted by the help
|
|
13
|
+
// fallback.
|
|
14
|
+
export const command = "diff [compare-branch]";
|
|
15
|
+
export const describe = "Show a git-style schema diff between the current branch and another branch";
|
|
16
|
+
export const builder = (argv) => argv
|
|
17
|
+
.usage("$0 diff [compare-branch] [options]")
|
|
18
|
+
.positional("compare-branch", {
|
|
19
|
+
describe: "Branch name or id to compare against (the reference / '---' side). " +
|
|
20
|
+
"Defaults to the current branch's parent.",
|
|
21
|
+
type: "string",
|
|
22
|
+
})
|
|
23
|
+
.options({
|
|
24
|
+
"project-id": {
|
|
25
|
+
describe: "Project ID",
|
|
26
|
+
type: "string",
|
|
27
|
+
},
|
|
28
|
+
branch: {
|
|
29
|
+
alias: "b",
|
|
30
|
+
describe: "The branch to review (the '+++' side). Defaults to the branch " +
|
|
31
|
+
"pinned in the local context (.neon).",
|
|
32
|
+
type: "string",
|
|
33
|
+
},
|
|
34
|
+
database: {
|
|
35
|
+
alias: "db",
|
|
36
|
+
describe: "Limit the diff to a single database. Defaults to every database on the current branch.",
|
|
37
|
+
type: "string",
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
.middleware(fillSingleProject)
|
|
41
|
+
.middleware((args) => {
|
|
42
|
+
// The positional arrives as `compare-branch`; surface it under the
|
|
43
|
+
// camelCase name the handler reads, and mirror it to `branchId` for
|
|
44
|
+
// analytics (same pattern as the `branches` command group).
|
|
45
|
+
const compareBranch = args["compare-branch"];
|
|
46
|
+
if (typeof compareBranch === "string") {
|
|
47
|
+
args.compareBranch = compareBranch;
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.example([
|
|
51
|
+
[
|
|
52
|
+
"$0 diff main",
|
|
53
|
+
"Diff the current branch's schema against the main branch",
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"$0 diff",
|
|
57
|
+
"Diff the current branch's schema against its parent branch",
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
"$0 diff main --branch feature/checkout",
|
|
61
|
+
"Diff an explicit branch against main (ignoring the .neon context)",
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"$0 diff main --db neondb",
|
|
65
|
+
"Diff only the neondb database against main",
|
|
66
|
+
],
|
|
67
|
+
]);
|
|
68
|
+
export const handler = async (props) => {
|
|
69
|
+
const branches = (await props.apiClient.listProjectBranches({
|
|
70
|
+
projectId: props.projectId,
|
|
71
|
+
})).data.branches;
|
|
72
|
+
const after = resolveAfterBranch(branches, props.branch);
|
|
73
|
+
const before = resolveBeforeBranch(branches, after, props.compareBranch);
|
|
74
|
+
if (before.branchId === after.branchId) {
|
|
75
|
+
throw new Error(`Nothing to compare: both sides resolve to branch ${after.branchName} (${after.branchId}).`);
|
|
76
|
+
}
|
|
77
|
+
const databases = await resolveDatabases(props, after);
|
|
78
|
+
const diffs = [];
|
|
79
|
+
for (const database of databases) {
|
|
80
|
+
const [beforeSql, afterSql] = await Promise.all([
|
|
81
|
+
fetchSchemaSql(props, before.branchId, database),
|
|
82
|
+
fetchSchemaSql(props, after.branchId, database),
|
|
83
|
+
]);
|
|
84
|
+
diffs.push({
|
|
85
|
+
database,
|
|
86
|
+
before: { ...before, sql: beforeSql },
|
|
87
|
+
after: { ...after, sql: afterSql },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
91
|
+
writeStructured(props, diffs);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
log.info("%s Comparing schema %s → %s", chalk.dim("→"), chalk.red(`${before.branchName}`), chalk.green(`${after.branchName}`));
|
|
95
|
+
const { hasChanges, text } = renderSchemaDiffReport(diffs, {
|
|
96
|
+
color: props.color !== false,
|
|
97
|
+
});
|
|
98
|
+
if (!hasChanges) {
|
|
99
|
+
log.info("No schema differences between %s and %s.", before.branchName, after.branchName);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
writer(props).text(`${text}\n`);
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the branch under review (`+++` side). Prefers the explicit
|
|
106
|
+
* `branch`/`--branch` value (name or `br-…` id), falling back to the project's
|
|
107
|
+
* default branch. An unknown `br-…` id is trusted as-is (it may be too new to
|
|
108
|
+
* appear in the listing); an unknown *name* is a hard error.
|
|
109
|
+
*/
|
|
110
|
+
const resolveAfterBranch = (branches, ref) => {
|
|
111
|
+
if (ref) {
|
|
112
|
+
return resolveRef(branches, ref);
|
|
113
|
+
}
|
|
114
|
+
const def = branches.find((b) => b.default);
|
|
115
|
+
if (!def) {
|
|
116
|
+
throw new Error("No branch specified and no default branch found. Pass --branch <name|id>.");
|
|
117
|
+
}
|
|
118
|
+
return { branchId: def.id, branchName: def.name ?? def.id };
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the reference branch (`---` side). Uses the `compare-branch`
|
|
122
|
+
* positional when given, otherwise the parent of the branch under review — so a
|
|
123
|
+
* bare `neon diff` answers "what did I change since branching?".
|
|
124
|
+
*/
|
|
125
|
+
const resolveBeforeBranch = (branches, after, ref) => {
|
|
126
|
+
if (ref) {
|
|
127
|
+
return resolveRef(branches, ref);
|
|
128
|
+
}
|
|
129
|
+
const afterBranch = branches.find((b) => b.id === after.branchId);
|
|
130
|
+
const parentId = afterBranch?.parent_id;
|
|
131
|
+
if (!parentId) {
|
|
132
|
+
throw new Error(`Branch "${after.branchName}" has no parent to compare against. ` +
|
|
133
|
+
"Pass a branch to compare with, e.g. `neon diff main`.");
|
|
134
|
+
}
|
|
135
|
+
const parent = branches.find((b) => b.id === parentId);
|
|
136
|
+
return {
|
|
137
|
+
branchId: parentId,
|
|
138
|
+
branchName: parent?.name ?? parentId,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
/** Resolve a branch reference (name or `br-…` id) against the fetched listing. */
|
|
142
|
+
const resolveRef = (branches, ref) => {
|
|
143
|
+
const found = looksLikeBranchId(ref)
|
|
144
|
+
? branches.find((b) => b.id === ref)
|
|
145
|
+
: branches.find((b) => b.name === ref);
|
|
146
|
+
if (found) {
|
|
147
|
+
return { branchId: found.id, branchName: found.name ?? found.id };
|
|
148
|
+
}
|
|
149
|
+
// A `br-…` id absent from the listing is still usable as an id; only an
|
|
150
|
+
// unresolved name is a genuine error (mirrors resolveBranchRef in enrichers).
|
|
151
|
+
if (looksLikeBranchId(ref)) {
|
|
152
|
+
return { branchId: ref, branchName: ref };
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Branch ${ref} not found.\nAvailable branches: ${branches
|
|
155
|
+
.map((b) => b.name)
|
|
156
|
+
.join(", ")}`);
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* The databases to diff: the one passed via `--database` (validated against the
|
|
160
|
+
* branch under review), or every database on that branch when none is given.
|
|
161
|
+
*/
|
|
162
|
+
const resolveDatabases = async (props, after) => {
|
|
163
|
+
const databases = (await props.apiClient.listProjectBranchDatabases(props.projectId, after.branchId)).data.databases;
|
|
164
|
+
if (props.database !== undefined) {
|
|
165
|
+
if (!databases.find((d) => d.name === props.database)) {
|
|
166
|
+
throw new Error(`Database "${props.database}" not found on branch ${after.branchName}. ` +
|
|
167
|
+
`Available: ${databases.map((d) => d.name).join(", ")}`);
|
|
168
|
+
}
|
|
169
|
+
return [props.database];
|
|
170
|
+
}
|
|
171
|
+
if (databases.length === 0) {
|
|
172
|
+
throw new Error(`No databases found on branch ${after.branchName} (${after.branchId}).`);
|
|
173
|
+
}
|
|
174
|
+
return databases.map((d) => d.name);
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Fetch a branch database's `CREATE …` SQL. A database absent from the branch
|
|
178
|
+
* (404) yields an empty schema, so the diff shows it as fully added/removed
|
|
179
|
+
* rather than failing — the natural outcome when a database exists on only one
|
|
180
|
+
* side of the comparison.
|
|
181
|
+
*/
|
|
182
|
+
const fetchSchemaSql = async (props, branchId, database) => {
|
|
183
|
+
try {
|
|
184
|
+
const { data } = await props.apiClient.getProjectBranchSchema({
|
|
185
|
+
projectId: props.projectId,
|
|
186
|
+
branchId,
|
|
187
|
+
db_name: database,
|
|
188
|
+
});
|
|
189
|
+
return data.sql ?? "";
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
193
|
+
log.debug("diff: database %s not found on branch %s; treating schema as empty", database, branchId);
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/** Machine-readable output for `--output json|yaml`: one entry per database. */
|
|
200
|
+
const writeStructured = (props, diffs) => {
|
|
201
|
+
const report = diffs.map((diff) => {
|
|
202
|
+
const rendered = renderDatabaseSchemaDiff(diff, { color: false });
|
|
203
|
+
return {
|
|
204
|
+
database: diff.database,
|
|
205
|
+
base_branch: diff.before.branchName,
|
|
206
|
+
base_branch_id: diff.before.branchId,
|
|
207
|
+
compare_branch: diff.after.branchName,
|
|
208
|
+
compare_branch_id: diff.after.branchId,
|
|
209
|
+
has_changes: rendered.hasChanges,
|
|
210
|
+
diff: rendered.text,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
writer(props).end(report, {
|
|
214
|
+
fields: [
|
|
215
|
+
"database",
|
|
216
|
+
"base_branch",
|
|
217
|
+
"compare_branch",
|
|
218
|
+
"has_changes",
|
|
219
|
+
"diff",
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
};
|
package/dist/commands/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import * as dataApi from "./data_api.js";
|
|
|
10
10
|
import * as databases from "./databases.js";
|
|
11
11
|
import * as deploy from "./deploy.js";
|
|
12
12
|
import * as dev from "./dev.js";
|
|
13
|
+
import * as diff from "./diff.js";
|
|
13
14
|
import * as env from "./env.js";
|
|
14
15
|
import * as functions from "./functions.js";
|
|
15
16
|
import * as init from "./init.js";
|
|
@@ -47,6 +48,7 @@ export default [
|
|
|
47
48
|
dataApi,
|
|
48
49
|
functions,
|
|
49
50
|
dev,
|
|
51
|
+
diff,
|
|
50
52
|
config,
|
|
51
53
|
status,
|
|
52
54
|
deploy,
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,9 @@ const NO_SUBCOMMANDS_VERBS = [
|
|
|
31
31
|
"init",
|
|
32
32
|
"dev",
|
|
33
33
|
"deploy",
|
|
34
|
+
// `diff <compare-branch>` has a handler but no subcommands (like `status`),
|
|
35
|
+
// so the help-fallback middleware must not intercept a bare `neon diff main`.
|
|
36
|
+
"diff",
|
|
34
37
|
"bootstrap",
|
|
35
38
|
// alias of `config status`
|
|
36
39
|
"status",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
const UNSET = "(unset)";
|
|
3
|
+
/**
|
|
4
|
+
* Shared palette for the config diff, matching `neon diff`'s `git diff` styling
|
|
5
|
+
* (bold headers, red "before", green "after", dim connective glyphs). When
|
|
6
|
+
* `color` is false every entry is the identity function, so the identical layout
|
|
7
|
+
* renders without ANSI codes for `--no-color`, non-TTY pipes, and test snapshots.
|
|
8
|
+
*/
|
|
9
|
+
const palette = (color) => {
|
|
10
|
+
const id = (s) => s;
|
|
11
|
+
if (!color) {
|
|
12
|
+
return { title: id, group: id, added: id, removed: id, arrow: id };
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
title: (s) => chalk.bold(s),
|
|
16
|
+
group: (s) => chalk.bold(s),
|
|
17
|
+
added: (s) => chalk.green(s),
|
|
18
|
+
removed: (s) => chalk.red(s),
|
|
19
|
+
arrow: (s) => chalk.dim(s),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
const stringifyValue = (value) => {
|
|
24
|
+
if (value === undefined || value === null || value === "")
|
|
25
|
+
return UNSET;
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
return value;
|
|
28
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
return JSON.stringify(value);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Expand one policy field into flat {@link FieldChange}s. When either side is a
|
|
35
|
+
* plain object (compute settings, Data API settings) it splits into one
|
|
36
|
+
* `field.key` entry per key across both sides, so the diff reads as sorted
|
|
37
|
+
* scalar lines (`computeSettings.autoscalingLimitMaxCu 2 → 4`) rather than an
|
|
38
|
+
* opaque JSON blob. Scalars produce a single entry.
|
|
39
|
+
*/
|
|
40
|
+
const expandField = (field, current, desired) => {
|
|
41
|
+
if (isPlainObject(current) || isPlainObject(desired)) {
|
|
42
|
+
const cur = isPlainObject(current) ? current : {};
|
|
43
|
+
const des = isPlainObject(desired) ? desired : {};
|
|
44
|
+
const keys = Array.from(new Set([...Object.keys(cur), ...Object.keys(des)])).sort();
|
|
45
|
+
return keys.map((key) => ({
|
|
46
|
+
field: `${field}.${key}`,
|
|
47
|
+
...(key in cur ? { current: stringifyValue(cur[key]) } : {}),
|
|
48
|
+
desired: stringifyValue(des[key]),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
field,
|
|
54
|
+
...(current !== undefined
|
|
55
|
+
? { current: stringifyValue(current) }
|
|
56
|
+
: {}),
|
|
57
|
+
desired: stringifyValue(desired),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
};
|
|
61
|
+
/** Render the indented `field current → desired` lines for one branch group. */
|
|
62
|
+
const renderFieldLines = (fields, paint) => {
|
|
63
|
+
const width = fields.reduce((max, f) => Math.max(max, f.field.length), 0);
|
|
64
|
+
return fields.map((f) => {
|
|
65
|
+
const name = f.field.padEnd(width);
|
|
66
|
+
const arrow = paint.arrow("→");
|
|
67
|
+
return f.current !== undefined
|
|
68
|
+
? ` ${name} ${paint.removed(f.current)} ${arrow} ${paint.added(f.desired)}`
|
|
69
|
+
: ` ${name} ${arrow} ${paint.added(f.desired)}`;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
/** Group field changes by branch, each block sorted alphabetically by field. */
|
|
73
|
+
const renderBranchGroups = (byBranch, paint) => {
|
|
74
|
+
const lines = [];
|
|
75
|
+
const groups = [...byBranch.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
76
|
+
for (const [branch, fields] of groups) {
|
|
77
|
+
const sorted = [...fields].sort((a, b) => a.field.localeCompare(b.field));
|
|
78
|
+
lines.push(` ${paint.group(`~ ${branch}`)}`);
|
|
79
|
+
lines.push(...renderFieldLines(sorted, paint));
|
|
80
|
+
}
|
|
81
|
+
return lines;
|
|
82
|
+
};
|
|
83
|
+
/** Friendly label for a service change identifier (`bucket:x` → `bucket x`). */
|
|
84
|
+
const serviceLabel = (identifier) => {
|
|
85
|
+
if (identifier === "auth")
|
|
86
|
+
return "Neon Auth";
|
|
87
|
+
if (identifier === "dataApi")
|
|
88
|
+
return "Data API";
|
|
89
|
+
if (identifier.startsWith("bucket:")) {
|
|
90
|
+
return `bucket ${identifier.slice("bucket:".length)}`;
|
|
91
|
+
}
|
|
92
|
+
if (identifier.startsWith("function:")) {
|
|
93
|
+
return `function ${identifier.slice("function:".length)}`;
|
|
94
|
+
}
|
|
95
|
+
return identifier;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The desired-only field changes for an applied/planned **branch** update. The
|
|
99
|
+
* synthesized `AppliedChange.details` carry the new value keyed by `field`
|
|
100
|
+
* (`ttl`→`expiresAt`, `protected`→`protected`, `computeSettings`→`settings`);
|
|
101
|
+
* the previous value isn't threaded through in Phase 1, so these render as
|
|
102
|
+
* `field → desired` (no red "before"). Object settings expand into sub-fields.
|
|
103
|
+
*/
|
|
104
|
+
const appliedBranchFields = (change) => {
|
|
105
|
+
const details = change.details ?? {};
|
|
106
|
+
const field = typeof details.field === "string" ? details.field : "setting";
|
|
107
|
+
switch (field) {
|
|
108
|
+
case "ttl":
|
|
109
|
+
return expandField("ttl", undefined, details.expiresAt);
|
|
110
|
+
case "protected":
|
|
111
|
+
return expandField("protected", undefined, details.protected);
|
|
112
|
+
case "computeSettings":
|
|
113
|
+
return expandField("computeSettings", undefined, details.settings);
|
|
114
|
+
default:
|
|
115
|
+
return expandField(field, undefined, details.settings);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Render the applied (or planned) changes as a `git diff`-style list:
|
|
120
|
+
*
|
|
121
|
+
* - **Service changes** (auth / Data API / buckets / functions) are additions
|
|
122
|
+
* with no meaningful "before", so they list as green `+ <label>` lines
|
|
123
|
+
* (`~ <label>` for the lone Data API settings *update*).
|
|
124
|
+
* - **Branch setting changes** group per branch and render as sorted
|
|
125
|
+
* `field → value` lines under a `~ <branch>` header.
|
|
126
|
+
*
|
|
127
|
+
* Returns "" when there is nothing to show. Pure — callers own the heading
|
|
128
|
+
* spacing and any surrounding summary.
|
|
129
|
+
*/
|
|
130
|
+
export const renderAppliedChanges = (changes, title, opts) => {
|
|
131
|
+
if (changes.length === 0)
|
|
132
|
+
return "";
|
|
133
|
+
const paint = palette(opts.color);
|
|
134
|
+
const lines = [paint.title(title)];
|
|
135
|
+
const services = changes
|
|
136
|
+
.filter((c) => c.kind === "service")
|
|
137
|
+
.sort((a, b) => a.identifier.localeCompare(b.identifier));
|
|
138
|
+
for (const service of services) {
|
|
139
|
+
const label = serviceLabel(service.identifier);
|
|
140
|
+
lines.push(service.action === "create"
|
|
141
|
+
? ` ${paint.added(`+ ${label}`)}`
|
|
142
|
+
: ` ${paint.arrow(`~ ${label}`)}`);
|
|
143
|
+
}
|
|
144
|
+
const byBranch = new Map();
|
|
145
|
+
for (const change of changes.filter((c) => c.kind === "branch")) {
|
|
146
|
+
const existing = byBranch.get(change.identifier) ?? [];
|
|
147
|
+
byBranch.set(change.identifier, [
|
|
148
|
+
...existing,
|
|
149
|
+
...appliedBranchFields(change),
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
lines.push(...renderBranchGroups(byBranch, paint));
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Render branch-setting **conflicts** (drift the policy wants to change but that
|
|
157
|
+
* needs `--update-existing`) as a `git diff`-style before→after report: grouped
|
|
158
|
+
* per branch, sorted by field, `current → desired` with the old value in red and
|
|
159
|
+
* the new in green. Conflicts already carry both sides, so this is the fullest
|
|
160
|
+
* form of the diff. Returns "" when there are no conflicts.
|
|
161
|
+
*/
|
|
162
|
+
export const renderBranchSettingConflicts = (conflicts, opts) => {
|
|
163
|
+
if (conflicts.length === 0)
|
|
164
|
+
return "";
|
|
165
|
+
const paint = palette(opts.color);
|
|
166
|
+
const byBranch = new Map();
|
|
167
|
+
for (const conflict of conflicts) {
|
|
168
|
+
const existing = byBranch.get(conflict.identifier) ?? [];
|
|
169
|
+
byBranch.set(conflict.identifier, [
|
|
170
|
+
...existing,
|
|
171
|
+
...expandField(conflict.field, conflict.current, conflict.desired),
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
const lines = [
|
|
175
|
+
paint.title("Branch settings differ (re-run with --update-existing to apply)"),
|
|
176
|
+
...renderBranchGroups(byBranch, paint),
|
|
177
|
+
];
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { structuredPatch } from "diff";
|
|
3
|
+
/**
|
|
4
|
+
* Color palette for the rendered diff. When `color` is false every entry is the
|
|
5
|
+
* identity function, so the exact same layout is emitted without ANSI codes
|
|
6
|
+
* (for `--no-color`, non-TTY pipes, and stable test snapshots).
|
|
7
|
+
*/
|
|
8
|
+
const palette = (color) => {
|
|
9
|
+
const id = (s) => s;
|
|
10
|
+
if (!color) {
|
|
11
|
+
return {
|
|
12
|
+
header: id,
|
|
13
|
+
removedFile: id,
|
|
14
|
+
addedFile: id,
|
|
15
|
+
hunk: id,
|
|
16
|
+
added: id,
|
|
17
|
+
removed: id,
|
|
18
|
+
noNewline: id,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
header: (s) => chalk.bold(s),
|
|
23
|
+
removedFile: (s) => chalk.bold.red(s),
|
|
24
|
+
addedFile: (s) => chalk.bold.green(s),
|
|
25
|
+
hunk: (s) => chalk.cyan(s),
|
|
26
|
+
added: (s) => chalk.green(s),
|
|
27
|
+
removed: (s) => chalk.red(s),
|
|
28
|
+
noNewline: (s) => chalk.dim(s),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const branchLabel = (schema) => `${schema.branchName} (${schema.branchId})`;
|
|
32
|
+
/**
|
|
33
|
+
* Render a single database's schema comparison as a git-style unified diff.
|
|
34
|
+
*
|
|
35
|
+
* Pure: given the two schemas it computes hunks with `diff`'s `structuredPatch`
|
|
36
|
+
* and lays them out like `git diff` — a bold header, red `---` / green `+++`
|
|
37
|
+
* branch lines, cyan `@@` hunk headers, and green/red line bodies. Returns
|
|
38
|
+
* `hasChanges: false` with empty text when the two schemas are identical.
|
|
39
|
+
*/
|
|
40
|
+
export const renderDatabaseSchemaDiff = (diff, opts) => {
|
|
41
|
+
const patch = structuredPatch(diff.database, diff.database, diff.before.sql, diff.after.sql, "", "", { context: 3 });
|
|
42
|
+
if (patch.hunks.length === 0) {
|
|
43
|
+
return { database: diff.database, hasChanges: false, text: "" };
|
|
44
|
+
}
|
|
45
|
+
const paint = palette(opts.color);
|
|
46
|
+
const lines = [
|
|
47
|
+
paint.header(`diff --neon database ${diff.database}`),
|
|
48
|
+
paint.removedFile(`--- ${branchLabel(diff.before)}`),
|
|
49
|
+
paint.addedFile(`+++ ${branchLabel(diff.after)}`),
|
|
50
|
+
];
|
|
51
|
+
for (const hunk of patch.hunks) {
|
|
52
|
+
lines.push(paint.hunk(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
|
|
53
|
+
for (const line of hunk.lines) {
|
|
54
|
+
// `structuredPatch` prefixes every line: '+' added, '-' removed,
|
|
55
|
+
// ' ' unchanged context, and '\' for the "No newline at end of file"
|
|
56
|
+
// marker. Color the first three; dim the marker; leave context plain.
|
|
57
|
+
if (line.startsWith("+")) {
|
|
58
|
+
lines.push(paint.added(line));
|
|
59
|
+
}
|
|
60
|
+
else if (line.startsWith("-")) {
|
|
61
|
+
lines.push(paint.removed(line));
|
|
62
|
+
}
|
|
63
|
+
else if (line.startsWith("\\")) {
|
|
64
|
+
lines.push(paint.noNewline(line));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
lines.push(line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
database: diff.database,
|
|
73
|
+
hasChanges: true,
|
|
74
|
+
text: lines.join("\n"),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Render every database's diff into one report, keeping only databases whose
|
|
79
|
+
* schema actually changed. `hasChanges` is false when nothing changed across
|
|
80
|
+
* all databases, letting the caller print a single "no differences" note.
|
|
81
|
+
*/
|
|
82
|
+
export const renderSchemaDiffReport = (diffs, opts) => {
|
|
83
|
+
const rendered = diffs.map((diff) => renderDatabaseSchemaDiff(diff, opts));
|
|
84
|
+
const changed = rendered.filter((r) => r.hasChanges);
|
|
85
|
+
return {
|
|
86
|
+
hasChanges: changed.length > 0,
|
|
87
|
+
text: changed.map((r) => r.text).join("\n\n"),
|
|
88
|
+
changedDatabases: changed.map((r) => r.database),
|
|
89
|
+
};
|
|
90
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neonctl",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"description": "CLI tool for Neon Serverless Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"which": "3.0.1",
|
|
52
52
|
"yaml": "^2.9.0",
|
|
53
53
|
"yargs": "17.7.2",
|
|
54
|
-
"@neon/sdk": "1.
|
|
55
|
-
"@neon/config": "0.9.
|
|
56
|
-
"@neon/config-runtime": "0.9.
|
|
57
|
-
"@neon/env": "0.11.
|
|
54
|
+
"@neon/sdk": "1.1.0",
|
|
55
|
+
"@neon/config": "0.9.2",
|
|
56
|
+
"@neon/config-runtime": "0.9.2",
|
|
57
|
+
"@neon/env": "0.11.1"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
60
|
"esbuild": "0.28.1"
|