neonctl 2.13.1 → 2.15.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.
package/README.md CHANGED
@@ -64,19 +64,19 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
64
64
 
65
65
  ## Commands
66
66
 
67
- | Command | Subcommands | Description |
68
- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------- |
69
- | [auth](https://neon.tech/docs/reference/cli-auth) | | Authenticate |
70
- | [projects](https://neon.tech/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
71
- | [ip-allow](https://neon.tech/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
72
- | [me](https://neon.tech/docs/reference/cli-me) | | Show current user |
73
- | [branches](https://neon.tech/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `delete`, `get` | Manage branches |
74
- | [databases](https://neon.tech/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
75
- | [roles](https://neon.tech/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
76
- | [operations](https://neon.tech/docs/reference/cli-operations) | `list` | Manage operations |
77
- | [connection-string](https://neon.tech/docs/reference/cli-connection-string) | | Get connection string |
78
- | [set-context](https://neon.tech/docs/reference/cli-set-context) | | Set context for session |
79
- | [completion](https://neon.tech/docs/reference/cli-completion) | | Generate a completion script |
67
+ | Command | Subcommands | Description |
68
+ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------- |
69
+ | [auth](https://neon.com/docs/reference/cli-auth) | | Authenticate |
70
+ | [projects](https://neon.com/docs/reference/cli-projects) | `list`, `create`, `update`, `delete`, `get` | Manage projects |
71
+ | [ip-allow](https://neon.com/docs/reference/cli-ip-allow) | `list`, `add`, `remove`, `reset` | Manage IP Allow |
72
+ | [me](https://neon.com/docs/reference/cli-me) | | Show current user |
73
+ | [branches](https://neon.com/docs/reference/cli-branches) | `list`, `create`, `rename`, `add-compute`, `set-default`, `set-expiration`, `delete`, `get` | Manage branches |
74
+ | [databases](https://neon.com/docs/reference/cli-databases) | `list`, `create`, `delete` | Manage databases |
75
+ | [roles](https://neon.com/docs/reference/cli-roles) | `list`, `create`, `delete` | Manage roles |
76
+ | [operations](https://neon.com/docs/reference/cli-operations) | `list` | Manage operations |
77
+ | [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
78
+ | [set-context](https://neon.com/docs/reference/cli-set-context) | | Set context for session |
79
+ | [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
80
80
 
81
81
  ## Global options
82
82
 
@@ -15,6 +15,7 @@ export const BRANCH_FIELDS = [
15
15
  'default',
16
16
  'current_state',
17
17
  'created_at',
18
+ 'expires_at',
18
19
  ];
19
20
  const BRANCH_FIELDS_RESET = [
20
21
  'id',
@@ -85,6 +86,11 @@ export const builder = (argv) => argv
85
86
  type: 'boolean',
86
87
  default: false,
87
88
  },
89
+ 'expires-at': {
90
+ describe: 'Set an expiration date for the branch. Accepts a date string (e.g., 2024-12-31T23:59:59Z).',
91
+ type: 'string',
92
+ requiresArg: true,
93
+ },
88
94
  }), (args) => create(args))
89
95
  .command('reset <id|name>', 'Reset a branch', (yargs) => yargs.options({
90
96
  parent: {
@@ -131,6 +137,18 @@ export const builder = (argv) => argv
131
137
  ]), (args) => restore(args))
132
138
  .command('rename <id|name> <new-name>', 'Rename a branch', (yargs) => yargs, (args) => rename(args))
133
139
  .command('set-default <id|name>', 'Set a branch as default', (yargs) => yargs, (args) => setDefault(args))
140
+ .command({
141
+ command: 'set-expiration <id|name>',
142
+ describe: 'Set an expiration date for the branch',
143
+ builder: (yargs) => yargs.options({
144
+ 'expires-at': {
145
+ describe: 'Set a expiration date for the branch. If omitted, expiration will be removed. Format [RFC3339]: 2024-12-31T23:59:59Z',
146
+ type: 'string',
147
+ requiresArg: false,
148
+ },
149
+ }),
150
+ handler: (args) => setExpiration(args),
151
+ })
134
152
  .command('add-compute <id|name>', 'Add a compute to a branch', (yargs) => yargs.options({
135
153
  type: {
136
154
  type: 'string',
@@ -241,6 +259,9 @@ const create = async (props) => {
241
259
  name: props.name,
242
260
  ...parentProps,
243
261
  ...(props.schemaOnly ? { init_source: 'schema-only' } : {}),
262
+ ...(props['expires-at']
263
+ ? { expires_at: new Date(props['expires-at']).toISOString() }
264
+ : {}),
244
265
  },
245
266
  endpoints: props.compute
246
267
  ? [
@@ -394,3 +415,21 @@ const restore = async (props) => {
394
415
  }
395
416
  writeInst.end();
396
417
  };
418
+ const setExpiration = async (props) => {
419
+ // Accept either branch name or id
420
+ const branchId = await branchIdFromProps({
421
+ ...props,
422
+ id: props.branchId,
423
+ });
424
+ const expiresAt = typeof props.expiresAt === 'string'
425
+ ? new Date(props.expiresAt).toISOString()
426
+ : null;
427
+ const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, {
428
+ branch: {
429
+ expires_at: expiresAt,
430
+ },
431
+ }));
432
+ writer(props).end(data.branch, {
433
+ fields: BRANCH_FIELDS,
434
+ });
435
+ };
@@ -180,6 +180,18 @@ describe('branches', () => {
180
180
  stderr: 'ERROR: Schema-only branches require a read-write compute endpoint',
181
181
  });
182
182
  });
183
+ test('create with expiration date', async ({ testCliCommand }) => {
184
+ await testCliCommand([
185
+ 'branches',
186
+ 'create',
187
+ '--project-id',
188
+ 'test',
189
+ '--name',
190
+ 'test_branch',
191
+ '--expiration-date',
192
+ '2021-01-01T00:00:00.000Z',
193
+ ]);
194
+ });
183
195
  /* delete */
184
196
  test('delete by id', async ({ testCliCommand }) => {
185
197
  await testCliCommand([
@@ -352,4 +364,92 @@ describe('branches', () => {
352
364
  Available branches: self-tolsn-123456, any-branch, parent-tots, another-branch`,
353
365
  });
354
366
  });
367
+ /* set expiration */
368
+ test('set expiration date', async ({ testCliCommand }) => {
369
+ await testCliCommand([
370
+ 'branches',
371
+ 'set-expiration',
372
+ 'br-sunny-branch-123456',
373
+ '--project-id',
374
+ 'test',
375
+ '--expires-at',
376
+ '2024-12-31T23:59:59Z',
377
+ ]);
378
+ });
379
+ test('remove expiration date', async ({ testCliCommand }) => {
380
+ await testCliCommand([
381
+ 'branches',
382
+ 'set-expiration',
383
+ 'br-sunny-branch-123456',
384
+ '--project-id',
385
+ 'test',
386
+ ]);
387
+ });
388
+ test('set expiration by branch name', async ({ testCliCommand }) => {
389
+ await testCliCommand([
390
+ 'branches',
391
+ 'set-expiration',
392
+ 'test_branch',
393
+ '--project-id',
394
+ 'test',
395
+ '--expires-at',
396
+ '2024-12-31T23:59:59Z',
397
+ ]);
398
+ });
399
+ test('set expiration fails on default branch', async ({ testCliCommand }) => {
400
+ await testCliCommand([
401
+ 'branches',
402
+ 'set-expiration',
403
+ 'br-main-branch-123456',
404
+ '--project-id',
405
+ 'test',
406
+ '--expires-at',
407
+ '2024-12-31T23:59:59Z',
408
+ ], {
409
+ code: 1,
410
+ stderr: 'ERROR: Default branch cannot have an expiration date',
411
+ });
412
+ });
413
+ test('set expiration fails on default branch by name', async ({ testCliCommand, }) => {
414
+ await testCliCommand([
415
+ 'branches',
416
+ 'set-expiration',
417
+ 'main',
418
+ '--project-id',
419
+ 'test',
420
+ '--expires-at',
421
+ '2024-12-31T23:59:59Z',
422
+ ], {
423
+ code: 1,
424
+ stderr: 'ERROR: Default branch cannot have an expiration date',
425
+ });
426
+ });
427
+ test('set expiration fails on protected branch', async ({ testCliCommand, }) => {
428
+ await testCliCommand([
429
+ 'branches',
430
+ 'set-expiration',
431
+ 'br-protected-branch-123456',
432
+ '--project-id',
433
+ 'test',
434
+ '--expires-at',
435
+ '2024-12-31T23:59:59Z',
436
+ ], {
437
+ code: 1,
438
+ stderr: 'ERROR: Protected branch cannot have an expiration date',
439
+ });
440
+ });
441
+ test('set expiration fails on protected branch by name', async ({ testCliCommand, }) => {
442
+ await testCliCommand([
443
+ 'branches',
444
+ 'set-expiration',
445
+ 'protected_branch',
446
+ '--project-id',
447
+ 'test',
448
+ '--expires-at',
449
+ '2024-12-31T23:59:59Z',
450
+ ], {
451
+ code: 1,
452
+ stderr: 'ERROR: Protected branch cannot have an expiration date',
453
+ });
454
+ });
355
455
  });
package/index.js CHANGED
@@ -141,7 +141,7 @@ builder = builder
141
141
  .alias('version', 'v')
142
142
  .completion()
143
143
  .scriptName(basename(process.argv[1]) === 'neon' ? 'neon' : 'neonctl')
144
- .epilog('For more information, visit https://neon.tech/docs/reference/neon-cli')
144
+ .epilog('For more information, visit https://neon.com/docs/reference/neon-cli')
145
145
  .wrap(null)
146
146
  .fail(false);
147
147
  async function handleError(msg, err) {
@@ -208,7 +208,7 @@ void (async () => {
208
208
  process.exit(0);
209
209
  }
210
210
  await closeAnalytics();
211
- process.exit(0);
211
+ break;
212
212
  }
213
213
  catch (err) {
214
214
  attempts++;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git+ssh://git@github.com/neondatabase/neonctl.git"
6
6
  },
7
7
  "type": "module",
8
- "version": "2.13.1",
8
+ "version": "2.15.0",
9
9
  "description": "CLI tool for NeonDB Cloud management",
10
10
  "main": "index.js",
11
11
  "author": "NeonDB",
@@ -54,7 +54,7 @@
54
54
  "vitest": "^1.6.0"
55
55
  },
56
56
  "dependencies": {
57
- "@neondatabase/api-client": "2.1.0",
57
+ "@neondatabase/api-client": "2.2.0",
58
58
  "@segment/analytics-node": "^1.0.0-beta.26",
59
59
  "axios": "^1.4.0",
60
60
  "axios-debug-log": "^1.0.0",
package/parameters.gen.js CHANGED
@@ -286,6 +286,11 @@ export const branchCreateRequest = {
286
286
  description: "The source of initialization for the branch. Valid values are `schema-only` and `parent-data` (default).\n * `schema-only` - creates a new root branch containing only the schema. Use `parent_id` to specify the source branch. Optionally, you can provide `parent_lsn` or `parent_timestamp` to branch from a specific point in time or LSN. These fields define which branch to copy the schema from and at what point—they do not establish a parent-child relationship between the `parent_id` branch and the new schema-only branch.\n * `parent-data` - creates the branch with both schema and data from the parent.\n",
287
287
  demandOption: false,
288
288
  },
289
+ 'branch.expires_at': {
290
+ type: "string",
291
+ description: "The timestamp when the branch is scheduled to expire and be automatically deleted. Must be set by the client following the [RFC 3339, section 5.6](https://tools.ietf.org/html/rfc3339#section-5.6) format with precision up to seconds (such as 2025-06-09T18:02:16Z). Deletion is performed by a background job and may not occur exactly at the specified time.\n\nThis feature is still under development and not yet generally available.\n",
292
+ demandOption: false,
293
+ },
289
294
  };
290
295
  export const branchCreateRequestEndpointOptions = {
291
296
  'type': {
@@ -316,6 +321,11 @@ export const branchUpdateRequest = {
316
321
  description: undefined,
317
322
  demandOption: false,
318
323
  },
324
+ 'branch.expires_at': {
325
+ type: "string",
326
+ description: "The timestamp when the branch is scheduled to expire and be automatically deleted. Must be set by the client following the [RFC 3339, section 5.6](https://tools.ietf.org/html/rfc3339#section-5.6) format with precision up to seconds (such as 2025-06-09T18:02:16Z). Deletion is performed by a background job and may not occur exactly at the specified time. If this field is set to null, the expiration timestamp is removed.\n\nThis feature is still under development and not yet generally available.\n",
327
+ demandOption: false,
328
+ },
319
329
  };
320
330
  export const endpointCreateRequest = {
321
331
  'endpoint.branch_id': {
@@ -365,6 +375,11 @@ export const endpointCreateRequest = {
365
375
  description: "Duration of inactivity in seconds after which the compute endpoint is\nautomatically suspended. The value `0` means use the default value.\nThe value `-1` means never suspend. The default value is `300` seconds (5 minutes).\nThe minimum value is `60` seconds (1 minute).\nThe maximum value is `604800` seconds (1 week). For more information, see\n[Scale to zero configuration](https://neon.tech/docs/manage/endpoints#scale-to-zero-configuration).\n",
366
376
  demandOption: false,
367
377
  },
378
+ 'endpoint.name': {
379
+ type: "string",
380
+ description: "Optional name of the compute endpoint\n",
381
+ demandOption: false,
382
+ },
368
383
  };
369
384
  export const endpointUpdateRequest = {
370
385
  'endpoint.branch_id': {
@@ -403,6 +418,11 @@ export const endpointUpdateRequest = {
403
418
  description: "Duration of inactivity in seconds after which the compute endpoint is\nautomatically suspended. The value `0` means use the default value.\nThe value `-1` means never suspend. The default value is `300` seconds (5 minutes).\nThe minimum value is `60` seconds (1 minute).\nThe maximum value is `604800` seconds (1 week). For more information, see\n[Scale to zero configuration](https://neon.tech/docs/manage/endpoints#scale-to-zero-configuration).\n",
404
419
  demandOption: false,
405
420
  },
421
+ 'endpoint.name': {
422
+ type: "string",
423
+ description: "Optional name of the compute endpoint\n",
424
+ demandOption: false,
425
+ },
406
426
  };
407
427
  export const databaseCreateRequest = {
408
428
  'database.name': {
@@ -61,6 +61,7 @@ export const test = originalTest.extend({
61
61
  log.error(data.toString());
62
62
  });
63
63
  cp.on('error', (err) => {
64
+ log.error(err);
64
65
  throw err;
65
66
  });
66
67
  cp.on('close', (code) => {
@@ -78,6 +79,9 @@ export const test = originalTest.extend({
78
79
  reject(err instanceof Error ? err : new Error(String(err)));
79
80
  }
80
81
  });
82
+ }).catch((err) => {
83
+ log.error(err);
84
+ throw err;
81
85
  });
82
86
  });
83
87
  },