neon 7.0.3 → 7.0.5

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.
@@ -6,11 +6,12 @@ import { branchCreateRequest } from "../parameters.gen.js";
6
6
  import { writer } from "../writer.js";
7
7
  import { EndpointType } from "../utils/api_enums.js";
8
8
  import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp } from "../utils/formats.js";
9
- import { branchIdFromProps, branchIdResolve, fillSingleProject } from "../utils/enrichers.js";
9
+ import { branchIdFromProps, fillSingleProject, resolveBranchFromProps } from "../utils/enrichers.js";
10
10
  import { getComputeUnits } from "../utils/compute_units.js";
11
11
  import { psql } from "../utils/psql.js";
12
12
  import { parsePointInTime } from "../utils/point_in_time.js";
13
13
  import { parseSchemaDiffParams, schemaDiff } from "./schema_diff.js";
14
+ import chalk from "chalk";
14
15
  //#region src/commands/branches.ts
15
16
  var branches_exports = /* @__PURE__ */ __exportAll({
16
17
  BRANCH_FIELDS: () => BRANCH_FIELDS,
@@ -27,14 +28,64 @@ const BRANCH_FIELDS = [
27
28
  "expires_at",
28
29
  "created_at"
29
30
  ];
30
- const BRANCH_FIELDS_RESET = [
31
+ const BRANCH_FIELDS_RESET = [...BRANCH_FIELDS, "last_reset_at"];
32
+ const BRANCH_DETAIL_FIELDS = [
31
33
  "name",
32
34
  "id",
33
- "default",
35
+ "parent_id",
34
36
  "current_state",
37
+ "expires_at",
35
38
  "created_at",
36
- "last_reset_at"
39
+ "created_by",
40
+ "last_reset_at",
41
+ "logical_size"
37
42
  ];
43
+ /**
44
+ * The branch `.neon` pins, by name (preferred) or id, when `.neon` links this same project;
45
+ * a `--project-id` pointing elsewhere makes its branch not current.
46
+ */
47
+ const currentBranchOf = (props) => {
48
+ const context = readContextFile(props.contextFile);
49
+ return context.projectId === props.projectId ? contextBranch(context) : void 0;
50
+ };
51
+ const branchNameCell = (br, labels) => {
52
+ const tags = [];
53
+ if (br.default) tags.push(chalk.cyan("[default]"));
54
+ if (br.protected) tags.push(chalk.yellow("[protected]"));
55
+ if (labels.annotations?.[br.id]?.value.anonymized) tags.push(chalk.dim("[anon]"));
56
+ if (labels.current !== void 0 && (br.id === labels.current || br.name === labels.current)) tags.push(chalk.green("[current]"));
57
+ tags.push(br.name);
58
+ return tags.join(" ");
59
+ };
60
+ const stateCell = (state) => {
61
+ if (!state) return "";
62
+ if (state === "ready") return chalk.green(state);
63
+ if (state === "init" || state === "resetting") return chalk.yellow(state);
64
+ return chalk.dim(state);
65
+ };
66
+ const formatBytes = (bytes) => {
67
+ const units = [
68
+ "bytes",
69
+ "KiB",
70
+ "MiB",
71
+ "GiB",
72
+ "TiB"
73
+ ];
74
+ let value = bytes;
75
+ let unit = 0;
76
+ while (value >= 1024 && unit < units.length - 1) {
77
+ value /= 1024;
78
+ unit++;
79
+ }
80
+ return unit === 0 ? `${bytes} bytes` : `${value.toFixed(1)} ${units[unit]}`;
81
+ };
82
+ const branchColumns = (labels) => ({
83
+ name: (br) => branchNameCell(br, labels),
84
+ current_state: (br) => stateCell(br.current_state),
85
+ expires_at: (br) => br.expires_at || "never",
86
+ created_by: (br) => br.created_by?.name ?? "",
87
+ logical_size: (br) => br.logical_size === void 0 ? "" : formatBytes(br.logical_size)
88
+ });
38
89
  const command = "branches";
39
90
  const describe = "Manage branches";
40
91
  const aliases = ["branch"];
@@ -171,22 +222,12 @@ const handler = (args) => {
171
222
  };
172
223
  const list = async (props) => {
173
224
  const { data: { branches, annotations } } = await props.apiClient.listProjectBranches({ projectId: props.projectId });
174
- const currentBranch = contextBranch(readContextFile(props.contextFile));
175
225
  writer(props).end(branches, {
176
226
  fields: BRANCH_FIELDS,
177
- renderColumns: {
178
- expires_at: (br) => br.expires_at || "never",
179
- name: (br) => {
180
- const isAnon = annotations[br.id]?.value.anonymized;
181
- const labels = [];
182
- if (br.default) labels.push("[default]");
183
- if (br.protected) labels.push("[protected]");
184
- if (isAnon) labels.push("[anon]");
185
- if (currentBranch !== void 0 && (br.id === currentBranch || br.name === currentBranch)) labels.push("[current]");
186
- labels.push(br.name);
187
- return labels.join(" ");
188
- }
189
- }
227
+ renderColumns: branchColumns({
228
+ annotations,
229
+ current: currentBranchOf(props)
230
+ })
190
231
  });
191
232
  };
192
233
  const create = async (props) => {
@@ -237,16 +278,20 @@ const create = async (props) => {
237
278
  out.write(data.branch, {
238
279
  fields: BRANCH_FIELDS,
239
280
  title: "branch",
240
- emptyMessage: "No branches have been found."
281
+ humanTitle: "Branch",
282
+ emptyMessage: "No branches have been found.",
283
+ renderColumns: branchColumns({ current: currentBranchOf(props) })
241
284
  });
242
285
  if (writeEndpoints) out.write(endpoints, {
243
286
  fields: ["id", "created_at"],
244
287
  title: "endpoints",
288
+ humanTitle: "Compute",
245
289
  emptyMessage: "No endpoints have been found."
246
290
  });
247
291
  if (writeUris) out.write(connectionUris, {
248
292
  fields: ["connection_uri"],
249
293
  title: "connection_uris",
294
+ humanTitle: "Connection string",
250
295
  emptyMessage: "No connection uris have been found"
251
296
  });
252
297
  }
@@ -259,24 +304,54 @@ const create = async (props) => {
259
304
  }
260
305
  };
261
306
  const rename = async (props) => {
262
- const branchId = await branchIdFromProps(props);
307
+ const { branchId, listing } = await resolveBranchFromProps(props);
263
308
  const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, { branch: { name: props.newName } }));
264
- writer(props).end(data.branch, { fields: BRANCH_FIELDS });
309
+ writer(props).end(data.branch, {
310
+ fields: BRANCH_FIELDS,
311
+ renderColumns: branchColumns({
312
+ annotations: listing?.annotations,
313
+ current: currentBranchOf(props)
314
+ })
315
+ });
265
316
  };
266
317
  const setDefault = async (props) => {
267
- const branchId = await branchIdFromProps(props);
318
+ const { branchId, listing } = await resolveBranchFromProps(props);
268
319
  const { data } = await retryOnLock(() => props.apiClient.setDefaultProjectBranch(props.projectId, branchId));
269
- writer(props).end(data.branch, { fields: BRANCH_FIELDS });
320
+ writer(props).end(data.branch, {
321
+ fields: BRANCH_FIELDS,
322
+ renderColumns: branchColumns({
323
+ annotations: listing?.annotations,
324
+ current: currentBranchOf(props)
325
+ })
326
+ });
270
327
  };
271
328
  const deleteBranch = async (props) => {
272
- const branchId = await branchIdFromProps(props);
329
+ const { branchId, listing } = await resolveBranchFromProps(props);
273
330
  const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.projectId, branchId));
274
- if (data) writer(props).end(data.branch, { fields: BRANCH_FIELDS });
331
+ if (data) writer(props).end(data.branch, {
332
+ fields: BRANCH_FIELDS,
333
+ renderColumns: branchColumns({
334
+ annotations: listing?.annotations,
335
+ current: currentBranchOf(props)
336
+ })
337
+ });
275
338
  };
276
339
  const get = async (props) => {
277
- const branchId = await branchIdFromProps(props);
278
- const { data } = await props.apiClient.getProjectBranch(props.projectId, branchId);
279
- writer(props).end(data.branch, { fields: BRANCH_FIELDS });
340
+ const resolved = await resolveBranchFromProps(props);
341
+ let branch = resolved.branch;
342
+ let annotations = resolved.listing?.annotations;
343
+ if (!branch) {
344
+ const { data } = await props.apiClient.getProjectBranch(props.projectId, resolved.branchId);
345
+ branch = data.branch;
346
+ annotations = data.annotation ? { [data.branch.id]: data.annotation } : void 0;
347
+ }
348
+ writer(props).end(branch, {
349
+ fields: BRANCH_DETAIL_FIELDS,
350
+ renderColumns: branchColumns({
351
+ annotations,
352
+ current: currentBranchOf(props)
353
+ })
354
+ });
280
355
  };
281
356
  const addCompute = async (props) => {
282
357
  const branchId = await branchIdFromProps(props);
@@ -291,34 +366,50 @@ const addCompute = async (props) => {
291
366
  };
292
367
  const reset = async (props) => {
293
368
  if (!props.parent) throw new Error("Only resetting to parent is supported for now");
294
- const branchId = await branchIdFromProps(props);
295
- const { data: { branch: { parent_id } } } = await props.apiClient.getProjectBranch(props.projectId, branchId);
369
+ const { branchId, branch, listing } = await resolveBranchFromProps(props);
370
+ const parent_id = branch?.parent_id ?? (await props.apiClient.getProjectBranch(props.projectId, branchId)).data.branch.parent_id;
296
371
  if (!parent_id) throw new Error("Branch has no parent");
297
372
  const { data } = await retryOnLock(() => props.apiClient.restoreProjectBranch(props.projectId, branchId, {
298
373
  source_branch_id: parent_id,
299
374
  preserve_under_name: props.preserveUnderName || void 0
300
375
  }));
301
- writer(props).end(data.branch, { fields: BRANCH_FIELDS_RESET });
376
+ writer(props).end(data.branch, {
377
+ fields: BRANCH_FIELDS_RESET,
378
+ renderColumns: branchColumns({
379
+ annotations: listing?.annotations,
380
+ current: currentBranchOf(props)
381
+ })
382
+ });
302
383
  };
303
384
  const restore = async (props) => {
304
- const targetBranchId = await branchIdResolve({
305
- branch: props.id,
385
+ const { branchId: targetBranchId, branch: targetBranch, listing } = await resolveBranchFromProps({
386
+ apiClient: props.apiClient,
306
387
  projectId: props.projectId,
307
- apiClient: props.apiClient
388
+ id: props.id
308
389
  });
309
390
  const pointInTime = await parsePointInTime({
310
391
  pointInTime: props.pointInTime,
311
392
  targetBranchId,
312
393
  projectId: props.projectId,
313
- api: props.apiClient
394
+ api: props.apiClient,
395
+ branches: listing?.branches,
396
+ targetBranch
314
397
  });
315
- log.info(`Restoring branch ${targetBranchId} to the branch ${pointInTime.branchId} ${pointInTime.tag === "lsn" && "LSN " + pointInTime.lsn || pointInTime.tag === "timestamp" && "timestamp " + pointInTime.timestamp || "head"}`);
398
+ if (props.output === "table") {
399
+ const nameOf = (id) => listing?.branches.find((b) => b.id === id)?.name ?? id;
400
+ const point = pointInTime.tag === "lsn" ? `LSN ${pointInTime.lsn} of` : pointInTime.tag === "timestamp" ? `timestamp ${pointInTime.timestamp} of` : "the head of";
401
+ log.info(`Restoring ${nameOf(targetBranchId)} to ${point} ${nameOf(pointInTime.branchId)}`);
402
+ } else log.info(`Restoring branch ${targetBranchId} to the branch ${pointInTime.branchId} ${pointInTime.tag === "lsn" && "LSN " + pointInTime.lsn || pointInTime.tag === "timestamp" && "timestamp " + pointInTime.timestamp || "head"}`);
316
403
  const { data } = await retryOnLock(() => props.apiClient.restoreProjectBranch(props.projectId, targetBranchId, {
317
404
  source_branch_id: pointInTime.branchId,
318
405
  preserve_under_name: props.preserveUnderName || void 0,
319
406
  ...pointInTime.tag === "lsn" && { source_lsn: pointInTime.lsn },
320
407
  ...pointInTime.tag === "timestamp" && { source_timestamp: pointInTime.timestamp }
321
408
  }));
409
+ const labels = {
410
+ annotations: listing?.annotations,
411
+ current: currentBranchOf(props)
412
+ };
322
413
  const writeInst = writer(props).write(data.branch, {
323
414
  title: "Restored branch",
324
415
  fields: [
@@ -326,7 +417,8 @@ const restore = async (props) => {
326
417
  "name",
327
418
  "last_reset_at"
328
419
  ],
329
- emptyMessage: "No branches have been restored."
420
+ emptyMessage: "No branches have been restored.",
421
+ renderColumns: branchColumns(labels)
330
422
  });
331
423
  const parentId = data.branch.parent_id;
332
424
  if (props.preserveUnderName && parentId) {
@@ -334,19 +426,26 @@ const restore = async (props) => {
334
426
  writeInst.write(data.branch, {
335
427
  title: "Backup branch",
336
428
  fields: ["id", "name"],
337
- emptyMessage: "Backup branch has not been found."
429
+ emptyMessage: "Backup branch has not been found.",
430
+ renderColumns: branchColumns(labels)
338
431
  });
339
432
  }
340
433
  writeInst.end();
341
434
  };
342
435
  const setExpiration = async (props) => {
343
- const branchId = await branchIdFromProps({
436
+ const { branchId, listing } = await resolveBranchFromProps({
344
437
  ...props,
345
438
  id: props.branchId
346
439
  });
347
440
  const expiresAt = typeof props.expiresAt === "string" ? new Date(props.expiresAt).toISOString() : null;
348
441
  const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, { branch: { expires_at: expiresAt } }));
349
- writer(props).end(data.branch, { fields: BRANCH_FIELDS });
442
+ writer(props).end(data.branch, {
443
+ fields: BRANCH_FIELDS,
444
+ renderColumns: branchColumns({
445
+ annotations: listing?.annotations,
446
+ current: currentBranchOf(props)
447
+ })
448
+ });
350
449
  };
351
450
  //#endregion
352
451
  export { BRANCH_FIELDS, aliases, builder, command, describe, handler, branches_exports as t };
@@ -85,17 +85,25 @@ const handler = async (props) => {
85
85
  };
86
86
  if (props.branch) props.branch = parsedPIT.branch;
87
87
  const branchId = await branchIdFromProps(props);
88
- const { data: { endpoints } } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
88
+ const endpointsRequest = props.apiClient.listProjectBranchEndpoints(projectId, branchId);
89
+ const rolesRequest = props.roleName ? void 0 : props.apiClient.listProjectBranchRoles(projectId, branchId);
90
+ const databasesRequest = props.apiClient.listProjectBranchDatabases(projectId, branchId);
91
+ for (const request of [
92
+ endpointsRequest,
93
+ rolesRequest,
94
+ databasesRequest
95
+ ]) request?.catch(() => void 0);
96
+ const { data: { endpoints } } = await endpointsRequest;
89
97
  const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
90
98
  let endpoint = endpoints.find((e) => e.type === matchEndpointType);
91
99
  if (!endpoint && props.endpointType == null) endpoint = endpoints[0];
92
100
  if (!endpoint) throw new Error(`No ${props.endpointType ?? ""} endpoint found for the branch: ${branchId}`);
93
- const role = props.roleName || await props.apiClient.listProjectBranchRoles(projectId, branchId).then(({ data }) => {
101
+ const role = !rolesRequest ? props.roleName : await rolesRequest.then(({ data }) => {
94
102
  if (data.roles.length === 0) throw new Error(`No roles found for the branch: ${branchId}`);
95
103
  if (data.roles.length === 1) return data.roles[0].name;
96
104
  throw new Error(`Multiple roles found for the branch, please provide one with the --role-name option: ${data.roles.map((r) => r.name).join(", ")}`);
97
105
  });
98
- const { data: { databases: branchDatabases } } = await props.apiClient.listProjectBranchDatabases(projectId, branchId);
106
+ const { data: { databases: branchDatabases } } = await databasesRequest;
99
107
  const database = props.databaseName || (() => {
100
108
  if (branchDatabases.length === 0) throw new Error(`No databases found for the branch: ${branchId}`);
101
109
  if (branchDatabases.length === 1) return branchDatabases[0].name;
@@ -2,7 +2,7 @@ import { log } from "../log.js";
2
2
  import { isNeonApiError, messageFromBody } from "../api.js";
3
3
  import { sendError } from "../analytics.js";
4
4
  import { writer } from "../writer.js";
5
- import { branchIdFromProps, listAllProjectBranches } from "../utils/enrichers.js";
5
+ import { listAllProjectBranchesWithAnnotations, resolveBranchFromProps } from "../utils/enrichers.js";
6
6
  import { parsePointInTime } from "../utils/point_in_time.js";
7
7
  import chalk from "chalk";
8
8
  import { createPatch } from "diff";
@@ -15,12 +15,14 @@ const COLORS = {
15
15
  };
16
16
  const schemaDiff = async (props) => {
17
17
  props.branch = props.baseBranch || props.branch;
18
- const baseBranch = await branchIdFromProps(props);
18
+ const { branchId: baseBranch, branch: baseBranchData, listing } = await resolveBranchFromProps(props, props.branchListing);
19
19
  let pointInTime = await parsePointInTime({
20
20
  pointInTime: props.compareSource,
21
21
  targetBranchId: baseBranch,
22
22
  projectId: props.projectId,
23
- api: props.apiClient
23
+ api: props.apiClient,
24
+ branches: listing?.branches,
25
+ targetBranch: baseBranchData
24
26
  });
25
27
  const comparingWithParent = props.compareSource.startsWith("^parent");
26
28
  let baseBranchPoint = {
@@ -32,21 +34,35 @@ const schemaDiff = async (props) => {
32
34
  if (props.database) {
33
35
  const database = baseDatabases.find((db) => db.name === props.database);
34
36
  if (!database) throw new Error(`Database ${props.database} does not exist in base branch ${baseBranch}`);
35
- const patch = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
36
- writer(props).text(colorize(patch));
37
+ const diff = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
38
+ writer(props).text(colorize(diff.patch));
39
+ reportUnchanged(diff, [baseBranchPoint, pointInTime], database, props, listing);
37
40
  return;
38
41
  }
39
42
  await Promise.all(baseDatabases.map(async (database) => {
40
- const patch = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
41
- writer(props).text(colorize(patch));
43
+ const diff = await createSchemaDiff(baseBranchPoint, pointInTime, database, props);
44
+ writer(props).text(colorize(diff.patch));
45
+ reportUnchanged(diff, [baseBranchPoint, pointInTime], database, props, listing);
42
46
  }));
43
47
  };
48
+ /** The patch alone is two header lines when nothing differs, which reads like missing output. */
49
+ const reportUnchanged = (diff, [base, compare], database, props, listing) => {
50
+ if (!diff.unchanged || props.output !== "table") return;
51
+ const label = (point) => {
52
+ const name = listing?.branches.find((b) => b.id === point.branchId)?.name ?? point.branchId;
53
+ return point.tag === "lsn" ? `${name} at ${point.lsn}` : point.tag === "timestamp" ? `${name} at ${point.timestamp}` : name;
54
+ };
55
+ log.info(`No schema differences for database ${database.name} between ${label(base)} and ${label(compare)}.`);
56
+ };
44
57
  const fetchDatabases = async (branch, props) => {
45
58
  return props.apiClient.listProjectBranchDatabases(props.projectId, branch).then((response) => response.data.databases);
46
59
  };
47
60
  const createSchemaDiff = async (baseBranch, pointInTime, database, props) => {
48
61
  const [baseSchema, compareSchema] = await Promise.all([fetchSchema(baseBranch, database, props), fetchSchema(pointInTime, database, props)]);
49
- return createPatch(`Database: ${database.name}`, baseSchema, compareSchema, generateHeader(baseBranch), generateHeader(pointInTime));
62
+ return {
63
+ patch: createPatch(`Database: ${database.name}`, baseSchema, compareSchema, generateHeader(baseBranch), generateHeader(pointInTime)),
64
+ unchanged: baseSchema === compareSchema
65
+ };
50
66
  };
51
67
  const fetchSchema = async (pointInTime, database, props) => {
52
68
  try {
@@ -68,6 +84,7 @@ const colorize = (patch) => {
68
84
  return patch.replace(/^([^\n]+)\n([^\n]+)\n/m, "").replace(/^-.*/gm, colorizer("removed")).replace(/^\+.*/gm, colorizer("added")).replace(/^@@.+@@.*/gm, colorizer("section"));
69
85
  };
70
86
  const colorizer = (colorId) => {
87
+ if (process.env.NO_COLOR) return (line) => line;
71
88
  const color = COLORS[colorId];
72
89
  return (line) => color(line);
73
90
  };
@@ -92,11 +109,13 @@ const parseSchemaDiffParams = async (props) => {
92
109
  props.compareSource = props.baseBranch;
93
110
  props.baseBranch = props.branch;
94
111
  } else if (props.branch) {
95
- if ((await listAllProjectBranches(props.apiClient, props.projectId)).find((b) => b.id === props.branch || b.name === props.branch)?.parent_id == void 0) throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
112
+ props.branchListing = await listAllProjectBranchesWithAnnotations(props.apiClient, props.projectId);
113
+ if (props.branchListing.branches.find((b) => b.id === props.branch || b.name === props.branch)?.parent_id == void 0) throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
96
114
  log.info(`No branches specified. Comparing your context branch '${props.branch}' with its parent`);
97
115
  props.compareSource = "^parent";
98
116
  } else {
99
- if ((await listAllProjectBranches(props.apiClient, props.projectId)).find((b) => b.default)?.parent_id == void 0) 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.");
117
+ props.branchListing = await listAllProjectBranchesWithAnnotations(props.apiClient, props.projectId);
118
+ if (props.branchListing.branches.find((b) => b.default)?.parent_id == void 0) 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.");
100
119
  log.info(`No branches specified. Comparing default branch with its parent`);
101
120
  props.compareSource = "^parent";
102
121
  }
@@ -22,8 +22,11 @@ function parseColumns(value) {
22
22
  if (!Number.isFinite(n) || n <= 0) return;
23
23
  return n;
24
24
  }
25
+ function stripAnsi(s) {
26
+ return s.replace(ANSI_RE, "");
27
+ }
25
28
  function displayWidth(s) {
26
- const plain = s.replace(ANSI_RE, "");
29
+ const plain = stripAnsi(s);
27
30
  let width = 0;
28
31
  for (const char of plain) {
29
32
  const code = char.codePointAt(0);
@@ -203,4 +206,4 @@ function charWidth(code) {
203
206
  return 1;
204
207
  }
205
208
  //#endregion
206
- export { displayWidth, formatHumanChunk, parseColumns, resolveOutputWidth };
209
+ export { displayWidth, formatHumanChunk, parseColumns, resolveOutputWidth, stripAnsi };
@@ -7,8 +7,9 @@ const BRANCHES_LIST_LIMIT = 100;
7
7
  * Branch list pages cap at 100. A name stored in `.neon` can live on a later
8
8
  * page, so resolution has to walk `pagination.next` before treating it as missing.
9
9
  */
10
- const listAllProjectBranches = async (apiClient, projectId) => {
11
- const result = [];
10
+ const listAllProjectBranchesWithAnnotations = async (apiClient, projectId) => {
11
+ const branches = [];
12
+ const annotations = {};
12
13
  let cursor;
13
14
  while (true) {
14
15
  const { data } = await apiClient.listProjectBranches({
@@ -16,16 +17,21 @@ const listAllProjectBranches = async (apiClient, projectId) => {
16
17
  limit: BRANCHES_LIST_LIMIT,
17
18
  cursor
18
19
  });
19
- result.push(...data.branches);
20
+ branches.push(...data.branches);
21
+ Object.assign(annotations, data.annotations);
20
22
  cursor = data.pagination?.next;
21
23
  if (!cursor || data.branches.length === 0) break;
22
24
  }
23
- return result;
25
+ return {
26
+ branches,
27
+ annotations
28
+ };
24
29
  };
25
- const branchIdResolve = async ({ branch, apiClient, projectId }) => {
30
+ const listAllProjectBranches = async (apiClient, projectId) => (await listAllProjectBranchesWithAnnotations(apiClient, projectId)).branches;
31
+ const branchIdResolve = async ({ branch, apiClient, projectId, branches: listed }) => {
26
32
  branch = branch.toString();
27
33
  if (looksLikeBranchId(branch)) return branch;
28
- const branches = await listAllProjectBranches(apiClient, projectId);
34
+ const branches = listed ?? await listAllProjectBranches(apiClient, projectId);
29
35
  const branchData = branches.find((b) => b.name === branch);
30
36
  if (!branchData) throw new Error(`Branch ${branch} not found.\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`);
31
37
  return branchData.id;
@@ -45,6 +51,33 @@ const branchIdFromProps = async (props) => {
45
51
  props.branchId = await getBranchIdFromProps(props);
46
52
  return props.branchId;
47
53
  };
54
+ /**
55
+ * {@link branchIdFromProps}, plus whatever the resolution already fetched: a name or the
56
+ * default branch is resolved from a listing, so the branch object and the listing come back
57
+ * with the id and callers need not fetch them again. An explicit `br-…` id is not listed.
58
+ */
59
+ const resolveBranchFromProps = async (props, known) => {
60
+ const ref = typeof props.branch === "string" ? props.branch : props.id;
61
+ const explicit = ref ? String(ref) : void 0;
62
+ if (explicit !== void 0 && looksLikeBranchId(explicit)) {
63
+ props.branchId = explicit;
64
+ const branch = known?.branches.find((b) => b.id === explicit);
65
+ return branch ? {
66
+ branchId: explicit,
67
+ branch,
68
+ listing: known
69
+ } : { branchId: explicit };
70
+ }
71
+ const listing = known ?? await listAllProjectBranchesWithAnnotations(props.apiClient, props.projectId);
72
+ const branch = explicit === void 0 ? listing.branches.find((b) => b.default) : listing.branches.find((b) => b.name === explicit);
73
+ if (!branch) throw new Error(explicit === void 0 ? "No default branch found" : `Branch ${explicit} not found.\nAvailable branches: ${listing.branches.map((b) => b.name).join(", ")}`);
74
+ props.branchId = branch.id;
75
+ return {
76
+ branchId: branch.id,
77
+ branch,
78
+ listing
79
+ };
80
+ };
48
81
  const resolveBranchRef = async (props) => {
49
82
  const branch = typeof props.branch === "string" ? props.branch : props.id;
50
83
  const branches = await listAllProjectBranches(props.apiClient, props.projectId);
@@ -130,4 +163,4 @@ const fillSingleOrg = async (props) => {
130
163
  };
131
164
  };
132
165
  //#endregion
133
- export { branchIdFromProps, branchIdResolve, fillSingleOrg, fillSingleProject, listAllProjectBranches, resolveBranchRef, resolveSingleDatabase };
166
+ export { branchIdFromProps, branchIdResolve, fillSingleOrg, fillSingleProject, listAllProjectBranches, listAllProjectBranchesWithAnnotations, resolveBranchFromProps, resolveBranchRef, resolveSingleDatabase };
@@ -28,19 +28,19 @@ const parsePITBranch = (input) => {
28
28
  }
29
29
  return result;
30
30
  };
31
- const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api }) => {
31
+ const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api, branches, targetBranch }) => {
32
32
  const parsedPIT = parsePITBranch(pointInTime);
33
33
  let branchId = "";
34
34
  if (parsedPIT.branch === "^self") branchId = targetBranchId;
35
35
  else if (parsedPIT.branch === "^parent") {
36
- const { data } = await api.getProjectBranch(projectId, targetBranchId);
37
- const { parent_id: parentId } = data.branch;
36
+ const parentId = (targetBranch?.id === targetBranchId ? targetBranch : (await api.getProjectBranch(projectId, targetBranchId)).data.branch).parent_id;
38
37
  if (parentId == null) throw new PointInTimeParseError("Branch has no parent");
39
38
  branchId = parentId;
40
39
  } else branchId = await branchIdResolve({
41
40
  branch: parsedPIT.branch,
42
41
  projectId,
43
- apiClient: api
42
+ apiClient: api,
43
+ branches
44
44
  });
45
45
  delete parsedPIT.branch;
46
46
  return {
package/dist/writer.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isCi } from "./env.js";
2
2
  import { toSnakeCase } from "./utils/string.js";
3
- import { formatHumanChunk, resolveOutputWidth } from "./human_table.js";
3
+ import { formatHumanChunk, resolveOutputWidth, stripAnsi } from "./human_table.js";
4
4
  import YAML from "yaml";
5
5
  //#region src/writer.ts
6
6
  const writeYaml = (chunks) => {
@@ -11,17 +11,20 @@ const writeJson = (chunks) => {
11
11
  return json === void 0 ? json : `${json}\n`;
12
12
  };
13
13
  const writeTable = (chunks, out, width) => {
14
+ const noColor = Boolean(process.env.NO_COLOR);
14
15
  chunks.forEach(({ data, config }, i) => {
15
- out.write(formatHumanChunk({
16
+ const title = config.humanTitle ?? config.title;
17
+ const text = formatHumanChunk({
16
18
  data,
17
19
  fields: config.fields,
18
- title: config.title,
20
+ title,
19
21
  emptyMessage: config.emptyMessage,
20
22
  renderColumns: config.renderColumns,
21
23
  width,
22
24
  colorTitle: !isCi(),
23
- leadingBlank: i > 0 && Boolean(config.title)
24
- }));
25
+ leadingBlank: i > 0 && Boolean(title)
26
+ });
27
+ out.write(noColor ? stripAnsi(text) : text);
25
28
  });
26
29
  };
27
30
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "7.0.3",
3
+ "version": "7.0.5",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -64,9 +64,9 @@
64
64
  "yaml": "^2.9.0",
65
65
  "yargs": "17.7.2",
66
66
  "yoctocolors": "^2.1.2",
67
+ "@neon/sdk": "7.0.0",
67
68
  "@neon/config": "1.8.3",
68
- "@neon/config-runtime": "1.7.3",
69
- "@neon/sdk": "7.0.0"
69
+ "@neon/config-runtime": "1.7.3"
70
70
  },
71
71
  "optionalDependencies": {
72
72
  "@napi-rs/keyring": "1.3.0",