neonctl 2.37.0 → 2.38.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.
Files changed (160) hide show
  1. package/{LICENSE.md → LICENSE} +27 -3
  2. package/README.md +15 -700
  3. package/bin/cli.js +3 -0
  4. package/package.json +8 -75
  5. package/dist/analytics.js +0 -168
  6. package/dist/api.js +0 -720
  7. package/dist/auth.js +0 -125
  8. package/dist/callback.html +0 -51
  9. package/dist/cli.js +0 -9
  10. package/dist/commands/api.js +0 -278
  11. package/dist/commands/auth.js +0 -214
  12. package/dist/commands/bootstrap.js +0 -481
  13. package/dist/commands/branches.js +0 -488
  14. package/dist/commands/bucket.js +0 -549
  15. package/dist/commands/checkout.js +0 -321
  16. package/dist/commands/config.js +0 -626
  17. package/dist/commands/connection_string.js +0 -172
  18. package/dist/commands/data_api.js +0 -284
  19. package/dist/commands/databases.js +0 -82
  20. package/dist/commands/deploy.js +0 -26
  21. package/dist/commands/dev.js +0 -698
  22. package/dist/commands/diff.js +0 -222
  23. package/dist/commands/env.js +0 -189
  24. package/dist/commands/functions.js +0 -373
  25. package/dist/commands/index.js +0 -62
  26. package/dist/commands/init.js +0 -73
  27. package/dist/commands/inspect.js +0 -65
  28. package/dist/commands/ip_allow.js +0 -137
  29. package/dist/commands/link.js +0 -1121
  30. package/dist/commands/neon_auth.js +0 -1028
  31. package/dist/commands/operations.js +0 -28
  32. package/dist/commands/orgs.js +0 -24
  33. package/dist/commands/projects.js +0 -413
  34. package/dist/commands/psql.js +0 -62
  35. package/dist/commands/roles.js +0 -65
  36. package/dist/commands/schema_diff.js +0 -151
  37. package/dist/commands/set_context.js +0 -29
  38. package/dist/commands/snapshots.js +0 -455
  39. package/dist/commands/status.js +0 -40
  40. package/dist/commands/user.js +0 -15
  41. package/dist/commands/vpc_endpoints.js +0 -134
  42. package/dist/config.js +0 -11
  43. package/dist/config_format.js +0 -72
  44. package/dist/context.js +0 -231
  45. package/dist/current_branch_fast_path.js +0 -55
  46. package/dist/dev/env.js +0 -244
  47. package/dist/dev/functions.js +0 -70
  48. package/dist/dev/inputs.js +0 -63
  49. package/dist/dev/runtime.js +0 -146
  50. package/dist/env.js +0 -36
  51. package/dist/env_file.js +0 -159
  52. package/dist/errors.js +0 -80
  53. package/dist/functions_api.js +0 -48
  54. package/dist/help.js +0 -146
  55. package/dist/index.js +0 -240
  56. package/dist/log.js +0 -18
  57. package/dist/parameters.gen.js +0 -480
  58. package/dist/pkg.js +0 -25
  59. package/dist/psql/cli.js +0 -53
  60. package/dist/psql/command/cmd_cond.js +0 -437
  61. package/dist/psql/command/cmd_connect.js +0 -820
  62. package/dist/psql/command/cmd_copy.js +0 -1035
  63. package/dist/psql/command/cmd_describe.js +0 -1815
  64. package/dist/psql/command/cmd_format.js +0 -948
  65. package/dist/psql/command/cmd_io.js +0 -2193
  66. package/dist/psql/command/cmd_lo.js +0 -393
  67. package/dist/psql/command/cmd_meta.js +0 -969
  68. package/dist/psql/command/cmd_misc.js +0 -187
  69. package/dist/psql/command/cmd_pipeline.js +0 -1148
  70. package/dist/psql/command/cmd_restrict.js +0 -171
  71. package/dist/psql/command/cmd_show.js +0 -766
  72. package/dist/psql/command/dispatch.js +0 -343
  73. package/dist/psql/command/inputQueue.js +0 -42
  74. package/dist/psql/command/shared.js +0 -71
  75. package/dist/psql/complete/filenames.js +0 -139
  76. package/dist/psql/complete/index.js +0 -104
  77. package/dist/psql/complete/matcher.js +0 -315
  78. package/dist/psql/complete/psqlVars.js +0 -249
  79. package/dist/psql/complete/queries.js +0 -493
  80. package/dist/psql/complete/rules.js +0 -2424
  81. package/dist/psql/core/common.js +0 -1253
  82. package/dist/psql/core/help.js +0 -576
  83. package/dist/psql/core/mainloop.js +0 -1360
  84. package/dist/psql/core/prompt.js +0 -439
  85. package/dist/psql/core/settings.js +0 -686
  86. package/dist/psql/core/sqlHelp.js +0 -1066
  87. package/dist/psql/core/startup.js +0 -846
  88. package/dist/psql/core/syncVars.js +0 -116
  89. package/dist/psql/core/variables.js +0 -287
  90. package/dist/psql/describe/formatters.js +0 -1330
  91. package/dist/psql/describe/processNamePattern.js +0 -270
  92. package/dist/psql/describe/queries.js +0 -2452
  93. package/dist/psql/describe/versionGate.js +0 -44
  94. package/dist/psql/index.js +0 -2030
  95. package/dist/psql/io/history.js +0 -299
  96. package/dist/psql/io/input.js +0 -120
  97. package/dist/psql/io/lineEditor/buffer.js +0 -325
  98. package/dist/psql/io/lineEditor/complete.js +0 -227
  99. package/dist/psql/io/lineEditor/filename.js +0 -159
  100. package/dist/psql/io/lineEditor/index.js +0 -893
  101. package/dist/psql/io/lineEditor/keymap.js +0 -745
  102. package/dist/psql/io/lineEditor/vt100.js +0 -363
  103. package/dist/psql/io/pgpass.js +0 -202
  104. package/dist/psql/io/pgservice.js +0 -194
  105. package/dist/psql/io/psqlrc.js +0 -422
  106. package/dist/psql/print/aligned.js +0 -1748
  107. package/dist/psql/print/asciidoc.js +0 -230
  108. package/dist/psql/print/crosstab.js +0 -463
  109. package/dist/psql/print/csv.js +0 -76
  110. package/dist/psql/print/html.js +0 -240
  111. package/dist/psql/print/json.js +0 -96
  112. package/dist/psql/print/latex.js +0 -379
  113. package/dist/psql/print/pager.js +0 -267
  114. package/dist/psql/print/troff.js +0 -240
  115. package/dist/psql/print/unaligned.js +0 -99
  116. package/dist/psql/print/units.js +0 -188
  117. package/dist/psql/scanner/slash.js +0 -515
  118. package/dist/psql/scanner/sql.js +0 -914
  119. package/dist/psql/scanner/stringutils.js +0 -394
  120. package/dist/psql/types/backslash.js +0 -1
  121. package/dist/psql/types/connection.js +0 -1
  122. package/dist/psql/types/index.js +0 -7
  123. package/dist/psql/types/printer.js +0 -1
  124. package/dist/psql/types/repl.js +0 -1
  125. package/dist/psql/types/scanner.js +0 -24
  126. package/dist/psql/types/settings.js +0 -1
  127. package/dist/psql/types/variables.js +0 -1
  128. package/dist/psql/wire/connection.js +0 -2857
  129. package/dist/psql/wire/copy.js +0 -108
  130. package/dist/psql/wire/notify.js +0 -59
  131. package/dist/psql/wire/pipeline.js +0 -521
  132. package/dist/psql/wire/protocol.js +0 -466
  133. package/dist/psql/wire/sasl.js +0 -294
  134. package/dist/psql/wire/tls.js +0 -602
  135. package/dist/storage_api.js +0 -147
  136. package/dist/test_utils/fixtures.js +0 -121
  137. package/dist/test_utils/oauth_server.js +0 -9
  138. package/dist/types.js +0 -1
  139. package/dist/utils/ai_gateway_notice.js +0 -180
  140. package/dist/utils/api_enums.js +0 -33
  141. package/dist/utils/auth.js +0 -5
  142. package/dist/utils/branch_notice.js +0 -22
  143. package/dist/utils/branch_picker.js +0 -103
  144. package/dist/utils/compute_units.js +0 -28
  145. package/dist/utils/config_diff.js +0 -185
  146. package/dist/utils/enrichers.js +0 -161
  147. package/dist/utils/esbuild.js +0 -158
  148. package/dist/utils/formats.js +0 -18
  149. package/dist/utils/git_diff.js +0 -90
  150. package/dist/utils/inspect_db.js +0 -153
  151. package/dist/utils/inspect_queries.js +0 -372
  152. package/dist/utils/middlewares.js +0 -20
  153. package/dist/utils/openapi.js +0 -114
  154. package/dist/utils/package_manager.js +0 -68
  155. package/dist/utils/point_in_time.js +0 -56
  156. package/dist/utils/psql.js +0 -120
  157. package/dist/utils/string.js +0 -5
  158. package/dist/utils/ui.js +0 -59
  159. package/dist/utils/zip.js +0 -4
  160. package/dist/writer.js +0 -97
@@ -1,134 +0,0 @@
1
- import { log } from "../log.js";
2
- import { fillSingleOrg, fillSingleProject } from "../utils/enrichers.js";
3
- import { writer } from "../writer.js";
4
- import { REGIONS } from "./projects.js";
5
- const VPC_ENDPOINT_FIELDS = ["vpc_endpoint_id", "label"];
6
- const VPC_ENDPOINT_DETAILS_FIELDS = [
7
- "vpc_endpoint_id",
8
- "label",
9
- "state",
10
- "num_restricted_projects",
11
- "example_restricted_projects",
12
- ];
13
- export const command = "vpc";
14
- export const describe = "Manage VPC endpoints and project VPC restrictions";
15
- export const builder = (argv) => {
16
- return argv
17
- .usage("$0 vpc <sub-command> [options]")
18
- .command("endpoint", "Manage VPC endpoints.\n" +
19
- "See: https://neon.tech/docs/guides/neon-private-networking\n" +
20
- "After adding an endpoint to an organization, client connections will be accepted\n" +
21
- "from the corresponding VPC for all projects in the organization, unless overridden\n" +
22
- "by a project-level VPC endpoint restriction.", (yargs) => {
23
- return yargs
24
- .options({
25
- "org-id": {
26
- describe: "Organization ID",
27
- type: "string",
28
- },
29
- "region-id": {
30
- describe: `The region ID. Possible values: ${REGIONS.join(", ")}`,
31
- type: "string",
32
- demandOption: true,
33
- },
34
- })
35
- .middleware(fillSingleOrg)
36
- .command("list", "List configured VPC endpoints for this organization.", (yargs) => yargs, async (args) => {
37
- await listOrg(args);
38
- })
39
- .command({
40
- command: "assign <id>",
41
- aliases: ["update <id>", "add <id>"],
42
- describe: "Add or update a VPC endpoint for this organization.\n" +
43
- "Note: Azure regions are not yet supported.",
44
- builder: (yargs) => yargs.options({
45
- label: {
46
- describe: "An optional descriptive label for the VPC endpoint",
47
- type: "string",
48
- },
49
- }),
50
- handler: async (args) => {
51
- await assignOrg(args);
52
- },
53
- })
54
- .command("remove <id>", "Remove a VPC endpoint from this organization.", (yargs) => yargs, async (args) => {
55
- await removeOrg(args);
56
- })
57
- .command("status <id>", "Get the status of a VPC endpoint for this organization.", (yargs) => yargs, async (args) => {
58
- await statusOrg(args);
59
- });
60
- })
61
- .command("project", "Manage project-level VPC endpoint restrictions.\n" +
62
- "By default, connections are accepted from any VPC configured at the organization level.\n" +
63
- "A project-level VPC endpoint restriction can be used to restrict connections to a specific VPC.", (yargs) => {
64
- return yargs
65
- .options({
66
- "project-id": {
67
- describe: "Project ID",
68
- type: "string",
69
- },
70
- })
71
- .middleware(fillSingleProject)
72
- .command("list", "List VPC endpoint restrictions for this project.", (yargs) => yargs, async (args) => {
73
- await listProject(args);
74
- })
75
- .command({
76
- command: "restrict <id>",
77
- aliases: ["update <id>"],
78
- describe: "Configure or update a VPC endpoint restriction for this project.",
79
- builder: (yargs) => yargs.options({
80
- label: {
81
- describe: "An optional descriptive label for the VPC endpoint restriction",
82
- type: "string",
83
- },
84
- }),
85
- handler: async (args) => {
86
- await assignProject(args);
87
- },
88
- })
89
- .command("remove <id>", "Remove a VPC endpoint restriction from this project.", (yargs) => yargs, async (args) => {
90
- await removeProject(args);
91
- });
92
- });
93
- };
94
- const listOrg = async (props) => {
95
- const { data } = await props.apiClient.listOrganizationVpcEndpoints(props.orgId, props.regionId);
96
- writer(props).end(data.endpoints, {
97
- fields: VPC_ENDPOINT_FIELDS,
98
- });
99
- };
100
- const assignOrg = async (props) => {
101
- const vpcEndpointAssignment = {
102
- label: props.label || "",
103
- };
104
- const { data } = await props.apiClient.assignOrganizationVpcEndpoint(props.orgId, props.regionId, props.id, vpcEndpointAssignment);
105
- writer(props).end(data, { fields: [] });
106
- if (props.regionId.startsWith("azure")) {
107
- log.info("VPC endpoint configuration is not supported for Azure regions");
108
- }
109
- };
110
- const removeOrg = async (props) => {
111
- const { data } = await props.apiClient.deleteOrganizationVpcEndpoint(props.orgId, props.regionId, props.id);
112
- writer(props).end(data, { fields: [] });
113
- };
114
- const statusOrg = async (props) => {
115
- const { data } = await props.apiClient.getOrganizationVpcEndpointDetails(props.orgId, props.regionId, props.id);
116
- writer(props).end(data, { fields: VPC_ENDPOINT_DETAILS_FIELDS });
117
- };
118
- const listProject = async (props) => {
119
- const { data } = await props.apiClient.listProjectVpcEndpoints(props.projectId);
120
- writer(props).end(data.endpoints, {
121
- fields: VPC_ENDPOINT_FIELDS,
122
- });
123
- };
124
- const assignProject = async (props) => {
125
- const vpcEndpointAssignment = {
126
- label: props.label || "",
127
- };
128
- const { data } = await props.apiClient.assignProjectVpcEndpoint(props.projectId, props.id, vpcEndpointAssignment);
129
- writer(props).end(data, { fields: [] });
130
- };
131
- const removeProject = async (props) => {
132
- const { data } = await props.apiClient.deleteProjectVpcEndpoint(props.projectId, props.id);
133
- writer(props).end(data, { fields: [] });
134
- };
package/dist/config.js DELETED
@@ -1,11 +0,0 @@
1
- import { existsSync, mkdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { isCi } from "./env.js";
5
- export const CREDENTIALS_FILE = "credentials.json";
6
- export const defaultDir = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "neonctl");
7
- export const ensureConfigDir = ({ "config-dir": configDir, "force-auth": forceAuth, }) => {
8
- if (!existsSync(configDir) && (!isCi() || forceAuth)) {
9
- mkdirSync(configDir, { recursive: true });
10
- }
11
- };
@@ -1,72 +0,0 @@
1
- /**
2
- * Render a TTL in whole seconds back to the canonical `neon.ts` duration string (e.g.
3
- * `604800` -> `"7d"`), falling back to seconds when no clean unit boundary matches. Mirrors
4
- * the formatter `@neon/config` uses when it emits a TTL, so `config status` shows
5
- * the same value a user would write in `neon.ts`.
6
- */
7
- export const formatDurationSeconds = (totalSeconds) => {
8
- const units = [
9
- ["w", 7 * 24 * 60 * 60],
10
- ["d", 24 * 60 * 60],
11
- ["h", 60 * 60],
12
- ["m", 60],
13
- ];
14
- for (const [unit, perUnit] of units) {
15
- if (totalSeconds % perUnit === 0)
16
- return `${totalSeconds / perUnit}${unit}`;
17
- }
18
- return `${totalSeconds}s`;
19
- };
20
- /**
21
- * Project a resolved branch config (plus the separately-pulled preview state, since
22
- * functions/buckets don't live on the closure) into a {@link NeonConfigView}.
23
- *
24
- * - Service toggles are surfaced as `true` only when enabled (disabled is the absence of
25
- * the key, exactly as in `neon.ts`).
26
- * - `ttlSeconds` is rendered back to a duration string (`7d`).
27
- * - The `branch` section is the JSON-able part of what would otherwise be the `branch`
28
- * closure: `parent` / `ttl` / `protected` / `postgres.computeSettings`.
29
- * - `branch` and `preview` are omitted entirely when they would be empty.
30
- */
31
- export const toNeonConfigView = (resolved, preview) => {
32
- const view = {};
33
- if (resolved.authEnabled)
34
- view.auth = true;
35
- if (resolved.dataApiEnabled)
36
- view.dataApi = true;
37
- const previewView = toPreviewView(preview);
38
- if (previewView)
39
- view.preview = previewView;
40
- const branch = {};
41
- if (resolved.parent !== undefined)
42
- branch.parent = resolved.parent;
43
- if (resolved.ttlSeconds !== undefined)
44
- branch.ttl = formatDurationSeconds(resolved.ttlSeconds);
45
- if (resolved.protected !== undefined)
46
- branch.protected = resolved.protected;
47
- if (resolved.postgres?.computeSettings)
48
- branch.postgres = resolved.postgres;
49
- if (Object.keys(branch).length > 0)
50
- view.branch = branch;
51
- return view;
52
- };
53
- const toPreviewView = (preview) => {
54
- if (!preview)
55
- return undefined;
56
- const out = {};
57
- if (preview.functions.length > 0) {
58
- out.functions = Object.fromEntries(preview.functions.map((fn) => [fn.slug, { name: fn.name }]));
59
- }
60
- if (preview.buckets.length > 0) {
61
- out.buckets = Object.fromEntries(preview.buckets.map((b) => [b.name, { access: b.access }]));
62
- }
63
- if (preview.credentials.length > 0) {
64
- out.credentials = preview.credentials.map((c) => ({
65
- id: c.tokenIdShort,
66
- ...(c.name ? { name: c.name } : {}),
67
- scopes: c.scopes,
68
- ...(c.lastUsedAt ? { lastUsedAt: c.lastUsedAt } : {}),
69
- }));
70
- }
71
- return Object.keys(out).length > 0 ? out : undefined;
72
- };
package/dist/context.js DELETED
@@ -1,231 +0,0 @@
1
- import { accessSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, normalize, resolve } from "node:path";
4
- import { log } from "./log.js";
5
- /**
6
- * The branch pinned in a context, reading the current `branch` field and
7
- * falling back to the legacy `branchId` so pre-migration `.neon` files keep
8
- * working.
9
- */
10
- export const contextBranch = (context) => context.branch ?? context.branchId;
11
- /**
12
- * True when the invocation is the offline "current branch" probe:
13
- * `(config) status --current-branch`. This mode only reads the pinned branch
14
- * from the local `.neon` file (for shell prompts like starship), so it MUST
15
- * NOT touch the network — several middlewares (auth, analytics, single-project
16
- * resolution) consult this to early-return and skip their API calls / login.
17
- *
18
- * Gated on the exact command as well as the flag so an accidental
19
- * `--current-branch` on an unrelated command (e.g. `config plan`, where the flag
20
- * is undefined but non-strict yargs still parses it) can't silently skip
21
- * auth/analytics. The probe is only `status` (the top-level alias) or
22
- * `config status` (`_ = ['config', 'status']`).
23
- */
24
- export const isCurrentBranchProbe = (args) => args.currentBranch === true &&
25
- (args._[0] === "status" ||
26
- (args._[0] === "config" && args._[1] === "status"));
27
- /**
28
- * `config init` only scaffolds a local `neon.ts` and installs npm packages — it
29
- * never calls the Neon API. Gated on the exact command path so the global auth
30
- * middleware and the single-project resolver can skip it (it runs with no API
31
- * client), mirroring {@link isCurrentBranchProbe}.
32
- */
33
- export const isConfigInit = (args) => args._[0] === "config" && args._[1] === "init";
34
- const CONTEXT_FILE = ".neon";
35
- const GITIGNORE_FILE = ".gitignore";
36
- const canAccessFile = (file) => {
37
- try {
38
- accessSync(file);
39
- return true;
40
- }
41
- catch {
42
- return false;
43
- }
44
- };
45
- /**
46
- * Walk upward to find an existing `.neon` file.
47
- *
48
- * The walk keeps the established home and POSIX-root boundaries, then also
49
- * stops when resolving a parent makes no progress. That second guard handles
50
- * Windows drive and UNC-share roots, whose paths do not equal `normalize("/")`.
51
- */
52
- export const walkContextFile = (cwd, root, home, resolvePath, canAccess) => {
53
- let currentDir = cwd;
54
- while (currentDir !== root && currentDir !== home) {
55
- const contextFile = resolvePath(currentDir, CONTEXT_FILE);
56
- if (canAccess(contextFile)) {
57
- return contextFile;
58
- }
59
- const parentDir = resolvePath(currentDir, "..");
60
- if (parentDir === currentDir) {
61
- break;
62
- }
63
- currentDir = parentDir;
64
- }
65
- return resolvePath(cwd, CONTEXT_FILE);
66
- };
67
- /**
68
- * Resolve the default `.neon` path for the current working directory.
69
- *
70
- * Walks UP from `cwd` looking ONLY for an already-existing `.neon` file so
71
- * commands run from a sub-directory of a linked project still pick up the
72
- * project's context. If no `.neon` is found, the path defaults to
73
- * `<cwd>/.neon`, which makes `neonctl link` and `neonctl set-context`
74
- * predictable: they always write the context file into the directory they
75
- * were invoked from.
76
- *
77
- * Historically the walk also considered `package.json` and `.git` as project
78
- * markers, but that led to surprising behaviour when running `link` from a
79
- * fresh sub-directory inside an unrelated repo (the new link would land in
80
- * the parent repo's root instead of the cwd).
81
- *
82
- * `cwd` is overridable so tests can exercise the walk-up without mutating
83
- * `process.cwd()` (which would race with other tests running in parallel).
84
- */
85
- export const currentContextFile = (cwd = process.cwd()) => walkContextFile(cwd, normalize("/"), homedir(), resolve, canAccessFile);
86
- export const readContextFile = (file) => {
87
- try {
88
- return JSON.parse(readFileSync(file, "utf-8"));
89
- }
90
- catch {
91
- return {};
92
- }
93
- };
94
- export const enrichFromContext = (args) => {
95
- // `link` and the deprecated `set-context` manage the context file themselves
96
- // and must see the raw flags rather than values pre-filled from an existing
97
- // `.neon`, so skip enrichment for both.
98
- if (args._[0] === "link" || args._[0] === "set-context") {
99
- return;
100
- }
101
- const context = readContextFile(args.contextFile);
102
- if (!args.orgId) {
103
- args.orgId = context.orgId;
104
- }
105
- if (!args.projectId) {
106
- args.projectId = context.projectId;
107
- }
108
- if (!args.branch &&
109
- !args.id &&
110
- !args.name &&
111
- context.projectId === args.projectId) {
112
- args.branch = contextBranch(context);
113
- }
114
- };
115
- export const updateContextFile = (file, context) => {
116
- writeFileSync(file, JSON.stringify(context, null, 2));
117
- };
118
- /**
119
- * Shared primitive used by `link`, the deprecated `set-context`, and `checkout`
120
- * to persist context. Mirrors the destructive write semantics of
121
- * `updateContextFile` — any field not present in `context` is dropped from the
122
- * file.
123
- *
124
- * `.gitignore` scaffolding only happens when the context file is being
125
- * *created* (it didn't exist before this write). On updates to an existing
126
- * `.neon` we never touch `.gitignore`, so a user who deliberately un-ignored
127
- * the file (e.g. to commit shared context) won't have the entry re-added on
128
- * every subsequent command.
129
- */
130
- export const applyContext = (file, context) => {
131
- const isNewFile = !existsSync(file);
132
- updateContextFile(file, context);
133
- if (isNewFile) {
134
- ensureGitignored(file);
135
- }
136
- };
137
- /**
138
- * Low-level writer for callers that already hold the resolved identifiers and
139
- * just need to record them — e.g. `init` or `projects create`, which create a
140
- * project and want to link it without the resolution, verification, prompting,
141
- * or env-pull that `link` performs.
142
- *
143
- * Unlike the loose {@link applyContext}, this enforces at the type level that
144
- * `orgId` and `projectId` are present, so the `.neon` file never ends up with a
145
- * dangling project that has no org. The branch stays optional. It writes through
146
- * {@link applyContext}, so the same `.gitignore` scaffolding applies.
147
- */
148
- export const setContext = (file, context) => {
149
- applyContext(file, {
150
- orgId: context.orgId,
151
- projectId: context.projectId,
152
- branch: context.branch,
153
- });
154
- };
155
- /**
156
- * Make sure the `.gitignore` next to `file` covers the file's basename — used for the `.neon`
157
- * context file and for a `.env` we create (both carry credentials that must not be committed).
158
- * Creates the `.gitignore` if it doesn't exist, otherwise appends the entry only when nothing
159
- * there already covers it: an exact line, or a basename glob such as `.env*` / `*.local`
160
- * (see {@link gitignoreCovers}), so a repo that already ignores env files doesn't collect a
161
- * redundant line per pull.
162
- *
163
- * Best-effort: a failure here (e.g. read-only filesystem) is logged at debug
164
- * level and swallowed; persisting the context file is the primary goal and
165
- * must not be blocked by a `.gitignore` write error.
166
- */
167
- export const ensureGitignored = (file) => {
168
- try {
169
- const dir = dirname(file);
170
- const entry = basenameOf(file);
171
- const gitignorePath = resolve(dir, GITIGNORE_FILE);
172
- if (!existsSync(gitignorePath)) {
173
- writeFileSync(gitignorePath, `${entry}\n`);
174
- return;
175
- }
176
- const current = readFileSync(gitignorePath, "utf-8");
177
- if (hasGitignoreEntry(current, entry)) {
178
- return;
179
- }
180
- const needsLeadingNewline = current.length > 0 && !current.endsWith("\n");
181
- const addition = `${needsLeadingNewline ? "\n" : ""}${entry}\n`;
182
- writeFileSync(gitignorePath, current + addition);
183
- }
184
- catch (err) {
185
- const message = err instanceof Error ? err.message : String(err);
186
- log.debug("Failed to update .gitignore next to %s: %s", file, message);
187
- }
188
- };
189
- const basenameOf = (file) => {
190
- const parts = file.split(/[\\/]/);
191
- return parts[parts.length - 1] || CONTEXT_FILE;
192
- };
193
- const hasGitignoreEntry = (content, entry) => {
194
- return content
195
- .split(/\r?\n/)
196
- .some((line) => gitignoreCovers(line.trim(), entry));
197
- };
198
- /**
199
- * Whether a single `.gitignore` line already ignores `entry` (a bare basename like `.neon` or
200
- * `.env.local`).
201
- *
202
- * Deliberately narrow: an exact match, or a glob **without a path separator** — the
203
- * `.env*` / `*.local` / `.env.?` shapes that repos actually use for env files — matched
204
- * against the whole basename. Everything else returns false, which at worst appends an entry
205
- * git already covers (harmless) rather than skipping one it doesn't (a committed credential).
206
- * That's why path-scoped patterns (`config/.env`), negations (`!.env.local`), character
207
- * classes, and comments are all treated as "does not cover".
208
- */
209
- const gitignoreCovers = (line, entry) => {
210
- if (line === "" || line.startsWith("#") || line.startsWith("!")) {
211
- return false;
212
- }
213
- // A trailing slash marks a directory-only pattern; the leading one anchors to the
214
- // .gitignore's own directory, which is exactly where `entry` lives.
215
- const pattern = line.replace(/\/$/, "").replace(/^\//, "");
216
- if (pattern === entry)
217
- return true;
218
- if (pattern.includes("/") || pattern.includes("["))
219
- return false;
220
- if (!pattern.includes("*") && !pattern.includes("?"))
221
- return false;
222
- return globToRegExp(pattern).test(entry);
223
- };
224
- /** Compile a separator-free `.gitignore` glob (`*` / `?` only) into an anchored RegExp. */
225
- const globToRegExp = (pattern) => {
226
- const source = pattern
227
- .replace(/[.+^${}()|\\]/g, "\\$&")
228
- .replace(/\*/g, "[^/]*")
229
- .replace(/\?/g, "[^/]");
230
- return new RegExp(`^${source}$`);
231
- };
@@ -1,55 +0,0 @@
1
- import { contextBranch, currentContextFile, readContextFile, } from "./context.js";
2
- import { log } from "./log.js";
3
- /**
4
- * Offline fast path for `(config) status --current-branch` (used by shell prompts).
5
- *
6
- * Reading the pinned branch out of the local `.neon` file does not need the CLI's
7
- * full command tree, `@neondatabase/api-client`, or yargs — importing those is ~200ms,
8
- * which dwarfs the actual work. So the entry point ({@link file://./cli.ts}) calls this
9
- * BEFORE importing `index.js`, and only falls through to the full CLI when this returns
10
- * `false`. On the fast path the process loads only this module + `context.js`/`log.js`
11
- * (~25ms total incl. Node startup) instead of ~230ms.
12
- *
13
- * It mirrors the `--current-branch` short-circuit in `status()` (commands/config.ts):
14
- * print the pinned branch to stdout and exit 0, or print nothing + a `neonctl checkout`
15
- * hint on stderr and exit non-zero when no branch is pinned.
16
- *
17
- * Deliberately conservative: only the EXACT bare invocation is handled —
18
- * `status --current-branch` or `config status --current-branch` with no other args.
19
- * Anything else (extra flags like `--context-file`/`--output`, more args, etc.) returns
20
- * `false` and flows through the normal yargs pipeline, so behavior can never diverge —
21
- * the worst case is "not faster", never "wrong".
22
- *
23
- * @returns `true` if it handled the invocation (caller should NOT load the full CLI).
24
- */
25
- export const tryCurrentBranchFastPath = (argv,
26
- // `cwd` is overridable so tests can exercise the `.neon` walk-up without mutating
27
- // `process.cwd()` (which isn't allowed in vitest workers), mirroring currentContextFile.
28
- cwd = process.cwd()) => {
29
- // argv is [execPath, scriptPath, ...userArgs].
30
- if (!isExactCurrentBranchInvocation(argv.slice(2))) {
31
- return false;
32
- }
33
- const branch = contextBranch(readContextFile(currentContextFile(cwd)));
34
- if (branch) {
35
- process.stdout.write(`${branch}\n`);
36
- }
37
- else {
38
- log.info("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
39
- process.exitCode = 1;
40
- }
41
- return true;
42
- };
43
- /**
44
- * True only for `status --current-branch` or `config status --current-branch` with no
45
- * other arguments. Any extra token (another flag, `--context-file`, `=`-style flags,
46
- * positional args) makes this false so the full CLI handles it.
47
- */
48
- const isExactCurrentBranchInvocation = (args) => {
49
- const rest = args[0] === "status"
50
- ? args.slice(1)
51
- : args[0] === "config" && args[1] === "status"
52
- ? args.slice(2)
53
- : null;
54
- return rest !== null && rest.length === 1 && rest[0] === "--current-branch";
55
- };