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.
- package/dist/commands/canary.js +2 -2
- package/dist/commands/canary.js.map +1 -1
- package/dist/commands/logDrains.d.ts +7 -0
- package/dist/commands/logDrains.d.ts.map +1 -0
- package/dist/commands/logDrains.js +42 -0
- package/dist/commands/logDrains.js.map +1 -0
- package/dist/commands/metrics.d.ts +10 -0
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +54 -0
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/services.d.ts +4 -0
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +38 -4
- package/dist/commands/services.js.map +1 -1
- package/dist/commands/volumeFiles.d.ts +15 -0
- package/dist/commands/volumeFiles.d.ts.map +1 -0
- package/dist/commands/volumeFiles.js +116 -0
- package/dist/commands/volumeFiles.js.map +1 -0
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/canary.ts +2 -2
- package/src/commands/logDrains.ts +32 -0
- package/src/commands/metrics.ts +40 -0
- package/src/commands/services.ts +26 -4
- package/src/commands/volumeFiles.ts +65 -0
- package/src/index.ts +40 -2
|
@@ -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('
|
|
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')
|