neon 4.16.0 → 4.17.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.
package/dist/help.js CHANGED
@@ -1,8 +1,75 @@
1
+ import { globalOptionsTrailer, helpWidth, wrapHelpText } from "./utils/help_text.js";
1
2
  import { consumeBlockIfMatches, consumeNextMatching, drawPointer, splitColumns } from "./utils/ui.js";
2
3
  import chalk from "chalk";
3
4
  import cliui from "cliui";
4
5
  //#region src/help.ts
5
6
  const SPACE_WIDTH = 20;
7
+ const DESCRIPTION_GUTTER = 4;
8
+ const wrapDescription = (text) => wrapHelpText(text, Math.max(1, helpWidth() - SPACE_WIDTH - DESCRIPTION_GUTTER));
9
+ const isGlobalOptionsHeader = (header) => /global options:/i.test(header);
10
+ const isTopLevelUsage = (usage) => /^\S+ <command>/.test(usage);
11
+ const renderOptionBlock = (optionsBlock) => {
12
+ const result = [];
13
+ const [header, ...body] = optionsBlock;
14
+ if (header === void 0) return result;
15
+ result.push(header);
16
+ body.forEach((line) => {
17
+ const [option, description] = splitColumns(line);
18
+ const ui = cliui({
19
+ width: helpWidth(),
20
+ wrap: false
21
+ });
22
+ if (option.startsWith("-")) {
23
+ ui.div({
24
+ text: chalk.green(option),
25
+ padding: [
26
+ 0,
27
+ 0,
28
+ 0,
29
+ 0
30
+ ]
31
+ });
32
+ ui.div({
33
+ text: chalk.gray(drawPointer(SPACE_WIDTH)),
34
+ width: SPACE_WIDTH,
35
+ padding: [
36
+ 0,
37
+ 2,
38
+ 0,
39
+ 0
40
+ ]
41
+ }, {
42
+ text: chalk.rgb(210, 210, 210)(wrapDescription(description ?? "")),
43
+ padding: [
44
+ 0,
45
+ 0,
46
+ 0,
47
+ 0
48
+ ]
49
+ });
50
+ } else ui.div({
51
+ padding: [
52
+ 0,
53
+ 0,
54
+ 0,
55
+ 0
56
+ ],
57
+ text: "",
58
+ width: SPACE_WIDTH
59
+ }, {
60
+ text: chalk.rgb(210, 210, 210)(wrapDescription(option)),
61
+ padding: [
62
+ 0,
63
+ 0,
64
+ 0,
65
+ 0
66
+ ]
67
+ });
68
+ result.push(ui.toString());
69
+ });
70
+ result.push("");
71
+ return result;
72
+ };
6
73
  const formatHelp = (help) => {
7
74
  const lines = help.split("\n");
8
75
  const result = [];
@@ -11,11 +78,16 @@ const formatHelp = (help) => {
11
78
  result.push(chalk.bold(topLevelCommand.replace("[options]", chalk.reset.green("[options]"))));
12
79
  result.push("");
13
80
  }
14
- const commandsBlock = consumeBlockIfMatches(lines, /^Commands:/);
15
- if (commandsBlock.length > 0) {
81
+ const consumeCommands = () => {
82
+ const commandsBlock = consumeBlockIfMatches(lines, /^Commands:/);
83
+ if (commandsBlock.length === 0) return;
16
84
  const header = commandsBlock.shift();
85
+ if (header === void 0) return;
17
86
  result.push(header);
18
- const ui = cliui({ width: 0 });
87
+ const ui = cliui({
88
+ width: helpWidth(),
89
+ wrap: false
90
+ });
19
91
  commandsBlock.forEach((line) => {
20
92
  if (/^\s{3,}/.exec(line)) {
21
93
  ui.div({
@@ -28,7 +100,7 @@ const formatHelp = (help) => {
28
100
  0
29
101
  ]
30
102
  }, {
31
- text: line.trim(),
103
+ text: wrapDescription(line.trim()),
32
104
  padding: [
33
105
  0,
34
106
  0,
@@ -54,7 +126,7 @@ const formatHelp = (help) => {
54
126
  0
55
127
  ]
56
128
  }, {
57
- text: description,
129
+ text: wrapDescription(description),
58
130
  padding: [
59
131
  0,
60
132
  0,
@@ -65,12 +137,17 @@ const formatHelp = (help) => {
65
137
  });
66
138
  result.push(ui.toString());
67
139
  result.push("");
68
- }
69
- const positionalsBlock = consumeBlockIfMatches(lines, /Positionals:/);
70
- if (positionalsBlock.length > 0) {
140
+ };
141
+ const consumePositionals = () => {
142
+ const positionalsBlock = consumeBlockIfMatches(lines, /Positionals:/);
143
+ if (positionalsBlock.length === 0) return;
71
144
  const header = positionalsBlock.shift();
145
+ if (header === void 0) return;
72
146
  result.push(header);
73
- const ui = cliui({ width: 0 });
147
+ const ui = cliui({
148
+ width: helpWidth(),
149
+ wrap: false
150
+ });
74
151
  positionalsBlock.forEach((line) => {
75
152
  const [positional, description] = splitColumns(line);
76
153
  ui.div({
@@ -83,7 +160,7 @@ const formatHelp = (help) => {
83
160
  0
84
161
  ]
85
162
  }, {
86
- text: description,
163
+ text: wrapDescription(description),
87
164
  padding: [
88
165
  0,
89
166
  0,
@@ -94,73 +171,42 @@ const formatHelp = (help) => {
94
171
  });
95
172
  result.push(ui.toString());
96
173
  result.push("");
97
- }
174
+ };
175
+ consumeCommands();
176
+ consumePositionals();
98
177
  const descriptionBlock = consumeBlockIfMatches(lines, /^(?!.*options:)/i);
99
178
  if (descriptionBlock.length > 0) {
100
179
  result.push(...descriptionBlock);
101
180
  result.push("");
102
181
  }
182
+ consumeCommands();
183
+ consumePositionals();
184
+ const optionBlocks = [];
103
185
  while (true) {
104
186
  const optionsBlock = consumeBlockIfMatches(lines, /.*options:/i);
105
187
  if (optionsBlock.length === 0) break;
106
- result.push(optionsBlock.shift());
107
- optionsBlock.forEach((line) => {
108
- const [option, description] = splitColumns(line);
109
- const ui = cliui({ width: 0 });
110
- if (option.startsWith("-")) {
111
- ui.div({
112
- text: chalk.green(option),
113
- padding: [
114
- 0,
115
- 0,
116
- 0,
117
- 0
118
- ]
119
- });
120
- ui.div({
121
- text: chalk.gray(drawPointer(SPACE_WIDTH)),
122
- width: SPACE_WIDTH,
123
- padding: [
124
- 0,
125
- 2,
126
- 0,
127
- 0
128
- ]
129
- }, {
130
- text: chalk.rgb(210, 210, 210)(description ?? ""),
131
- padding: [
132
- 0,
133
- 0,
134
- 0,
135
- 0
136
- ]
137
- });
138
- } else ui.div({
139
- padding: [
140
- 0,
141
- 0,
142
- 0,
143
- 0
144
- ],
145
- text: "",
146
- width: SPACE_WIDTH
147
- }, {
148
- text: chalk.rgb(210, 210, 210)(option),
149
- padding: [
150
- 0,
151
- 0,
152
- 0,
153
- 0
154
- ]
155
- });
156
- result.push(ui.toString());
157
- });
188
+ optionBlocks.push(optionsBlock);
189
+ }
190
+ for (const block of optionBlocks) {
191
+ const header = block[0];
192
+ if (header !== void 0 && !isGlobalOptionsHeader(header)) result.push(...renderOptionBlock(block));
193
+ }
194
+ const globalBlocks = optionBlocks.filter((block) => {
195
+ const header = block[0];
196
+ return header !== void 0 && isGlobalOptionsHeader(header);
197
+ });
198
+ if (topLevelCommand !== null && isTopLevelUsage(topLevelCommand)) for (const block of globalBlocks) result.push(...renderOptionBlock(block));
199
+ else if (globalBlocks.length > 0 && topLevelCommand !== null) {
200
+ result.push(globalOptionsTrailer(topLevelCommand));
158
201
  result.push("");
159
202
  }
160
203
  const exampleBlock = consumeBlockIfMatches(lines, /Examples:/);
161
204
  if (exampleBlock.length > 0) {
162
205
  result.push(exampleBlock.shift());
163
- const ui = cliui({ width: 0 });
206
+ const ui = cliui({
207
+ width: helpWidth(),
208
+ wrap: false
209
+ });
164
210
  for (const line of exampleBlock) {
165
211
  const [command, description] = splitColumns(line);
166
212
  ui.div({
@@ -173,7 +219,7 @@ const formatHelp = (help) => {
173
219
  ]
174
220
  });
175
221
  ui.div({
176
- text: chalk.reset(description),
222
+ text: chalk.reset(wrapDescription(description)),
177
223
  padding: [
178
224
  0,
179
225
  0,
@@ -188,7 +234,16 @@ const formatHelp = (help) => {
188
234
  };
189
235
  const showHelp = async (argv) => {
190
236
  const help = await argv.getHelp();
191
- process.stderr.write(formatHelp(help).join("\n") + "\n");
237
+ const text = `${formatHelp(help).join("\n")}\n`;
238
+ await new Promise((resolve, reject) => {
239
+ process.stdout.write(text, (err) => {
240
+ if (err) {
241
+ reject(err);
242
+ return;
243
+ }
244
+ resolve();
245
+ });
246
+ });
192
247
  process.exit(0);
193
248
  };
194
249
  //#endregion
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- import { t as storeFor } from "./_chunks/credential_io-D8AR-jvB.js";
2
1
  import { log } from "./log.js";
3
2
  import pkg_default from "./pkg.js";
4
3
  import { isNeonApiError, messageFromBody } from "./api.js";
5
- import { authFailureMessage, getAuthContext, locationFromContext } from "./auth_context.js";
4
+ import { getAuthContext } from "./auth_context.js";
6
5
  import { r as defaultDir } from "./_chunks/paths-DMq0Lt7a.js";
7
6
  import { ensureConfigDir } from "./config.js";
8
7
  import { currentContextFile, enrichFromContext } from "./context.js";
9
8
  import { analyticsMiddleware, closeAnalytics, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent } from "./analytics.js";
10
9
  import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.js";
11
10
  import { AuthRefreshError, defaultClientID } from "./auth.js";
11
+ import { ensureAuth } from "./commands/auth.js";
12
+ import { recoverFrom401 } from "./auth_recovery.js";
12
13
  import { getCliName } from "./utils/cli_name.js";
13
14
  import { showHelp } from "./help.js";
14
- import { ensureAuth, isAccessTokenUsable, refreshStoredCredentials } from "./commands/auth.js";
15
15
  import { rewriteUnknownAgentArg } from "./init/plan.js";
16
16
  import commands_default from "./commands/index.js";
17
17
  import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
@@ -100,6 +100,7 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
100
100
  },
101
101
  "context-file": {
102
102
  describe: "Context file",
103
+ group: "Global options:",
103
104
  type: "string",
104
105
  default: currentContextFile
105
106
  },
@@ -124,63 +125,6 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
124
125
  }).alias("help", "h").middleware(async (args) => {
125
126
  if (args.help || args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0])) await showHelp(builder);
126
127
  }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(commands_default).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(false);
127
- function supersededOnDisk(rejectedToken) {
128
- if (rejectedToken === void 0) return false;
129
- const context = getAuthContext();
130
- if (context === null) return false;
131
- const at = locationFromContext(context);
132
- if (at === null) return false;
133
- try {
134
- const loaded = storeFor(context.configDir).read(at);
135
- return loaded !== null && loaded.credentials.access_token !== rejectedToken && isAccessTokenUsable(loaded.credentials, Date.now());
136
- } catch {
137
- return false;
138
- }
139
- }
140
- async function recoverFrom401(canRetry) {
141
- const context = getAuthContext();
142
- if (context === null || context.source !== "stored-credentials") {
143
- log.error(authFailureMessage(context));
144
- return false;
145
- }
146
- if (context.refreshed === true) {
147
- log.error(authFailureMessage(context));
148
- return false;
149
- }
150
- if (!canRetry) {
151
- log.error(authFailureMessage(context));
152
- return false;
153
- }
154
- if (supersededOnDisk(context.accessToken)) {
155
- log.debug("The rejected token has already been replaced on disk; retrying with the current one");
156
- return true;
157
- }
158
- const at = locationFromContext(context);
159
- if (at === null || context.oauthHost === void 0 || context.clientId === void 0) {
160
- log.error(authFailureMessage(context));
161
- return false;
162
- }
163
- try {
164
- if (await refreshStoredCredentials(at, {
165
- apiHost: "",
166
- oauthHost: context.oauthHost,
167
- clientId: context.clientId,
168
- configDir: context.configDir
169
- })) {
170
- log.debug("Refreshed the stored session after a 401; retrying");
171
- return true;
172
- }
173
- } catch (err) {
174
- if (err instanceof AuthRefreshError) {
175
- log.error(err.message);
176
- if (err.terminal) log.error(`Run \`neon auth --profile ${context.profile ?? "DEFAULT"}\` to sign in again.`);
177
- return false;
178
- }
179
- log.debug("Refresh after 401 failed: %s", err instanceof Error ? err.message : "unknown error");
180
- }
181
- log.error(authFailureMessage(context));
182
- return false;
183
- }
184
128
  async function handleError(msg, err, canRetry) {
185
129
  if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
186
130
  await showHelp(builder);
@@ -0,0 +1,38 @@
1
+ import { isNeonApiError } from "../api.js";
2
+ import { r as defaultDir } from "../_chunks/paths-DMq0Lt7a.js";
3
+ import "../config.js";
4
+ import "../auth.js";
5
+ import { ensureAuth } from "../commands/auth.js";
6
+ import { recoverFrom401 } from "../auth_recovery.js";
7
+ import { runLink } from "../commands/link.js";
8
+ //#region src/init/link.ts
9
+ const isUnauthorized = (error) => isNeonApiError(error) && error.status === 401;
10
+ const authenticationFailed = () => /* @__PURE__ */ new Error("Authentication failed.");
11
+ const runAuthenticatedLink = async (props) => {
12
+ const authenticated = {
13
+ ...props,
14
+ _: ["link"],
15
+ configDir: props.configDir ?? defaultDir,
16
+ oauthHost: props.oauthHost ?? process.env.NEON_OAUTH_HOST ?? "https://oauth2.neon.tech",
17
+ clientId: props.clientId ?? "neonctl",
18
+ help: false
19
+ };
20
+ await ensureAuth(authenticated);
21
+ try {
22
+ await runLink(authenticated);
23
+ return;
24
+ } catch (error) {
25
+ if (!isUnauthorized(error)) throw error;
26
+ if (!await recoverFrom401(true)) throw authenticationFailed();
27
+ }
28
+ await ensureAuth(authenticated);
29
+ try {
30
+ await runLink(authenticated);
31
+ } catch (error) {
32
+ if (!isUnauthorized(error)) throw error;
33
+ await recoverFrom401(false);
34
+ throw authenticationFailed();
35
+ }
36
+ };
37
+ //#endregion
38
+ export { runAuthenticatedLink };
@@ -91,5 +91,17 @@ const pickInitConfigInteractively = async () => {
91
91
  if (value === void 0) throw new Error("Aborted.");
92
92
  return value === true;
93
93
  };
94
+ const pickInitLinkInteractively = async () => {
95
+ requireInteractive();
96
+ const { value } = await prompts({
97
+ onState: restoreCursorOnAbort,
98
+ type: "confirm",
99
+ name: "value",
100
+ message: "Link this project to a Neon project now?",
101
+ initial: true
102
+ });
103
+ if (value === void 0) throw new Error("Aborted.");
104
+ return value === true;
105
+ };
94
106
  //#endregion
95
- export { INIT_TEMPLATE_PICKER_INITIAL, initTemplatePickerChoices, pickAgentSetupInteractively, pickInitConfigInteractively, pickInitTemplateInteractively };
107
+ export { INIT_TEMPLATE_PICKER_INITIAL, initTemplatePickerChoices, pickAgentSetupInteractively, pickInitConfigInteractively, pickInitLinkInteractively, pickInitTemplateInteractively };
@@ -85,6 +85,7 @@ const test = test$1.extend({
85
85
  expect(code).toBe(options?.code ?? 0);
86
86
  if (options.snapshot !== false) expect(output).toMatchSnapshot();
87
87
  if (options.stderr !== void 0) expect(strip(error).replace(/\s+/g, " ").trim()).toEqual(typeof options.stderr === "string" ? options.stderr.toString().replace(/\s+/g, " ") : options.stderr);
88
+ if (options.stdout !== void 0) expect(strip(output).replace(/\s+/g, " ").trim()).toEqual(typeof options.stdout === "string" ? options.stdout.toString().replace(/\s+/g, " ") : options.stdout);
88
89
  resolve({
89
90
  stdout: output,
90
91
  stderr: error,
@@ -1,10 +1,13 @@
1
+ import { parseColumns } from "../human_table.js";
1
2
  //#region src/utils/help_text.ts
2
3
  const HELP_WRAP = 78;
3
4
  const WRAP_INDENT = " ";
4
5
  const helpWidth = () => {
5
- const columns = process.stderr.columns ?? process.stdout.columns;
6
- if (typeof columns !== "number" || !Number.isFinite(columns) || columns < 1) return 78;
7
- return Math.max(24, columns - 1);
6
+ const fromStdout = process.stdout.columns;
7
+ if (typeof fromStdout === "number" && Number.isFinite(fromStdout) && fromStdout >= 1) return Math.max(24, fromStdout - 1);
8
+ const fromEnv = parseColumns(process.env.COLUMNS);
9
+ if (fromEnv !== void 0) return Math.max(24, fromEnv - 1);
10
+ return 78;
8
11
  };
9
12
  const helpCsv = (label, items, width = helpWidth()) => {
10
13
  if (items.length === 0) return "";
@@ -24,6 +27,31 @@ const helpCsv = (label, items, width = helpWidth()) => {
24
27
  lines.push(line);
25
28
  return lines.join("\n");
26
29
  };
30
+ const wrapHelpText = (text, width) => {
31
+ if (width < 1) return text;
32
+ const words = text.trim().split(/\s+/).filter((word) => word !== "");
33
+ if (words.length === 0) return text;
34
+ const lines = [];
35
+ let line = "";
36
+ for (const word of words) {
37
+ if (line === "") {
38
+ line = word;
39
+ continue;
40
+ }
41
+ if (line.length + 1 + word.length <= width) {
42
+ line = `${line} ${word}`;
43
+ continue;
44
+ }
45
+ lines.push(line);
46
+ line = word;
47
+ }
48
+ if (line !== "") lines.push(line);
49
+ return lines.join("\n");
50
+ };
27
51
  const helpEpilogue = (...blocks) => ["", ...blocks.filter((block) => block !== "")].join("\n");
52
+ const globalOptionsTrailer = (usage) => {
53
+ const cli = usage.trim().split(/\s+/)[0];
54
+ return `Global options: see ${cli === "" ? "neon" : cli} --help`;
55
+ };
28
56
  //#endregion
29
- export { HELP_WRAP, helpCsv, helpEpilogue, helpWidth };
57
+ export { HELP_WRAP, globalOptionsTrailer, helpCsv, helpEpilogue, helpWidth, wrapHelpText };
package/dist/writer.js CHANGED
@@ -7,7 +7,8 @@ const writeYaml = (chunks) => {
7
7
  return YAML.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
8
8
  };
9
9
  const writeJson = (chunks) => {
10
- return JSON.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
10
+ const json = JSON.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
11
+ return json === void 0 ? json : `${json}\n`;
11
12
  };
12
13
  const writeTable = (chunks, out, width) => {
13
14
  chunks.forEach(({ data, config }, i) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.16.0",
3
+ "version": "4.17.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.3.3",
67
- "@neon/config-runtime": "1.2.4",
68
- "@neon/sdk": "4.1.0"
66
+ "@neon/config": "1.4.0",
67
+ "@neon/sdk": "4.1.0",
68
+ "@neon/config-runtime": "1.3.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -98,8 +98,8 @@
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
100
  "@neon-internals/cli-core": "0.0.0",
101
- "@neon-internals/env-core": "0.0.11",
102
101
  "@neon/e2e-harness": "0.0.0",
102
+ "@neon-internals/env-core": "0.0.12",
103
103
  "@neon/functions": "0.9.0"
104
104
  },
105
105
  "publishConfig": {