neon 2.1.2 → 2.29.1

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 (166) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +601 -0
  3. package/dist/analytics.js +156 -0
  4. package/dist/api.js +665 -0
  5. package/dist/auth.js +127 -0
  6. package/dist/callback.html +51 -0
  7. package/dist/cli.js +9 -0
  8. package/dist/commands/auth.js +214 -0
  9. package/dist/commands/bootstrap.js +481 -0
  10. package/dist/commands/branches.js +481 -0
  11. package/dist/commands/bucket.js +543 -0
  12. package/dist/commands/checkout.js +289 -0
  13. package/dist/commands/config.js +544 -0
  14. package/dist/commands/connection_string.js +172 -0
  15. package/dist/commands/data_api.js +285 -0
  16. package/dist/commands/databases.js +82 -0
  17. package/dist/commands/deploy.js +26 -0
  18. package/dist/commands/dev.js +698 -0
  19. package/dist/commands/env.js +166 -0
  20. package/dist/commands/functions.js +373 -0
  21. package/dist/commands/index.js +54 -0
  22. package/dist/commands/init.js +73 -0
  23. package/dist/commands/ip_allow.js +137 -0
  24. package/dist/commands/link.js +1121 -0
  25. package/dist/commands/neon_auth.js +1028 -0
  26. package/dist/commands/operations.js +28 -0
  27. package/dist/commands/orgs.js +24 -0
  28. package/dist/commands/projects.js +372 -0
  29. package/dist/commands/psql.js +62 -0
  30. package/dist/commands/roles.js +65 -0
  31. package/dist/commands/schema_diff.js +151 -0
  32. package/dist/commands/set_context.js +29 -0
  33. package/dist/commands/status.js +40 -0
  34. package/dist/commands/user.js +15 -0
  35. package/dist/commands/vpc_endpoints.js +134 -0
  36. package/dist/config.js +11 -0
  37. package/dist/config_format.js +72 -0
  38. package/dist/context.js +177 -0
  39. package/dist/current_branch_fast_path.js +55 -0
  40. package/dist/dev/env.js +240 -0
  41. package/dist/dev/functions.js +70 -0
  42. package/dist/dev/inputs.js +63 -0
  43. package/dist/dev/runtime.js +146 -0
  44. package/dist/env.js +36 -0
  45. package/dist/env_file.js +159 -0
  46. package/dist/errors.js +80 -0
  47. package/dist/functions_api.js +44 -0
  48. package/dist/help.js +146 -0
  49. package/dist/index.js +234 -0
  50. package/dist/log.js +18 -0
  51. package/dist/parameters.gen.js +480 -0
  52. package/dist/pkg.js +25 -0
  53. package/dist/psql/cli.js +53 -0
  54. package/dist/psql/command/cmd_cond.js +437 -0
  55. package/dist/psql/command/cmd_connect.js +820 -0
  56. package/dist/psql/command/cmd_copy.js +1035 -0
  57. package/dist/psql/command/cmd_describe.js +1815 -0
  58. package/dist/psql/command/cmd_format.js +922 -0
  59. package/dist/psql/command/cmd_io.js +2193 -0
  60. package/dist/psql/command/cmd_lo.js +393 -0
  61. package/dist/psql/command/cmd_meta.js +970 -0
  62. package/dist/psql/command/cmd_misc.js +187 -0
  63. package/dist/psql/command/cmd_pipeline.js +1148 -0
  64. package/dist/psql/command/cmd_restrict.js +171 -0
  65. package/dist/psql/command/cmd_show.js +766 -0
  66. package/dist/psql/command/dispatch.js +343 -0
  67. package/dist/psql/command/inputQueue.js +42 -0
  68. package/dist/psql/command/shared.js +71 -0
  69. package/dist/psql/complete/filenames.js +139 -0
  70. package/dist/psql/complete/index.js +104 -0
  71. package/dist/psql/complete/matcher.js +315 -0
  72. package/dist/psql/complete/psqlVars.js +247 -0
  73. package/dist/psql/complete/queries.js +493 -0
  74. package/dist/psql/complete/rules.js +2424 -0
  75. package/dist/psql/core/common.js +1253 -0
  76. package/dist/psql/core/help.js +576 -0
  77. package/dist/psql/core/mainloop.js +1360 -0
  78. package/dist/psql/core/prompt.js +440 -0
  79. package/dist/psql/core/settings.js +684 -0
  80. package/dist/psql/core/sqlHelp.js +1066 -0
  81. package/dist/psql/core/startup.js +846 -0
  82. package/dist/psql/core/syncVars.js +116 -0
  83. package/dist/psql/core/variables.js +287 -0
  84. package/dist/psql/describe/formatters.js +1290 -0
  85. package/dist/psql/describe/processNamePattern.js +270 -0
  86. package/dist/psql/describe/queries.js +2378 -0
  87. package/dist/psql/describe/versionGate.js +43 -0
  88. package/dist/psql/index.js +2030 -0
  89. package/dist/psql/io/history.js +299 -0
  90. package/dist/psql/io/input.js +120 -0
  91. package/dist/psql/io/lineEditor/buffer.js +325 -0
  92. package/dist/psql/io/lineEditor/complete.js +227 -0
  93. package/dist/psql/io/lineEditor/filename.js +159 -0
  94. package/dist/psql/io/lineEditor/index.js +894 -0
  95. package/dist/psql/io/lineEditor/keymap.js +745 -0
  96. package/dist/psql/io/lineEditor/vt100.js +363 -0
  97. package/dist/psql/io/pgpass.js +202 -0
  98. package/dist/psql/io/pgservice.js +194 -0
  99. package/dist/psql/io/psqlrc.js +422 -0
  100. package/dist/psql/print/aligned.js +1765 -0
  101. package/dist/psql/print/asciidoc.js +248 -0
  102. package/dist/psql/print/crosstab.js +463 -0
  103. package/dist/psql/print/csv.js +95 -0
  104. package/dist/psql/print/html.js +258 -0
  105. package/dist/psql/print/json.js +96 -0
  106. package/dist/psql/print/latex.js +396 -0
  107. package/dist/psql/print/pager.js +267 -0
  108. package/dist/psql/print/troff.js +258 -0
  109. package/dist/psql/print/unaligned.js +119 -0
  110. package/dist/psql/print/units.js +135 -0
  111. package/dist/psql/scanner/slash.js +515 -0
  112. package/dist/psql/scanner/sql.js +914 -0
  113. package/dist/psql/scanner/stringutils.js +394 -0
  114. package/dist/psql/types/backslash.js +1 -0
  115. package/dist/psql/types/connection.js +1 -0
  116. package/dist/psql/types/index.js +7 -0
  117. package/dist/psql/types/printer.js +1 -0
  118. package/dist/psql/types/repl.js +1 -0
  119. package/dist/psql/types/scanner.js +24 -0
  120. package/dist/psql/types/settings.js +1 -0
  121. package/dist/psql/types/variables.js +1 -0
  122. package/dist/psql/wire/connection.js +2858 -0
  123. package/dist/psql/wire/copy.js +108 -0
  124. package/dist/psql/wire/notify.js +59 -0
  125. package/dist/psql/wire/pipeline.js +521 -0
  126. package/dist/psql/wire/protocol.js +466 -0
  127. package/dist/psql/wire/sasl.js +296 -0
  128. package/dist/psql/wire/tls.js +602 -0
  129. package/dist/storage_api.js +147 -0
  130. package/dist/test_utils/fixtures.js +122 -0
  131. package/dist/test_utils/oauth_server.js +9 -0
  132. package/dist/types.js +1 -0
  133. package/dist/utils/api_enums.js +33 -0
  134. package/dist/utils/auth.js +5 -0
  135. package/dist/utils/branch_notice.js +22 -0
  136. package/dist/utils/branch_picker.js +103 -0
  137. package/dist/utils/compute_units.js +28 -0
  138. package/dist/utils/enrichers.js +161 -0
  139. package/dist/utils/esbuild.js +158 -0
  140. package/dist/utils/formats.js +18 -0
  141. package/dist/utils/middlewares.js +20 -0
  142. package/dist/utils/package_manager.js +68 -0
  143. package/dist/utils/point_in_time.js +56 -0
  144. package/dist/utils/psql.js +120 -0
  145. package/dist/utils/string.js +5 -0
  146. package/dist/utils/ui.js +59 -0
  147. package/dist/utils/zip.js +4 -0
  148. package/dist/writer.js +97 -0
  149. package/package.json +117 -14
  150. package/.jshintrc +0 -3
  151. package/CHANGELOG.markdown +0 -22
  152. package/LICENSE +0 -21
  153. package/README.markdown +0 -76
  154. package/bower.json +0 -23
  155. package/license.txt +0 -9
  156. package/neon.js +0 -234
  157. package/stdlib/bubbling_support.js +0 -32
  158. package/stdlib/custom_event.js +0 -54
  159. package/stdlib/custom_event_support.js +0 -195
  160. package/stdlib/index.js +0 -7
  161. package/stdlib/node_support.js +0 -123
  162. package/stdlib/widget.js +0 -340
  163. package/test/neon_browser.html +0 -11
  164. package/test/neon_stdlib_browser.html +0 -15
  165. package/test/neon_stdlib_test.js +0 -84
  166. package/test/neon_test.js +0 -64
@@ -0,0 +1,151 @@
1
+ import chalk from "chalk";
2
+ import { createPatch } from "diff";
3
+ import { sendError } from "../analytics.js";
4
+ import { isNeonApiError, messageFromBody } from "../api.js";
5
+ import { log } from "../log.js";
6
+ import { branchIdFromProps } from "../utils/enrichers.js";
7
+ import { parsePointInTime, } from "../utils/point_in_time.js";
8
+ import { writer } from "../writer.js";
9
+ const COLORS = {
10
+ added: chalk.green,
11
+ removed: chalk.red,
12
+ header: chalk.yellow,
13
+ section: chalk.magenta,
14
+ };
15
+ export const schemaDiff = async (props) => {
16
+ props.branch = props.baseBranch || props.branch;
17
+ const baseBranch = await branchIdFromProps(props);
18
+ let pointInTime = await parsePointInTime({
19
+ pointInTime: props.compareSource,
20
+ targetBranchId: baseBranch,
21
+ projectId: props.projectId,
22
+ api: props.apiClient,
23
+ });
24
+ // Swap base and compare points if comparing with parent branch
25
+ const comparingWithParent = props.compareSource.startsWith("^parent");
26
+ let baseBranchPoint = {
27
+ branchId: baseBranch,
28
+ tag: "head",
29
+ };
30
+ [baseBranchPoint, pointInTime] = comparingWithParent
31
+ ? [pointInTime, baseBranchPoint]
32
+ : [baseBranchPoint, pointInTime];
33
+ const baseDatabases = await fetchDatabases(baseBranch, props);
34
+ if (props.database) {
35
+ const database = baseDatabases.find((db) => db.name === props.database);
36
+ if (!database) {
37
+ throw new Error(`Database ${props.database} does not exist in base branch ${baseBranch}`);
38
+ }
39
+ const patch = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
40
+ writer(props).text(colorize(patch));
41
+ return;
42
+ }
43
+ await Promise.all(baseDatabases.map(async (database) => {
44
+ const patch = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
45
+ writer(props).text(colorize(patch));
46
+ }));
47
+ };
48
+ const fetchDatabases = async (branch, props) => {
49
+ return props.apiClient
50
+ .listProjectBranchDatabases(props.projectId, branch)
51
+ .then((response) => response.data.databases);
52
+ };
53
+ const createSchemaDiff = async (baseBranch, pointInTime, database, props) => {
54
+ const [baseSchema, compareSchema] = await Promise.all([
55
+ fetchSchema(baseBranch, database, props),
56
+ fetchSchema(pointInTime, database, props),
57
+ ]);
58
+ return createPatch(`Database: ${database.name}`, baseSchema, compareSchema, generateHeader(baseBranch), generateHeader(pointInTime));
59
+ };
60
+ const fetchSchema = async (pointInTime, database, props) => {
61
+ try {
62
+ const response = await props.apiClient.getProjectBranchSchema({
63
+ projectId: props.projectId,
64
+ branchId: pointInTime.branchId,
65
+ db_name: database.name,
66
+ ...pointInTimeParams(pointInTime),
67
+ });
68
+ return response.data.sql ?? "";
69
+ }
70
+ catch (error) {
71
+ if (isNeonApiError(error)) {
72
+ sendError(error, "API_ERROR");
73
+ throw new Error(messageFromBody(error.data) ??
74
+ `Error while fetching schema for branch ${pointInTime.branchId}`);
75
+ }
76
+ throw error;
77
+ }
78
+ };
79
+ const colorize = (patch) => {
80
+ return patch
81
+ .replace(/^([^\n]+)\n([^\n]+)\n/m, "") // Remove first two lines
82
+ .replace(/^-.*/gm, colorizer("removed"))
83
+ .replace(/^\+.*/gm, colorizer("added"))
84
+ .replace(/^@@.+@@.*/gm, colorizer("section"));
85
+ };
86
+ const colorizer = (colorId) => {
87
+ const color = COLORS[colorId];
88
+ return (line) => color(line);
89
+ };
90
+ const pointInTimeParams = (pointInTime) => {
91
+ switch (pointInTime.tag) {
92
+ case "timestamp":
93
+ return {
94
+ timestamp: pointInTime.timestamp,
95
+ };
96
+ case "lsn":
97
+ return {
98
+ lsn: pointInTime.lsn ?? undefined,
99
+ };
100
+ default:
101
+ return {};
102
+ }
103
+ };
104
+ const generateHeader = (pointInTime) => {
105
+ const header = `(Branch: ${pointInTime.branchId}`;
106
+ switch (pointInTime.tag) {
107
+ case "timestamp":
108
+ return `${header} at ${pointInTime.timestamp})`;
109
+ case "lsn":
110
+ return `${header} at ${pointInTime.lsn})`;
111
+ default:
112
+ return `${header})`;
113
+ }
114
+ };
115
+ /*
116
+ The command has two positional optional arguments - [base-branch] and [compare-source]
117
+ If only one argument is specified, we should consider it as `compare-source`
118
+ and `base-branch` will be either read from context or the default branch of project.
119
+ If no branches are specified, compare the context branch with its parent
120
+ */
121
+ export const parseSchemaDiffParams = async (props) => {
122
+ if (!props.compareSource) {
123
+ if (props.baseBranch) {
124
+ props.compareSource = props.baseBranch;
125
+ props.baseBranch = props.branch;
126
+ }
127
+ else if (props.branch) {
128
+ const { data } = await props.apiClient.listProjectBranches({
129
+ projectId: props.projectId,
130
+ });
131
+ const contextBranch = data.branches.find((b) => b.id === props.branch || b.name === props.branch);
132
+ if (contextBranch?.parent_id == undefined) {
133
+ throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
134
+ }
135
+ log.info(`No branches specified. Comparing your context branch '${props.branch}' with its parent`);
136
+ props.compareSource = "^parent";
137
+ }
138
+ else {
139
+ const { data } = await props.apiClient.listProjectBranches({
140
+ projectId: props.projectId,
141
+ });
142
+ const defaultBranch = data.branches.find((b) => b.default);
143
+ if (defaultBranch?.parent_id == undefined) {
144
+ throw new Error("No branch specified. Include a base branch or add a set-context branch to continue. Your default branch has no parent, so no comparison is possible.");
145
+ }
146
+ log.info(`No branches specified. Comparing default branch with its parent`);
147
+ props.compareSource = "^parent";
148
+ }
149
+ }
150
+ return props;
151
+ };
@@ -0,0 +1,29 @@
1
+ import { applyContext } from "../context.js";
2
+ import { log } from "../log.js";
3
+ export const command = "set-context";
4
+ export const describe = "Deprecated: use `neonctl link`. Set the .neon context (raw write).";
5
+ export const builder = (argv) => argv.usage("$0 set-context [options]").options({
6
+ "project-id": {
7
+ describe: "Project ID",
8
+ type: "string",
9
+ },
10
+ "org-id": {
11
+ describe: "Organization ID",
12
+ type: "string",
13
+ },
14
+ "branch-id": {
15
+ describe: "Branch ID",
16
+ type: "string",
17
+ },
18
+ });
19
+ export const handler = (props) => {
20
+ log.warning("`neonctl set-context` is deprecated and will be removed in a future release. " +
21
+ "Use `neonctl link` instead — it verifies inputs and infers the org for you " +
22
+ "(or `neonctl link --no-checks` for the same write-without-checks behavior).");
23
+ const context = {
24
+ projectId: props.projectId,
25
+ orgId: props.orgId,
26
+ branchId: props.branchId,
27
+ };
28
+ applyContext(props.contextFile, context);
29
+ };
@@ -0,0 +1,40 @@
1
+ import { fillSingleProject } from "../utils/enrichers.js";
2
+ import { status } from "./config.js";
3
+ /**
4
+ * `neon status` is a top-level alias for `neon config status` — the most-reached-for
5
+ * config subcommand. It mirrors that command's options (including `--current-branch`,
6
+ * the offline branch probe) and delegates to the same `status` handler.
7
+ *
8
+ * Because it has a handler but no subcommands, `status` must also be listed in
9
+ * `NO_SUBCOMMANDS_VERBS` (see index.ts) so the help-fallback middleware doesn't
10
+ * intercept a bare `neon status`.
11
+ */
12
+ export const command = "status";
13
+ export const describe = "Show the branch's live Neon state (alias of `config status`)";
14
+ export const builder = (argv) => argv
15
+ .usage("$0 status [options]")
16
+ .options({
17
+ "project-id": {
18
+ describe: "Project ID",
19
+ type: "string",
20
+ },
21
+ branch: {
22
+ describe: "Branch ID or name",
23
+ type: "string",
24
+ },
25
+ "config-json": {
26
+ describe: "Print only the branch's live config as neon.ts-shaped JSON " +
27
+ "(services + branch tuning + preview), to stdout. Useful for " +
28
+ "scripting or copying into a neon.ts.",
29
+ type: "boolean",
30
+ default: false,
31
+ },
32
+ "current-branch": {
33
+ describe: "Print only the linked branch name from the local .neon file " +
34
+ "(no network). Exits non-zero when no branch is pinned.",
35
+ type: "boolean",
36
+ default: false,
37
+ },
38
+ })
39
+ .middleware(fillSingleProject);
40
+ export const handler = (args) => status(args);
@@ -0,0 +1,15 @@
1
+ import { writer } from "../writer.js";
2
+ export const command = "me";
3
+ export const describe = "Show current user";
4
+ export const builder = (yargs) => yargs.option("context-file", {
5
+ hidden: true,
6
+ });
7
+ export const handler = async (args) => {
8
+ await me(args);
9
+ };
10
+ const me = async (props) => {
11
+ const { data } = await props.apiClient.getCurrentUserInfo();
12
+ writer(props).end(data, {
13
+ fields: ["login", "email", "name", "projects_limit"],
14
+ });
15
+ };
@@ -0,0 +1,134 @@
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 ADDED
@@ -0,0 +1,11 @@
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
+ };
@@ -0,0 +1,72 @@
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
+ };
@@ -0,0 +1,177 @@
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 wrapWithContextFile = (dir) => resolve(dir, CONTEXT_FILE);
37
+ /**
38
+ * Resolve the default `.neon` path for the current working directory.
39
+ *
40
+ * Walks UP from `cwd` looking ONLY for an already-existing `.neon` file so
41
+ * commands run from a sub-directory of a linked project still pick up the
42
+ * project's context. If no `.neon` is found, the path defaults to
43
+ * `<cwd>/.neon`, which makes `neonctl link` and `neonctl set-context`
44
+ * predictable: they always write the context file into the directory they
45
+ * were invoked from.
46
+ *
47
+ * Historically the walk also considered `package.json` and `.git` as project
48
+ * markers, but that led to surprising behaviour when running `link` from a
49
+ * fresh sub-directory inside an unrelated repo (the new link would land in
50
+ * the parent repo's root instead of the cwd).
51
+ *
52
+ * `cwd` is overridable so tests can exercise the walk-up without mutating
53
+ * `process.cwd()` (which would race with other tests running in parallel).
54
+ */
55
+ export const currentContextFile = (cwd = process.cwd()) => {
56
+ let currentDir = cwd;
57
+ const root = normalize("/");
58
+ const home = homedir();
59
+ while (currentDir !== root && currentDir !== home) {
60
+ try {
61
+ accessSync(resolve(currentDir, CONTEXT_FILE));
62
+ return wrapWithContextFile(currentDir);
63
+ }
64
+ catch {
65
+ // ignore
66
+ }
67
+ currentDir = resolve(currentDir, "..");
68
+ }
69
+ return wrapWithContextFile(cwd);
70
+ };
71
+ export const readContextFile = (file) => {
72
+ try {
73
+ return JSON.parse(readFileSync(file, "utf-8"));
74
+ }
75
+ catch {
76
+ return {};
77
+ }
78
+ };
79
+ export const enrichFromContext = (args) => {
80
+ // `link` and the deprecated `set-context` manage the context file themselves
81
+ // and must see the raw flags rather than values pre-filled from an existing
82
+ // `.neon`, so skip enrichment for both.
83
+ if (args._[0] === "link" || args._[0] === "set-context") {
84
+ return;
85
+ }
86
+ const context = readContextFile(args.contextFile);
87
+ if (!args.orgId) {
88
+ args.orgId = context.orgId;
89
+ }
90
+ if (!args.projectId) {
91
+ args.projectId = context.projectId;
92
+ }
93
+ if (!args.branch &&
94
+ !args.id &&
95
+ !args.name &&
96
+ context.projectId === args.projectId) {
97
+ args.branch = contextBranch(context);
98
+ }
99
+ };
100
+ export const updateContextFile = (file, context) => {
101
+ writeFileSync(file, JSON.stringify(context, null, 2));
102
+ };
103
+ /**
104
+ * Shared primitive used by `link`, the deprecated `set-context`, and `checkout`
105
+ * to persist context. Mirrors the destructive write semantics of
106
+ * `updateContextFile` — any field not present in `context` is dropped from the
107
+ * file.
108
+ *
109
+ * `.gitignore` scaffolding only happens when the context file is being
110
+ * *created* (it didn't exist before this write). On updates to an existing
111
+ * `.neon` we never touch `.gitignore`, so a user who deliberately un-ignored
112
+ * the file (e.g. to commit shared context) won't have the entry re-added on
113
+ * every subsequent command.
114
+ */
115
+ export const applyContext = (file, context) => {
116
+ const isNewFile = !existsSync(file);
117
+ updateContextFile(file, context);
118
+ if (isNewFile) {
119
+ ensureGitignored(file);
120
+ }
121
+ };
122
+ /**
123
+ * Low-level writer for callers that already hold the resolved identifiers and
124
+ * just need to record them — e.g. `init` or `projects create`, which create a
125
+ * project and want to link it without the resolution, verification, prompting,
126
+ * or env-pull that `link` performs.
127
+ *
128
+ * Unlike the loose {@link applyContext}, this enforces at the type level that
129
+ * `orgId` and `projectId` are present, so the `.neon` file never ends up with a
130
+ * dangling project that has no org. The branch stays optional. It writes through
131
+ * {@link applyContext}, so the same `.gitignore` scaffolding applies.
132
+ */
133
+ export const setContext = (file, context) => {
134
+ applyContext(file, {
135
+ orgId: context.orgId,
136
+ projectId: context.projectId,
137
+ branch: context.branch,
138
+ });
139
+ };
140
+ /**
141
+ * Make sure the `.gitignore` next to `file` lists the file's basename
142
+ * (currently always `.neon`). Creates the `.gitignore` if it doesn't exist,
143
+ * or appends `.neon` if it's missing — never duplicates an existing entry.
144
+ *
145
+ * Best-effort: a failure here (e.g. read-only filesystem) is logged at debug
146
+ * level and swallowed; persisting the context file is the primary goal and
147
+ * must not be blocked by a `.gitignore` write error.
148
+ */
149
+ export const ensureGitignored = (file) => {
150
+ try {
151
+ const dir = dirname(file);
152
+ const entry = basenameOf(file);
153
+ const gitignorePath = resolve(dir, GITIGNORE_FILE);
154
+ if (!existsSync(gitignorePath)) {
155
+ writeFileSync(gitignorePath, `${entry}\n`);
156
+ return;
157
+ }
158
+ const current = readFileSync(gitignorePath, "utf-8");
159
+ if (hasGitignoreEntry(current, entry)) {
160
+ return;
161
+ }
162
+ const needsLeadingNewline = current.length > 0 && !current.endsWith("\n");
163
+ const addition = `${needsLeadingNewline ? "\n" : ""}${entry}\n`;
164
+ writeFileSync(gitignorePath, current + addition);
165
+ }
166
+ catch (err) {
167
+ const message = err instanceof Error ? err.message : String(err);
168
+ log.debug("Failed to update .gitignore next to %s: %s", file, message);
169
+ }
170
+ };
171
+ const basenameOf = (file) => {
172
+ const parts = file.split(/[\\/]/);
173
+ return parts[parts.length - 1] || CONTEXT_FILE;
174
+ };
175
+ const hasGitignoreEntry = (content, entry) => {
176
+ return content.split(/\r?\n/).some((line) => line.trim() === entry);
177
+ };