ninedeploy 0.17.1 → 0.18.4

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.
@@ -0,0 +1,65 @@
1
+ import { open, writeFile } from 'node:fs/promises';
2
+ import { managedVolumeName } from '@ninedeploy/schemas';
3
+ import type { NineDeployClient } from '../client.js';
4
+ import { error, info, success, table } from '../lib/format.js';
5
+ import { prompt } from '../prompts.js';
6
+
7
+ export interface VolumeFileOptions { server?: string; json?: boolean; file?: string; output?: string; force?: boolean; yes?: boolean }
8
+ function target(name: string, raw: string, options: VolumeFileOptions, root = false) {
9
+ managedVolumeName.parse(name);
10
+ if (options.server !== undefined && (!/^[1-9]\d*$/.test(options.server) || !Number.isSafeInteger(Number(options.server)))) throw Error('--server must be a positive decimal node id');
11
+ if (raw.startsWith('/') || /[\0\r\n]/.test(raw) || raw.length > 2048) throw Error('Use a relative path inside the volume');
12
+ const parts: string[] = [];
13
+ for (const part of raw.split('/')) {
14
+ if (!part || part === '.') continue;
15
+ if (part === '..') { if (!parts.length) throw Error('Path escapes the volume'); parts.pop(); }
16
+ else parts.push(part);
17
+ }
18
+ const path = parts.join('/');
19
+ if (!path && !root) throw Error('A path inside the volume is required');
20
+ const host: [] | [{serverId:number}] = options.server === undefined ? [] : [{serverId:Number(options.server)}];
21
+ return {path,host};
22
+ }
23
+ const fail=(err:unknown)=>error(err instanceof Error?err.message:String(err));
24
+ export async function volumeFilesList(client: NineDeployClient,name:string,path='',options:VolumeFileOptions={}) {
25
+ try {
26
+ const selected=target(name,path,options,true),result=await client.volumes.listFiles(name,selected.path,...selected.host);
27
+ if(options.json)console.log(JSON.stringify(result));
28
+ else if(!result.entries.length)info('Empty directory.');
29
+ else table(result.entries.map(entry=>({name:entry.name,type:entry.type,size:entry.sizeBytes})),['name','type','size']);
30
+ }catch(err){fail(err);}
31
+ }
32
+ export async function volumeFileRead(client:NineDeployClient,name:string,path:string,options:VolumeFileOptions={}) {
33
+ try {
34
+ const selected=target(name,path,options),result=await client.volumes.readFile(name,selected.path,...selected.host);
35
+ const bytes=Buffer.from(result.content,result.encoding==='base64'?'base64':'utf8');
36
+ if(options.output){await writeFile(options.output,bytes,{flag:options.force?'w':'wx'});success(`Saved ${bytes.length} bytes to ${options.output}`);}
37
+ else process.stdout.write(bytes);
38
+ }catch(err){fail(err);}
39
+ }
40
+ export async function volumeFileWrite(client:NineDeployClient,name:string,path:string,options:VolumeFileOptions={}) {
41
+ try {
42
+ const selected=target(name,path,options);
43
+ if(!options.file)throw Error('--file <local-file> is required');
44
+ const input=await open(options.file,'r');let bytes:Buffer;
45
+ try {
46
+ if(!(await input.stat()).isFile())throw Error('--file must name a regular file');
47
+ const buffer=Buffer.alloc(1024*1024+1);let length=0;
48
+ while(length<buffer.length){const read=await input.read(buffer,length,buffer.length-length,null);if(!read.bytesRead)break;length+=read.bytesRead;}
49
+ if(length>1024*1024)throw Error('File exceeds the 1 MiB editor limit');
50
+ bytes=buffer.subarray(0,length);
51
+ }finally{await input.close();}
52
+ await client.volumes.writeFile(name,{path:selected.path,contentBase64:bytes.toString('base64')},...selected.host);
53
+ success(`Wrote ${bytes.length} bytes to ${name}:${selected.path}${options.server?` on node #${options.server}`:''}`);
54
+ }catch(err){fail(err);}
55
+ }
56
+ export async function volumeFileMkdir(client:NineDeployClient,name:string,path:string,options:VolumeFileOptions={}) {
57
+ try {const selected=target(name,path,options);await client.volumes.mkdir(name,{path:selected.path},...selected.host);success('Directory created');}catch(err){fail(err);}
58
+ }
59
+ export async function volumeFileDelete(client:NineDeployClient,name:string,path:string,options:VolumeFileOptions={}) {
60
+ try {
61
+ const selected=target(name,path,options);
62
+ if(!options.yes){const answer=await prompt(`Type ${selected.path} to delete it recursively from ${name}${options.server?` on node #${options.server}`:''}`);if(answer!==selected.path)return error('Cancelled.');}
63
+ await client.volumes.deleteFile(name,selected.path,...selected.host);success('Volume path deleted');
64
+ }catch(err){fail(err);}
65
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { volumeFilesList, volumeFileRead, volumeFileWrite, volumeFileMkdir, volumeFileDelete, type VolumeFileOptions } from './commands/volumeFiles.js';
2
3
  import { createRequire } from 'node:module';
3
4
  import { Command } from 'commander';
4
5
  import { getClient } from './client.js';
@@ -9,6 +10,7 @@ import { setupAction } from './commands/setup.js';
9
10
  import {
10
11
  servicesCompose, servicesCreate, servicesDelete, servicesDeploy, servicesExport,
11
12
  servicesGet, servicesInspect, servicesLifecycle, servicesList, servicesLogs,
13
+ servicesLifecycleStatus,
12
14
  servicesStickyAction,
13
15
  servicesPortsAction,
14
16
  servicesDeployHookAction,
@@ -112,8 +114,9 @@ import {
112
114
  configPresetListAction, configPresetRegisterAction, configPresetRemoveAction,
113
115
  } from './commands/configPresets.js';
114
116
  import {
115
- metricsFlushAction, metricsShowAction,
117
+ metricsFlushAction, metricsShowAction, resourceMetricsSettingsAction, resourceMetricsConfigureAction,
116
118
  } from './commands/metrics.js';
119
+ import { logDrainQueueAction, logDrainRetryAction, logDrainDiscardAction } from './commands/logDrains.js';
117
120
  import {
118
121
  buildCacheStatsAction,
119
122
  } from './commands/buildCache.js';
@@ -274,6 +277,8 @@ services.command('stop <id>').description('Stop a running service').action((id:
274
277
  services.command('start <id>').description('Start a stopped service').action((id: string) => servicesLifecycle(getClient(), 'start', id));
275
278
 
276
279
  services.command('restart <id>').description('Restart a service').action((id: string) => servicesLifecycle(getClient(), 'restart', id));
280
+ services.command('lifecycle <id>').description('Show the last node runtime command and its pending reason').option('--retry', 'Retry the same command without changing its revision')
281
+ .action((id: string, opts: { retry?: boolean }) => servicesLifecycleStatus(getClient(), id, opts));
277
282
 
278
283
  services.command('delete <id>').description('Delete a service (with confirmation)').action((id: string) => servicesDelete(getClient(), id));
279
284
 
@@ -594,10 +599,26 @@ configPresetsCmd.command('apply <id>')
594
599
  configPresetsCmd.command('remove <id>').description('Unregister a preset (does NOT undo the live apply)').action((id: string) => configPresetRemoveAction(getClient(), id));
595
600
 
596
601
  // ── Metrics (G-09) ────────────────────────────────────────────────────────
597
- const metricsCmd = program.command('metrics').description('Metric history plugin — archive kernel events to a pluggable backend (G-09)');
602
+ const metricsCmd = program.command('metrics').description('Resource monitoring and kernel event history');
603
+ metricsCmd.command('settings').description('Resource retention, Prometheus and collector settings').action(() => resourceMetricsSettingsAction(getClient()));
604
+ metricsCmd.command('configure').description('Update resource monitoring settings')
605
+ .option('--retention-days <days>', 'Service history retention, 1–90 days')
606
+ .option('--prometheus <on|off>', 'Enable or disable authenticated Prometheus scraping')
607
+ .option('--otlp-endpoint <url>', 'OTLP/HTTP JSON metrics endpoint, empty disables export')
608
+ .option('--otlp-authorization-env <name>', 'Read collector Authorization from this environment variable')
609
+ .option('--clear-otlp-authorization', 'Remove the saved collector credential')
610
+ .option('--log-queue-mb <mb>', 'Encrypted log payload limit per destination, 1–256 MiB')
611
+ .action((opts) => resourceMetricsConfigureAction(getClient(), opts));
598
612
  metricsCmd.command('show').description('Show the active backend, events, and last flush marker').action(() => metricsShowAction(getClient()));
599
613
  metricsCmd.command('flush').description('Run the built-in backend retention sweep').action(() => metricsFlushAction(getClient()));
600
614
 
615
+ const logDrainsCmd = program.command('log-drains').description('Inspect and recover durable log delivery');
616
+ logDrainsCmd.command('queue <id>').description('Show pending, blocked and paused log delivery').action((id: string) => logDrainQueueAction(getClient(), id));
617
+ logDrainsCmd.command('retry <id>').description('Schedule pending logs for the next delivery cycle').action((id: string) => logDrainRetryAction(getClient(), id));
618
+ logDrainsCmd.command('discard <id>').description('Permanently discard pending logs')
619
+ .option('--yes', 'Confirm permanent removal of queued logs')
620
+ .action((id: string, opts) => logDrainDiscardAction(getClient(), id, opts));
621
+
601
622
  // ── Build Cache (G-01) ────────────────────────────────────────────────────
602
623
  const buildCacheCmd = program.command('build-cache').description('Build cache plugin — per-backend LRU stats (G-01)');
603
624
  buildCacheCmd.command('stats').description('Show per-backend cache counters and the merged totals').action(() => buildCacheStatsAction(getClient()));
@@ -677,6 +698,23 @@ ssoCmd.command('remove <id>').description('Remove a provider by id').action((id:
677
698
  // ── Volumes ────────────────────────────────────────────────────────────────
678
699
  const volumesCmd = program.command('volumes').description('Manage Docker volumes');
679
700
 
701
+ volumesCmd.command('files <name> [path]').description('List files inside a managed volume')
702
+ .option('--server <id>', 'Read this node instead of the panel host').option('--json', 'Print raw JSON')
703
+ .action((name:string,path:string|undefined,opts:VolumeFileOptions)=>volumeFilesList(getClient(),name,path??'',opts));
704
+ volumesCmd.command('read <name> <path>').description('Read exact file bytes from a managed volume')
705
+ .option('--server <id>', 'Read this node instead of the panel host').option('--output <file>', 'Save to a local file instead of stdout').option('--force', 'Overwrite an existing output file')
706
+ .action((name:string,path:string,opts:VolumeFileOptions)=>volumeFileRead(getClient(),name,path,opts));
707
+ volumesCmd.command('write <name> <path>').description('Write a local file into a managed volume (maximum 1 MiB)')
708
+ .requiredOption('--file <file>', 'Local input file').option('--server <id>', 'Write this node instead of the panel host')
709
+ .action((name:string,path:string,opts:VolumeFileOptions)=>volumeFileWrite(getClient(),name,path,opts));
710
+ volumesCmd.command('mkdir <name> <path>').description('Create a directory inside a managed volume')
711
+ .option('--server <id>', 'Use this node instead of the panel host')
712
+ .action((name:string,path:string,opts:VolumeFileOptions)=>volumeFileMkdir(getClient(),name,path,opts));
713
+ volumesCmd.command('delete-file <name> <path>').description('Delete a file or directory inside a volume, with confirmation')
714
+ .option('--server <id>', 'Use this node instead of the panel host').option('--yes', 'Confirm this exact path without prompting')
715
+ .action((name:string,path:string,opts:VolumeFileOptions)=>volumeFileDelete(getClient(),name,path,opts));
716
+
717
+
680
718
  volumesCmd.command('list')
681
719
  .description('List all volumes')
682
720
  .option('--server <id>', 'A node\'s volumes instead of the panel host\'s')