neonctl 2.27.1 → 2.29.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 (136) hide show
  1. package/README.md +35 -3
  2. package/dist/analytics.js +52 -34
  3. package/dist/api.js +643 -13
  4. package/dist/auth.js +50 -44
  5. package/dist/cli.js +8 -1
  6. package/dist/commands/auth.js +64 -51
  7. package/dist/commands/bootstrap.js +115 -157
  8. package/dist/commands/branches.js +160 -150
  9. package/dist/commands/bucket.js +183 -146
  10. package/dist/commands/checkout.js +51 -51
  11. package/dist/commands/config.js +228 -82
  12. package/dist/commands/connection_string.js +62 -62
  13. package/dist/commands/data_api.js +100 -101
  14. package/dist/commands/databases.js +29 -26
  15. package/dist/commands/deploy.js +12 -12
  16. package/dist/commands/dev.js +114 -114
  17. package/dist/commands/env.js +43 -43
  18. package/dist/commands/functions.js +101 -104
  19. package/dist/commands/index.js +27 -25
  20. package/dist/commands/init.js +23 -22
  21. package/dist/commands/ip_allow.js +29 -29
  22. package/dist/commands/link.js +232 -182
  23. package/dist/commands/neon_auth.js +385 -370
  24. package/dist/commands/operations.js +11 -11
  25. package/dist/commands/orgs.js +8 -8
  26. package/dist/commands/projects.js +103 -101
  27. package/dist/commands/psql.js +31 -31
  28. package/dist/commands/roles.js +27 -24
  29. package/dist/commands/schema_diff.js +25 -26
  30. package/dist/commands/set_context.js +17 -17
  31. package/dist/commands/status.js +40 -0
  32. package/dist/commands/user.js +5 -5
  33. package/dist/commands/vpc_endpoints.js +50 -50
  34. package/dist/config.js +7 -7
  35. package/dist/config_format.js +5 -5
  36. package/dist/context.js +37 -14
  37. package/dist/current_branch_fast_path.js +55 -0
  38. package/dist/dev/env.js +33 -33
  39. package/dist/dev/functions.js +4 -4
  40. package/dist/dev/inputs.js +6 -6
  41. package/dist/dev/runtime.js +25 -25
  42. package/dist/env.js +14 -14
  43. package/dist/env_file.js +13 -13
  44. package/dist/errors.js +68 -5
  45. package/dist/functions_api.js +10 -10
  46. package/dist/help.js +15 -15
  47. package/dist/index.js +110 -107
  48. package/dist/log.js +2 -2
  49. package/dist/parameters.gen.js +14 -14
  50. package/dist/pkg.js +5 -5
  51. package/dist/psql/cli.js +4 -2
  52. package/dist/psql/command/cmd_cond.js +61 -61
  53. package/dist/psql/command/cmd_connect.js +159 -154
  54. package/dist/psql/command/cmd_copy.js +107 -97
  55. package/dist/psql/command/cmd_describe.js +368 -363
  56. package/dist/psql/command/cmd_format.js +276 -263
  57. package/dist/psql/command/cmd_io.js +269 -263
  58. package/dist/psql/command/cmd_lo.js +74 -66
  59. package/dist/psql/command/cmd_meta.js +148 -148
  60. package/dist/psql/command/cmd_misc.js +17 -17
  61. package/dist/psql/command/cmd_pipeline.js +142 -135
  62. package/dist/psql/command/cmd_restrict.js +25 -25
  63. package/dist/psql/command/cmd_show.js +183 -168
  64. package/dist/psql/command/dispatch.js +26 -26
  65. package/dist/psql/command/shared.js +14 -14
  66. package/dist/psql/complete/filenames.js +16 -16
  67. package/dist/psql/complete/index.js +4 -4
  68. package/dist/psql/complete/matcher.js +33 -32
  69. package/dist/psql/complete/psqlVars.js +173 -173
  70. package/dist/psql/complete/queries.js +5 -3
  71. package/dist/psql/complete/rules.js +900 -863
  72. package/dist/psql/core/common.js +136 -133
  73. package/dist/psql/core/help.js +343 -343
  74. package/dist/psql/core/mainloop.js +160 -153
  75. package/dist/psql/core/prompt.js +126 -123
  76. package/dist/psql/core/settings.js +111 -111
  77. package/dist/psql/core/sqlHelp.js +150 -150
  78. package/dist/psql/core/startup.js +211 -205
  79. package/dist/psql/core/syncVars.js +14 -14
  80. package/dist/psql/core/variables.js +24 -24
  81. package/dist/psql/describe/formatters.js +302 -289
  82. package/dist/psql/describe/processNamePattern.js +28 -28
  83. package/dist/psql/describe/queries.js +656 -651
  84. package/dist/psql/index.js +436 -411
  85. package/dist/psql/io/history.js +36 -36
  86. package/dist/psql/io/input.js +15 -15
  87. package/dist/psql/io/lineEditor/buffer.js +27 -25
  88. package/dist/psql/io/lineEditor/complete.js +15 -15
  89. package/dist/psql/io/lineEditor/filename.js +22 -22
  90. package/dist/psql/io/lineEditor/index.js +65 -62
  91. package/dist/psql/io/lineEditor/keymap.js +325 -318
  92. package/dist/psql/io/lineEditor/vt100.js +60 -60
  93. package/dist/psql/io/pgpass.js +18 -18
  94. package/dist/psql/io/pgservice.js +14 -14
  95. package/dist/psql/io/psqlrc.js +46 -46
  96. package/dist/psql/print/aligned.js +175 -166
  97. package/dist/psql/print/asciidoc.js +51 -51
  98. package/dist/psql/print/crosstab.js +34 -31
  99. package/dist/psql/print/csv.js +25 -22
  100. package/dist/psql/print/html.js +54 -54
  101. package/dist/psql/print/json.js +12 -12
  102. package/dist/psql/print/latex.js +118 -118
  103. package/dist/psql/print/pager.js +28 -26
  104. package/dist/psql/print/troff.js +48 -48
  105. package/dist/psql/print/unaligned.js +15 -14
  106. package/dist/psql/print/units.js +17 -17
  107. package/dist/psql/scanner/slash.js +48 -46
  108. package/dist/psql/scanner/sql.js +88 -84
  109. package/dist/psql/scanner/stringutils.js +21 -17
  110. package/dist/psql/types/index.js +7 -7
  111. package/dist/psql/types/scanner.js +8 -8
  112. package/dist/psql/wire/connection.js +341 -327
  113. package/dist/psql/wire/copy.js +7 -7
  114. package/dist/psql/wire/pipeline.js +26 -24
  115. package/dist/psql/wire/protocol.js +102 -102
  116. package/dist/psql/wire/sasl.js +62 -62
  117. package/dist/psql/wire/tls.js +79 -73
  118. package/dist/storage_api.js +22 -23
  119. package/dist/test_utils/fixtures.js +74 -41
  120. package/dist/test_utils/oauth_server.js +5 -5
  121. package/dist/utils/api_enums.js +33 -0
  122. package/dist/utils/branch_notice.js +5 -5
  123. package/dist/utils/branch_picker.js +26 -26
  124. package/dist/utils/compute_units.js +4 -4
  125. package/dist/utils/enrichers.js +28 -16
  126. package/dist/utils/esbuild.js +28 -28
  127. package/dist/utils/formats.js +1 -1
  128. package/dist/utils/middlewares.js +3 -3
  129. package/dist/utils/package_manager.js +68 -0
  130. package/dist/utils/point_in_time.js +12 -12
  131. package/dist/utils/psql.js +30 -30
  132. package/dist/utils/string.js +2 -2
  133. package/dist/utils/ui.js +9 -9
  134. package/dist/utils/zip.js +1 -1
  135. package/dist/writer.js +17 -17
  136. package/package.json +10 -12
@@ -1,95 +1,99 @@
1
- import { isAxiosError } from 'axios';
2
- import prompts from 'prompts';
3
- import { applyContext, contextBranch, readContextFile, setContext, updateContextFile, } from '../context.js';
4
- import { isCi } from '../env.js';
5
- import { log } from '../log.js';
6
- import { createBranch, pickBranchInteractively, } from '../utils/branch_picker.js';
7
- import { autoPullEnvAfterPin, renderAgentPullNote } from './env.js';
8
- import { REGIONS } from './projects.js';
1
+ import prompts from "prompts";
2
+ import { isNeonApiError, messageFromBody } from "../api.js";
3
+ import { applyContext, contextBranch, readContextFile, setContext, updateContextFile, } from "../context.js";
4
+ import { isCi } from "../env.js";
5
+ import { log } from "../log.js";
6
+ import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
7
+ import { hasNeonConfigFile, initCmd } from "./config.js";
8
+ import { autoPullEnvAfterPin, renderAgentPullNote } from "./env.js";
9
+ import { REGIONS } from "./projects.js";
9
10
  const PROJECTS_LIST_LIMIT = 100;
10
- const CREATE_NEW_SENTINEL = '__create_new__';
11
- export const command = 'link';
12
- export const describe = 'Link the current directory to a Neon project';
11
+ const CREATE_NEW_SENTINEL = "__create_new__";
12
+ export const command = "link";
13
+ export const describe = "Link the current directory to a Neon project";
13
14
  export const builder = (argv) => argv
14
- .usage('$0 link [options]')
15
+ .usage("$0 link [options]")
15
16
  .options({
16
- 'org-id': {
17
- describe: 'Organization ID to link to',
18
- type: 'string',
17
+ "org-id": {
18
+ describe: "Organization ID to link to",
19
+ type: "string",
19
20
  },
20
- 'project-id': {
21
- describe: 'Existing project ID to link to',
22
- type: 'string',
21
+ "project-id": {
22
+ describe: "Existing project ID to link to",
23
+ type: "string",
23
24
  },
24
- 'project-name': {
25
- describe: 'Name for a new project to create and link to',
26
- type: 'string',
25
+ "project-name": {
26
+ describe: "Name for a new project to create and link to",
27
+ type: "string",
27
28
  },
28
- 'region-id': {
29
- describe: 'Region ID for a new project (e.g. aws-us-east-2). Required with --project-name.',
30
- type: 'string',
29
+ "region-id": {
30
+ describe: "Region ID for a new project (e.g. aws-us-east-2). Required with --project-name.",
31
+ type: "string",
31
32
  },
32
33
  branch: {
33
- alias: 'branch-id',
34
- describe: 'Branch name or ID to pin in the context (resolved to its ID before writing). ' +
35
- 'Without it, link only resolves the org and project — pin a branch with ' +
36
- '`neonctl checkout <branch>` (link never guesses a default).',
37
- type: 'string',
34
+ alias: "branch-id",
35
+ describe: "Branch name or ID to pin in the context (resolved to its ID before writing). " +
36
+ "Without it, link only resolves the org and project — pin a branch with " +
37
+ "`neonctl checkout <branch>` (link never guesses a default).",
38
+ type: "string",
38
39
  },
39
40
  params: {
40
41
  describe: 'JSON object with link parameters, e.g. \'{"orgId":"...","projectId":"..."}\' or \'{"orgId":"...","projectName":"...","regionId":"..."}\'. Flags take precedence over fields in --params.',
41
- type: 'string',
42
+ type: "string",
42
43
  },
43
44
  agent: {
44
- describe: 'Emit a JSON state-machine response designed for AI agents instead of prompting. The output is a single JSON object with a discriminated `status` field describing the next step.',
45
- type: 'boolean',
45
+ describe: "Emit a JSON state-machine response designed for AI agents instead of prompting. The output is a single JSON object with a discriminated `status` field describing the next step.",
46
+ type: "boolean",
46
47
  default: false,
47
48
  },
48
49
  yes: {
49
- alias: 'y',
50
+ alias: "y",
50
51
  describe: 'Skip the "already linked" confirmation in interactive mode and re-link anyway.',
51
- type: 'boolean',
52
+ type: "boolean",
52
53
  default: false,
53
54
  },
54
55
  clear: {
55
- describe: 'Remove the org/project/branch context (writes an empty context file) instead of linking.',
56
- type: 'boolean',
56
+ describe: "Remove the org/project/branch context (writes an empty context file) instead of linking.",
57
+ type: "boolean",
57
58
  default: false,
58
59
  },
59
60
  checks: {
60
- describe: 'Verify the org/project/branch exist (and resolve the org from the project) before ' +
61
- 'writing. On by default; use --no-checks to write the context offline with no API ' +
62
- 'calls — it then requires --org-id and --project-id (--branch optional) and skips ' +
63
- 'env pull.',
64
- type: 'boolean',
61
+ describe: "Verify the org/project/branch exist (and resolve the org from the project) before " +
62
+ "writing. On by default; use --no-checks to write the context offline with no API " +
63
+ "calls — it then requires --org-id and --project-id (--branch optional) and skips " +
64
+ "env pull.",
65
+ type: "boolean",
65
66
  default: true,
66
67
  },
67
- 'env-pull': {
68
+ "env-pull": {
68
69
  describe: "Pull the linked branch's Neon env vars (DATABASE_URL, …) into a local .env after " +
69
- 'linking. On by default; use --no-env-pull to skip (e.g. when injecting env at ' +
70
- 'runtime with `neon-env run` / `neon dev`). Only runs when a branch is pinned.',
71
- type: 'boolean',
70
+ "linking. On by default; use --no-env-pull to skip (e.g. when injecting env at " +
71
+ "runtime with `neon-env run` / `neon dev`). Only runs when a branch is pinned.",
72
+ type: "boolean",
72
73
  default: true,
73
74
  },
74
75
  })
75
76
  .example([
76
77
  [
77
- '$0 link --project-id polished-snowflake-12345678',
78
+ "$0 link --project-id polished-snowflake-12345678",
78
79
  "Link an existing project (org is inferred); pin a branch later with 'neonctl checkout'",
79
80
  ],
80
81
  [
81
- '$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2',
82
- 'Create a new project and link it',
82
+ "$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2",
83
+ "Create a new project and link it",
83
84
  ],
84
85
  [
85
- '$0 link --branch-id br-…',
86
- 'Pin a branch in the already-linked project',
86
+ "$0 link --branch-id br-…",
87
+ "Pin a branch in the already-linked project",
87
88
  ],
88
89
  [
89
- '$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678',
90
- 'Write the context offline (no API calls, no verification)',
90
+ "$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678",
91
+ "Write the context offline (no API calls, no verification)",
92
+ ],
93
+ [
94
+ "$0 link --clear",
95
+ "Forget the current org/project/branch context",
91
96
  ],
92
- ['$0 link --clear', 'Forget the current org/project/branch context'],
93
97
  ]);
94
98
  export const handler = async (props) => {
95
99
  if (props.clear) {
@@ -113,13 +117,13 @@ export const handler = async (props) => {
113
117
  }
114
118
  if (isCi()) {
115
119
  log.error([
116
- 'Missing inputs and CI environment detected (no TTY for prompts).',
117
- '',
118
- 'Use one of:',
119
- ' neonctl link --agent (JSON state machine for agents)',
120
- ' neonctl link --project-id <project> (link to an existing project; org is inferred)',
121
- ' neonctl link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)',
122
- ].join('\n'));
120
+ "Missing inputs and CI environment detected (no TTY for prompts).",
121
+ "",
122
+ "Use one of:",
123
+ " neonctl link --agent (JSON state machine for agents)",
124
+ " neonctl link --project-id <project> (link to an existing project; org is inferred)",
125
+ " neonctl link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)",
126
+ ].join("\n"));
123
127
  process.exit(1);
124
128
  return;
125
129
  }
@@ -130,7 +134,7 @@ export const handler = async (props) => {
130
134
  // ----------------------------------------------------------------------------
131
135
  const parseInputs = (props) => {
132
136
  let fromParams = {};
133
- if (props.params !== undefined && props.params !== '') {
137
+ if (props.params !== undefined && props.params !== "") {
134
138
  let parsed;
135
139
  try {
136
140
  parsed = JSON.parse(props.params);
@@ -150,33 +154,33 @@ const parseInputs = (props) => {
150
154
  };
151
155
  };
152
156
  const extractParams = (raw) => {
153
- if (raw === null || typeof raw !== 'object') {
154
- throw new Error('--params must be a JSON object');
157
+ if (raw === null || typeof raw !== "object") {
158
+ throw new Error("--params must be a JSON object");
155
159
  }
156
160
  const obj = raw;
157
161
  const pickString = (key) => {
158
162
  const value = obj[key];
159
163
  if (value === undefined || value === null)
160
164
  return undefined;
161
- if (typeof value !== 'string') {
165
+ if (typeof value !== "string") {
162
166
  throw new Error(`--params.${key} must be a string`);
163
167
  }
164
168
  return value;
165
169
  };
166
170
  return {
167
- orgId: pickString('orgId'),
168
- projectId: pickString('projectId'),
169
- projectName: pickString('projectName'),
170
- regionId: pickString('regionId'),
171
- branch: pickString('branch') ?? pickString('branchId'),
171
+ orgId: pickString("orgId"),
172
+ projectId: pickString("projectId"),
173
+ projectName: pickString("projectName"),
174
+ regionId: pickString("regionId"),
175
+ branch: pickString("branch") ?? pickString("branchId"),
172
176
  };
173
177
  };
174
178
  const validateInputs = (inputs) => {
175
179
  if (inputs.projectId && (inputs.projectName || inputs.regionId)) {
176
- throw new Error('Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.');
180
+ throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
177
181
  }
178
182
  if (inputs.projectName && inputs.branch) {
179
- throw new Error('Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then `neonctl checkout <branch>`.');
183
+ throw new Error("Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then `neonctl checkout <branch>`.");
180
184
  }
181
185
  };
182
186
  /**
@@ -224,7 +228,7 @@ const runWithoutChecks = (props) => {
224
228
  throw new Error("--no-checks can't create a project (that needs API access). Pass --org-id and --project-id for an existing project, or drop --no-checks.");
225
229
  }
226
230
  if (!inputs.orgId || !inputs.projectId) {
227
- throw new Error('--no-checks writes the context with no API calls, so it needs both --org-id and --project-id (--branch is optional).');
231
+ throw new Error("--no-checks writes the context with no API calls, so it needs both --org-id and --project-id (--branch is optional).");
228
232
  }
229
233
  setContext(props.contextFile, {
230
234
  orgId: inputs.orgId,
@@ -233,7 +237,7 @@ const runWithoutChecks = (props) => {
233
237
  });
234
238
  if (props.agent) {
235
239
  emitAgent({
236
- status: 'linked',
240
+ status: "linked",
237
241
  context_file: props.contextFile,
238
242
  context: {
239
243
  orgId: inputs.orgId,
@@ -241,7 +245,7 @@ const runWithoutChecks = (props) => {
241
245
  branch: inputs.branch,
242
246
  },
243
247
  project: { id: inputs.projectId },
244
- message: `Wrote ${props.contextFile} without checks (org ${inputs.orgId}, project ${inputs.projectId}${inputs.branch ? `, branch ${inputs.branch}` : ''}). No verification or env pull was performed.`,
248
+ message: `Wrote ${props.contextFile} without checks (org ${inputs.orgId}, project ${inputs.projectId}${inputs.branch ? `, branch ${inputs.branch}` : ""}). No verification or env pull was performed.`,
245
249
  });
246
250
  return;
247
251
  }
@@ -263,11 +267,11 @@ const runWithoutChecks = (props) => {
263
267
  class LinkInputError extends Error {
264
268
  constructor(message, agentCode) {
265
269
  super(message);
266
- this.name = 'LinkInputError';
270
+ this.name = "LinkInputError";
267
271
  this.agentCode = agentCode;
268
272
  }
269
273
  }
270
- const httpStatus = (err) => isAxiosError(err) ? err.response?.status : undefined;
274
+ const httpStatus = (err) => isNeonApiError(err) ? err.status : undefined;
271
275
  /**
272
276
  * Fetch a project, turning the common failure modes into clear, actionable
273
277
  * errors. 401 is rethrown so the global handler can refresh credentials;
@@ -284,10 +288,10 @@ const fetchProjectOrThrow = async (props, projectId) => {
284
288
  throw err;
285
289
  }
286
290
  if (status === 403) {
287
- throw new LinkInputError(`You don't have access to project '${projectId}'. Check that your API key's account or organization can see it.`, 'NO_ACCESS');
291
+ throw new LinkInputError(`You don't have access to project '${projectId}'. Check that your API key's account or organization can see it.`, "NO_ACCESS");
288
292
  }
289
293
  if (status === 404) {
290
- throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`, 'NOT_FOUND');
294
+ throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`, "NOT_FOUND");
291
295
  }
292
296
  throw err;
293
297
  }
@@ -310,7 +314,7 @@ const verifyOrgAccess = async (props, orgId) => {
310
314
  throw err;
311
315
  }
312
316
  if (status === 403 || status === 404) {
313
- throw new LinkInputError(`Organization '${orgId}' not found, or your API key doesn't have access to it. Find your org ID in the Neon Console under Settings.`, status === 403 ? 'NO_ACCESS' : 'NOT_FOUND');
317
+ throw new LinkInputError(`Organization '${orgId}' not found, or your API key doesn't have access to it. Find your org ID in the Neon Console under Settings.`, status === 403 ? "NO_ACCESS" : "NOT_FOUND");
314
318
  }
315
319
  throw err;
316
320
  }
@@ -331,10 +335,10 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
331
335
  }
332
336
  const available = data.branches.length > 0
333
337
  ? data.branches
334
- .map((b) => `${b.id}${b.name ? ` (${b.name})` : ''}`)
335
- .join(', ')
336
- : '(none)';
337
- throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`neonctl checkout <branch>\`.`, 'NOT_FOUND');
338
+ .map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`)
339
+ .join(", ")
340
+ : "(none)";
341
+ throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`neonctl checkout <branch>\`.`, "NOT_FOUND");
338
342
  };
339
343
  /**
340
344
  * The value to persist for a branch: prefer its human-readable **name** (nicer
@@ -359,7 +363,7 @@ const resolveOrgForProject = async (props, inputs, existing, projectId) => {
359
363
  const projectOrg = project.org_id ?? undefined;
360
364
  if (inputs.orgId) {
361
365
  if (projectOrg && projectOrg !== inputs.orgId) {
362
- throw new LinkInputError(`Project '${projectId}' belongs to organization '${projectOrg}', not '${inputs.orgId}'. Omit --org-id to use the project's own org, or pass the matching ID.`, 'ORG_MISMATCH');
366
+ throw new LinkInputError(`Project '${projectId}' belongs to organization '${projectOrg}', not '${inputs.orgId}'. Omit --org-id to use the project's own org, or pass the matching ID.`, "ORG_MISMATCH");
363
367
  }
364
368
  if (!projectOrg) {
365
369
  await verifyOrgAccess(props, inputs.orgId);
@@ -399,12 +403,12 @@ const resolvePinnedBranch = async (props, inputs, existing, projectId) => {
399
403
  const runNonInteractive = async (props, inputs, existing) => {
400
404
  // Create a new project and link it.
401
405
  if (inputs.projectName) {
402
- const orgId = mustString(inputs.orgId, 'orgId');
406
+ const orgId = mustString(inputs.orgId, "orgId");
403
407
  await verifyOrgAccess(props, orgId);
404
408
  const created = await createProject(props, {
405
409
  orgId,
406
410
  name: inputs.projectName,
407
- regionId: mustString(inputs.regionId, 'regionId'),
411
+ regionId: mustString(inputs.regionId, "regionId"),
408
412
  });
409
413
  applyContext(props.contextFile, {
410
414
  orgId,
@@ -486,16 +490,16 @@ const runInteractive = async (props, inputs) => {
486
490
  }
487
491
  const orgResolution = await resolveOrg(props, inputs.orgId);
488
492
  let orgId;
489
- if (orgResolution.kind === 'resolved') {
493
+ if (orgResolution.kind === "resolved") {
490
494
  orgId = orgResolution.orgId;
491
495
  if (orgResolution.autoDetected) {
492
496
  log.info(`Detected organization ${orgId} from your existing projects (organization-scoped API key).`);
493
497
  }
494
498
  }
495
499
  else if (orgResolution.orgKeyLimited) {
496
- throw new Error('This API key is organization-scoped, so the CLI cannot list your organizations, ' +
497
- 'and no existing project was found in this org to auto-detect the ID. ' +
498
- 'Re-run with `--org-id <your_org_id>` (find it in the Neon Console under Settings).');
500
+ throw new Error("This API key is organization-scoped, so the CLI cannot list your organizations, " +
501
+ "and no existing project was found in this org to auto-detect the ID. " +
502
+ "Re-run with `--org-id <your_org_id>` (find it in the Neon Console under Settings).");
499
503
  }
500
504
  else {
501
505
  orgId = await promptOrgFromList(orgResolution.orgs);
@@ -511,7 +515,7 @@ const runInteractive = async (props, inputs) => {
511
515
  projectId: created.project.id,
512
516
  branch: created.branchName,
513
517
  });
514
- await finalizeLink(props, {
518
+ await finalizeInteractiveLink(props, {
515
519
  contextFile: props.contextFile,
516
520
  orgId,
517
521
  projectId: created.project.id,
@@ -525,14 +529,14 @@ const runInteractive = async (props, inputs) => {
525
529
  // Need to ask: existing project or create a new one?
526
530
  const projects = await listAllProjects(props, orgId);
527
531
  const action = await promptProjectChoice(projects, inputs.projectName);
528
- if (action.type === 'existing') {
532
+ if (action.type === "existing") {
529
533
  const branch = await resolveInteractiveBranch(props, action.projectId);
530
534
  applyContext(props.contextFile, {
531
535
  orgId,
532
536
  projectId: action.projectId,
533
537
  branch,
534
538
  });
535
- await finalizeLink(props, {
539
+ await finalizeInteractiveLink(props, {
536
540
  contextFile: props.contextFile,
537
541
  orgId,
538
542
  projectId: action.projectId,
@@ -555,7 +559,7 @@ const runInteractive = async (props, inputs) => {
555
559
  projectId: created.project.id,
556
560
  branch: created.branchName,
557
561
  });
558
- await finalizeLink(props, {
562
+ await finalizeInteractiveLink(props, {
559
563
  contextFile: props.contextFile,
560
564
  orgId,
561
565
  projectId: created.project.id,
@@ -572,13 +576,13 @@ const confirmRelinkIfNeeded = async (props) => {
572
576
  }
573
577
  const { proceed } = await prompts({
574
578
  onState: onPromptState,
575
- type: 'confirm',
576
- name: 'proceed',
579
+ type: "confirm",
580
+ name: "proceed",
577
581
  message: `${props.contextFile} is already linked to project ${existing.projectId} (org ${existing.orgId}). Re-link?`,
578
582
  initial: true,
579
583
  });
580
584
  if (!proceed) {
581
- process.stdout.write('Aborted. Existing link preserved.\n');
585
+ process.stdout.write("Aborted. Existing link preserved.\n");
582
586
  process.exit(0);
583
587
  }
584
588
  };
@@ -595,9 +599,9 @@ const promptOrgFromList = async (orgs) => {
595
599
  }
596
600
  const { orgId } = await prompts({
597
601
  onState: onPromptState,
598
- type: 'select',
599
- name: 'orgId',
600
- message: 'Which organization would you like to link?',
602
+ type: "select",
603
+ name: "orgId",
604
+ message: "Which organization would you like to link?",
601
605
  choices: orgs.map((org) => ({
602
606
  title: `${org.name} (${org.id})`,
603
607
  value: org.id,
@@ -608,7 +612,7 @@ const promptOrgFromList = async (orgs) => {
608
612
  };
609
613
  const promptProjectChoice = async (projects, suggestedName) => {
610
614
  const choices = [
611
- { title: '+ Create new project…', value: CREATE_NEW_SENTINEL },
615
+ { title: "+ Create new project…", value: CREATE_NEW_SENTINEL },
612
616
  ...projects.map((project) => ({
613
617
  title: `${project.name} (${project.id})`,
614
618
  value: project.id,
@@ -618,18 +622,18 @@ const promptProjectChoice = async (projects, suggestedName) => {
618
622
  // is one; with no projects to show, the create option (index 0) is the only choice.
619
623
  const { selection } = await prompts({
620
624
  onState: onPromptState,
621
- type: 'select',
622
- name: 'selection',
623
- message: 'Which project would you like to link?',
625
+ type: "select",
626
+ name: "selection",
627
+ message: "Which project would you like to link?",
624
628
  choices,
625
629
  initial: projects.length > 0 ? 1 : 0,
626
630
  });
627
631
  if (selection === CREATE_NEW_SENTINEL) {
628
- return { type: 'create', suggestedName };
632
+ return { type: "create", suggestedName };
629
633
  }
630
634
  const project = projects.find((p) => p.id === selection);
631
635
  return {
632
- type: 'existing',
636
+ type: "existing",
633
637
  projectId: selection,
634
638
  name: project?.name,
635
639
  regionId: project?.region_id,
@@ -638,11 +642,13 @@ const promptProjectChoice = async (projects, suggestedName) => {
638
642
  const promptProjectName = async (suggestedName) => {
639
643
  const { name } = await prompts({
640
644
  onState: onPromptState,
641
- type: 'text',
642
- name: 'name',
643
- message: 'Name for the new project:',
645
+ type: "text",
646
+ name: "name",
647
+ message: "Name for the new project:",
644
648
  initial: suggestedName,
645
- validate: (value) => value && value.trim().length > 0 ? true : 'Project name is required',
649
+ validate: (value) => value && value.trim().length > 0
650
+ ? true
651
+ : "Project name is required",
646
652
  });
647
653
  return String(name).trim();
648
654
  };
@@ -651,9 +657,9 @@ const promptRegion = async (props) => {
651
657
  const defaultIndex = Math.max(0, regions.findIndex((r) => r.default));
652
658
  const { regionId } = await prompts({
653
659
  onState: onPromptState,
654
- type: 'select',
655
- name: 'regionId',
656
- message: 'Which region should the new project run in?',
660
+ type: "select",
661
+ name: "regionId",
662
+ message: "Which region should the new project run in?",
657
663
  choices: regions.map((region) => ({
658
664
  title: `${region.name} (${region.region_id})`,
659
665
  value: region.region_id,
@@ -689,7 +695,7 @@ const runAgent = async (props, inputs) => {
689
695
  projectId,
690
696
  branch: pinnedBranch,
691
697
  });
692
- const orgSuffix = orgId ? ` (org ${orgId})` : '';
698
+ const orgSuffix = orgId ? ` (org ${orgId})` : "";
693
699
  if (pinnedBranch) {
694
700
  const pullNote = renderAgentPullNote(await autoPullEnvAfterPin({
695
701
  ...props,
@@ -698,7 +704,7 @@ const runAgent = async (props, inputs) => {
698
704
  envPull: props.envPull,
699
705
  }));
700
706
  emitAgent({
701
- status: 'linked',
707
+ status: "linked",
702
708
  context_file: props.contextFile,
703
709
  context: { orgId, projectId, branch: pinnedBranch },
704
710
  project: { id: projectId },
@@ -707,7 +713,7 @@ const runAgent = async (props, inputs) => {
707
713
  return;
708
714
  }
709
715
  emitAgent({
710
- status: 'linked',
716
+ status: "linked",
711
717
  context_file: props.contextFile,
712
718
  context: { orgId, projectId },
713
719
  project: { id: projectId },
@@ -716,7 +722,7 @@ const runAgent = async (props, inputs) => {
716
722
  return;
717
723
  }
718
724
  const orgResolution = await resolveOrg(props, inputs.orgId);
719
- if (orgResolution.kind === 'needs_selection') {
725
+ if (orgResolution.kind === "needs_selection") {
720
726
  emitAgent(buildNeedsOrgResponse(orgResolution));
721
727
  return;
722
728
  }
@@ -724,7 +730,7 @@ const runAgent = async (props, inputs) => {
724
730
  if (projectName && !regionId) {
725
731
  const regions = await fetchRegions(props);
726
732
  emitAgent({
727
- status: 'needs_project_details',
733
+ status: "needs_project_details",
728
734
  instruction: `Ask the user which region to create project "${projectName}" in. After they pick one, re-run the next_command_template with the chosen --region-id value.`,
729
735
  regions: regions.map((region) => ({
730
736
  id: region.region_id,
@@ -754,7 +760,7 @@ const runAgent = async (props, inputs) => {
754
760
  envPull: props.envPull,
755
761
  }));
756
762
  emitAgent({
757
- status: 'linked',
763
+ status: "linked",
758
764
  context_file: props.contextFile,
759
765
  context: {
760
766
  orgId,
@@ -776,9 +782,9 @@ const runAgent = async (props, inputs) => {
776
782
  const projects = await listAllProjects(props, orgId);
777
783
  const branchNote = branch
778
784
  ? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then \`neonctl checkout ${branch}\`.`
779
- : '';
785
+ : "";
780
786
  emitAgent({
781
- status: 'needs_project',
787
+ status: "needs_project",
782
788
  instruction: (projects.length === 0
783
789
  ? `Organization ${orgId} has no projects yet. Ask the user for a name for the new project, then re-run the create_option.next_command_template.`
784
790
  : `Ask the user whether to link to one of these ${projects.length} existing projects (use next_command_template with --project-id) or create a new project (use create_option.next_command_template).`) +
@@ -789,7 +795,7 @@ const runAgent = async (props, inputs) => {
789
795
  region_id: project.region_id,
790
796
  })),
791
797
  create_option: {
792
- instruction: 'To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.',
798
+ instruction: "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
793
799
  next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`,
794
800
  },
795
801
  next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
@@ -801,16 +807,13 @@ const emitAgent = (response) => {
801
807
  // ----------------------------------------------------------------------------
802
808
  // API helpers
803
809
  // ----------------------------------------------------------------------------
804
- const ORG_KEY_LIMITED_FRAGMENT = 'not allowed for organization API keys';
810
+ const ORG_KEY_LIMITED_FRAGMENT = "not allowed for organization API keys";
805
811
  const isOrgKeyLimitedError = (err) => {
806
- if (!isAxiosError(err))
812
+ if (!isNeonApiError(err))
807
813
  return false;
808
- const data = err.response?.data;
809
- if (data === undefined || data === null || typeof data !== 'object') {
810
- return false;
811
- }
812
- const message = data.message;
813
- return (typeof message === 'string' && message.includes(ORG_KEY_LIMITED_FRAGMENT));
814
+ const message = messageFromBody(err.data);
815
+ return (typeof message === "string" &&
816
+ message.includes(ORG_KEY_LIMITED_FRAGMENT));
814
817
  };
815
818
  const fetchOrganizations = async (props) => {
816
819
  const { data } = await props.apiClient.getCurrentUserOrganizations();
@@ -827,23 +830,23 @@ const fetchOrganizations = async (props) => {
827
830
  */
828
831
  const resolveOrg = async (props, given) => {
829
832
  if (given) {
830
- return { kind: 'resolved', orgId: given, autoDetected: false };
833
+ return { kind: "resolved", orgId: given, autoDetected: false };
831
834
  }
832
835
  try {
833
836
  const orgs = await fetchOrganizations(props);
834
- return { kind: 'needs_selection', orgs, orgKeyLimited: false };
837
+ return { kind: "needs_selection", orgs, orgKeyLimited: false };
835
838
  }
836
839
  catch (err) {
837
840
  if (!isOrgKeyLimitedError(err)) {
838
841
  throw err;
839
842
  }
840
- log.debug('getCurrentUserOrganizations not allowed (org-scoped API key); attempting to derive org from existing projects.');
843
+ log.debug("getCurrentUserOrganizations not allowed (org-scoped API key); attempting to derive org from existing projects.");
841
844
  }
842
845
  const detected = await detectOrgIdFromProjects(props);
843
846
  if (detected) {
844
- return { kind: 'resolved', orgId: detected, autoDetected: true };
847
+ return { kind: "resolved", orgId: detected, autoDetected: true };
845
848
  }
846
- return { kind: 'needs_selection', orgs: [], orgKeyLimited: true };
849
+ return { kind: "needs_selection", orgs: [], orgKeyLimited: true };
847
850
  };
848
851
  const detectOrgIdFromProjects = async (props) => {
849
852
  try {
@@ -852,63 +855,64 @@ const detectOrgIdFromProjects = async (props) => {
852
855
  }
853
856
  catch (err) {
854
857
  const message = err instanceof Error ? err.message : String(err);
855
- log.debug('detectOrgIdFromProjects failed: %s', message);
858
+ log.debug("detectOrgIdFromProjects failed: %s", message);
856
859
  return undefined;
857
860
  }
858
861
  };
859
862
  const buildNeedsOrgResponse = (resolution) => {
860
863
  if (resolution.orgKeyLimited) {
861
864
  return {
862
- status: 'needs_org',
865
+ status: "needs_org",
863
866
  instruction: "This Neon API key is organization-scoped, so the CLI cannot list the user's organizations and no existing project was found to auto-detect the org ID. Ask the user for their Neon organization ID (visible in the Neon Console under the org's Settings page, formatted like `org-bitter-breeze-12345678`) and re-run the next_command_template with that --org-id.",
864
867
  options: [],
865
- next_command_template: 'neonctl link --agent --org-id <org_id>',
868
+ next_command_template: "neonctl link --agent --org-id <org_id>",
866
869
  };
867
870
  }
868
871
  const orgs = resolution.orgs;
869
872
  return {
870
- status: 'needs_org',
873
+ status: "needs_org",
871
874
  instruction: orgs.length === 0
872
- ? 'The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking.'
873
- : `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ? '' : 's'} they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.`,
875
+ ? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking."
876
+ : `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ? "" : "s"} they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.`,
874
877
  options: orgs.map((org) => ({ id: org.id, name: org.name })),
875
- next_command_template: 'neonctl link --agent --org-id <org_id>',
878
+ next_command_template: "neonctl link --agent --org-id <org_id>",
876
879
  };
877
880
  };
878
881
  const toAgentError = (err) => {
879
882
  if (err instanceof LinkInputError) {
880
- return { status: 'error', code: err.agentCode, message: err.message };
881
- }
882
- if (isAxiosError(err)) {
883
- const status = err.response?.status;
884
- const data = err.response?.data;
885
- const apiMessage = typeof data === 'object' && data !== null
886
- ? data.message
887
- : undefined;
888
- const message = typeof apiMessage === 'string' && apiMessage.length > 0
883
+ return { status: "error", code: err.agentCode, message: err.message };
884
+ }
885
+ if (isNeonApiError(err)) {
886
+ const status = err.status;
887
+ const apiMessage = messageFromBody(err.data);
888
+ const message = apiMessage !== undefined && apiMessage.length > 0
889
889
  ? apiMessage
890
890
  : err.message;
891
- let code = 'API_ERROR';
891
+ let code = "API_ERROR";
892
892
  if (status === 401 || status === 403) {
893
- code = 'AUTH_ERROR';
893
+ code = "AUTH_ERROR";
894
894
  }
895
895
  else if (status !== undefined && status >= 400 && status < 500) {
896
- code = 'CLIENT_ERROR';
896
+ code = "CLIENT_ERROR";
897
897
  }
898
898
  else if (status !== undefined && status >= 500) {
899
- code = 'SERVER_ERROR';
899
+ code = "SERVER_ERROR";
900
900
  }
901
- else if (err.code === 'ECONNABORTED') {
902
- code = 'TIMEOUT';
901
+ else if (err.code === "ECONNABORTED") {
902
+ code = "TIMEOUT";
903
903
  }
904
- return { status: 'error', code, message };
904
+ return { status: "error", code, message };
905
905
  }
906
906
  if (err instanceof Error) {
907
- return { status: 'error', code: 'INTERNAL_ERROR', message: err.message };
907
+ return {
908
+ status: "error",
909
+ code: "INTERNAL_ERROR",
910
+ message: err.message,
911
+ };
908
912
  }
909
913
  return {
910
- status: 'error',
911
- code: 'INTERNAL_ERROR',
914
+ status: "error",
915
+ code: "INTERNAL_ERROR",
912
916
  message: String(err),
913
917
  };
914
918
  };
@@ -948,11 +952,11 @@ const resolveInteractiveBranch = async (props, projectId) => {
948
952
  return branchPersistValue(only);
949
953
  }
950
954
  const picked = await pickBranchInteractively(branches, {
951
- message: 'Which branch would you like to link?',
952
- nonInteractiveMessage: 'No branch could be selected without an interactive terminal. ' +
953
- 'Re-run `neonctl link` interactively, or `neonctl checkout <branch>` to pin one.',
955
+ message: "Which branch would you like to link?",
956
+ nonInteractiveMessage: "No branch could be selected without an interactive terminal. " +
957
+ "Re-run `neonctl link` interactively, or `neonctl checkout <branch>` to pin one.",
954
958
  });
955
- if (picked.kind === 'existing') {
959
+ if (picked.kind === "existing") {
956
960
  const existing = branches.find((b) => b.id === picked.branchId);
957
961
  return existing ? branchPersistValue(existing) : picked.branchId;
958
962
  }
@@ -968,12 +972,12 @@ const fetchRegions = async (props) => {
968
972
  }
969
973
  }
970
974
  catch (err) {
971
- if (isAxiosError(err)) {
972
- log.debug('getActiveRegions failed (%s), falling back to the static region list.', err.response?.status ?? err.code ?? err.message);
975
+ if (isNeonApiError(err)) {
976
+ log.debug("getActiveRegions failed (%s), falling back to the static region list.", err.status ?? err.code ?? err.message);
973
977
  }
974
978
  else {
975
979
  const message = err instanceof Error ? err.message : String(err);
976
- log.debug('getActiveRegions failed (%s), falling back to the static region list.', message);
980
+ log.debug("getActiveRegions failed (%s), falling back to the static region list.", message);
977
981
  }
978
982
  }
979
983
  return staticRegionsFallback();
@@ -981,9 +985,9 @@ const fetchRegions = async (props) => {
981
985
  const staticRegionsFallback = () => REGIONS.map((id) => ({
982
986
  region_id: id,
983
987
  name: id,
984
- default: id === 'aws-us-east-2',
985
- geo_lat: '',
986
- geo_long: '',
988
+ default: id === "aws-us-east-2",
989
+ geo_lat: "",
990
+ geo_long: "",
987
991
  }));
988
992
  const createProject = async (props, args) => {
989
993
  const project = {
@@ -994,7 +998,7 @@ const createProject = async (props, args) => {
994
998
  };
995
999
  const { data } = await props.apiClient.createProject({ project });
996
1000
  if (!data.branch?.id) {
997
- throw new Error('Project was created but the API response did not include a default branch id.');
1001
+ throw new Error("Project was created but the API response did not include a default branch id.");
998
1002
  }
999
1003
  return {
1000
1004
  project: {
@@ -1009,9 +1013,9 @@ const createProject = async (props, args) => {
1009
1013
  const printSummary = (_props, summary) => {
1010
1014
  const lines = [];
1011
1015
  if (summary.created) {
1012
- lines.push(`Created project ${summary.projectId}${summary.projectName ? ` ("${summary.projectName}")` : ''}${summary.regionId ? ` in ${summary.regionId}` : ''}.`);
1016
+ lines.push(`Created project ${summary.projectId}${summary.projectName ? ` ("${summary.projectName}")` : ""}${summary.regionId ? ` in ${summary.regionId}` : ""}.`);
1013
1017
  }
1014
- lines.push(`${summary.orgOnly ? 'Updated' : 'Linked'} ${summary.contextFile}:`);
1018
+ lines.push(`${summary.orgOnly ? "Updated" : "Linked"} ${summary.contextFile}:`);
1015
1019
  if (summary.orgId) {
1016
1020
  lines.push(` orgId: ${summary.orgId}`);
1017
1021
  }
@@ -1022,15 +1026,15 @@ const printSummary = (_props, summary) => {
1022
1026
  lines.push(` branch: ${summary.branch}`);
1023
1027
  }
1024
1028
  if (summary.noChecks) {
1025
- lines.push('');
1026
- lines.push('Written offline (--no-checks): nothing was verified.');
1029
+ lines.push("");
1030
+ lines.push("Written offline (--no-checks): nothing was verified.");
1027
1031
  }
1028
1032
  else if (summary.projectId && !summary.branch && !summary.orgOnly) {
1029
- lines.push('');
1030
- lines.push('No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.');
1033
+ lines.push("");
1034
+ lines.push("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
1031
1035
  }
1032
- lines.push('');
1033
- process.stdout.write(`${lines.join('\n')}\n`);
1036
+ lines.push("");
1037
+ process.stdout.write(`${lines.join("\n")}\n`);
1034
1038
  };
1035
1039
  /**
1036
1040
  * Print the link summary, then run the bundled `env pull` so a human `link` that pinned a
@@ -1050,10 +1054,56 @@ const finalizeLink = async (props, summary) => {
1050
1054
  envPull: props.envPull,
1051
1055
  });
1052
1056
  };
1057
+ /**
1058
+ * Interactive `link` finalize: the shared {@link finalizeLink} (summary + env
1059
+ * pull), then — as the last step — offer to manage the project's Neon setup as
1060
+ * code with a `neon.ts`. Kept out of {@link finalizeLink} so the non-interactive
1061
+ * paths never prompt.
1062
+ */
1063
+ const finalizeInteractiveLink = async (props, summary) => {
1064
+ await finalizeLink(props, summary);
1065
+ await maybeOfferConfigInit(props, summary);
1066
+ };
1067
+ /**
1068
+ * Offer to set up infrastructure-as-code at the end of an interactive `link` —
1069
+ * the natural moment, since the project is now linked. Skipped when the project
1070
+ * already has a `neon.ts` (nothing to scaffold). On yes, `config init` writes the
1071
+ * starter `neon.ts` and installs the config packages, then env is pulled again so
1072
+ * the local `.env` reflects the policy — the same pull `link` runs when a project
1073
+ * already ships a `neon.ts`.
1074
+ */
1075
+ const maybeOfferConfigInit = async (props, summary) => {
1076
+ const cwd = process.cwd();
1077
+ if (hasNeonConfigFile(cwd)) {
1078
+ return;
1079
+ }
1080
+ const { value } = await prompts({
1081
+ onState: onPromptState,
1082
+ type: "confirm",
1083
+ name: "value",
1084
+ message: "Manage this project's Neon setup as code? Adds a neon.ts you can edit and apply with `neon config apply`.",
1085
+ initial: true,
1086
+ });
1087
+ if (value !== true) {
1088
+ return;
1089
+ }
1090
+ await initCmd({ cwd, install: true });
1091
+ // The neon.ts (and its deps) now exist — pull env again so the local .env
1092
+ // reflects the policy, matching how `link` pulls when a project already ships
1093
+ // a neon.ts. Only meaningful when a branch was pinned (same guard as finalize).
1094
+ if (summary.branch && summary.projectId) {
1095
+ await autoPullEnvAfterPin({
1096
+ ...props,
1097
+ projectId: summary.projectId,
1098
+ branch: summary.branch,
1099
+ envPull: props.envPull,
1100
+ });
1101
+ }
1102
+ };
1053
1103
  const onPromptState = (state) => {
1054
1104
  if (state.aborted) {
1055
- process.stdout.write('\x1B[?25h');
1056
- process.stdout.write('\n');
1105
+ process.stdout.write("\x1B[?25h");
1106
+ process.stdout.write("\n");
1057
1107
  process.exit(1);
1058
1108
  }
1059
1109
  };