aparavi-client 1.0.2
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/LICENSE +21 -0
- package/README.md +523 -0
- package/dist/aparavi-client-typescript-1.0.0.tgz +0 -0
- package/dist/aparavi-client-typescript-1.0.1.tgz +0 -0
- package/dist/cjs/client.js +1002 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/constants.js +37 -0
- package/dist/cjs/constants.js.map +1 -0
- package/dist/cjs/core/DAPBase.js +313 -0
- package/dist/cjs/core/DAPBase.js.map +1 -0
- package/dist/cjs/core/DAPClient.js +173 -0
- package/dist/cjs/core/DAPClient.js.map +1 -0
- package/dist/cjs/core/TransportBase.js +131 -0
- package/dist/cjs/core/TransportBase.js.map +1 -0
- package/dist/cjs/core/TransportWebSocket.js +492 -0
- package/dist/cjs/core/TransportWebSocket.js.map +1 -0
- package/dist/cjs/exceptions/index.js +109 -0
- package/dist/cjs/exceptions/index.js.map +1 -0
- package/dist/cjs/index.js +56 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/schema/Doc.js +79 -0
- package/dist/cjs/schema/Doc.js.map +1 -0
- package/dist/cjs/schema/DocFilter.js +133 -0
- package/dist/cjs/schema/DocFilter.js.map +1 -0
- package/dist/cjs/schema/DocGroup.js +235 -0
- package/dist/cjs/schema/DocGroup.js.map +1 -0
- package/dist/cjs/schema/DocMetadata.js +57 -0
- package/dist/cjs/schema/DocMetadata.js.map +1 -0
- package/dist/cjs/schema/Question.js +414 -0
- package/dist/cjs/schema/Question.js.map +1 -0
- package/dist/cjs/schema/index.js +50 -0
- package/dist/cjs/schema/index.js.map +1 -0
- package/dist/cjs/types/client.js +26 -0
- package/dist/cjs/types/client.js.map +1 -0
- package/dist/cjs/types/data.js +26 -0
- package/dist/cjs/types/data.js.map +1 -0
- package/dist/cjs/types/events.js +119 -0
- package/dist/cjs/types/events.js.map +1 -0
- package/dist/cjs/types/index.js +50 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/types/pipeline.js +26 -0
- package/dist/cjs/types/pipeline.js.map +1 -0
- package/dist/cjs/types/task.js +115 -0
- package/dist/cjs/types/task.js.map +1 -0
- package/dist/cli/cli/aparavi.js +1401 -0
- package/dist/cli/cli/aparavi.js.map +1 -0
- package/dist/cli/src/client.js +1002 -0
- package/dist/cli/src/client.js.map +1 -0
- package/dist/cli/src/constants.js +37 -0
- package/dist/cli/src/constants.js.map +1 -0
- package/dist/cli/src/core/DAPBase.js +313 -0
- package/dist/cli/src/core/DAPBase.js.map +1 -0
- package/dist/cli/src/core/DAPClient.js +173 -0
- package/dist/cli/src/core/DAPClient.js.map +1 -0
- package/dist/cli/src/core/TransportBase.js +131 -0
- package/dist/cli/src/core/TransportBase.js.map +1 -0
- package/dist/cli/src/core/TransportWebSocket.js +492 -0
- package/dist/cli/src/core/TransportWebSocket.js.map +1 -0
- package/dist/cli/src/exceptions/index.js +109 -0
- package/dist/cli/src/exceptions/index.js.map +1 -0
- package/dist/cli/src/index.js +56 -0
- package/dist/cli/src/index.js.map +1 -0
- package/dist/cli/src/schema/Doc.js +79 -0
- package/dist/cli/src/schema/Doc.js.map +1 -0
- package/dist/cli/src/schema/DocFilter.js +133 -0
- package/dist/cli/src/schema/DocFilter.js.map +1 -0
- package/dist/cli/src/schema/DocGroup.js +235 -0
- package/dist/cli/src/schema/DocGroup.js.map +1 -0
- package/dist/cli/src/schema/DocMetadata.js +57 -0
- package/dist/cli/src/schema/DocMetadata.js.map +1 -0
- package/dist/cli/src/schema/Question.js +414 -0
- package/dist/cli/src/schema/Question.js.map +1 -0
- package/dist/cli/src/schema/index.js +50 -0
- package/dist/cli/src/schema/index.js.map +1 -0
- package/dist/cli/src/types/client.js +26 -0
- package/dist/cli/src/types/client.js.map +1 -0
- package/dist/cli/src/types/data.js +26 -0
- package/dist/cli/src/types/data.js.map +1 -0
- package/dist/cli/src/types/events.js +119 -0
- package/dist/cli/src/types/events.js.map +1 -0
- package/dist/cli/src/types/index.js +50 -0
- package/dist/cli/src/types/index.js.map +1 -0
- package/dist/cli/src/types/pipeline.js +26 -0
- package/dist/cli/src/types/pipeline.js.map +1 -0
- package/dist/cli/src/types/task.js +115 -0
- package/dist/cli/src/types/task.js.map +1 -0
- package/dist/esm/client.js +997 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/constants.js +34 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/core/DAPBase.js +309 -0
- package/dist/esm/core/DAPBase.js.map +1 -0
- package/dist/esm/core/DAPClient.js +169 -0
- package/dist/esm/core/DAPClient.js.map +1 -0
- package/dist/esm/core/TransportBase.js +127 -0
- package/dist/esm/core/TransportBase.js.map +1 -0
- package/dist/esm/core/TransportWebSocket.js +488 -0
- package/dist/esm/core/TransportWebSocket.js.map +1 -0
- package/dist/esm/exceptions/index.js +100 -0
- package/dist/esm/exceptions/index.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/schema/Doc.js +75 -0
- package/dist/esm/schema/Doc.js.map +1 -0
- package/dist/esm/schema/DocFilter.js +129 -0
- package/dist/esm/schema/DocFilter.js.map +1 -0
- package/dist/esm/schema/DocGroup.js +231 -0
- package/dist/esm/schema/DocGroup.js.map +1 -0
- package/dist/esm/schema/DocMetadata.js +53 -0
- package/dist/esm/schema/DocMetadata.js.map +1 -0
- package/dist/esm/schema/Question.js +409 -0
- package/dist/esm/schema/Question.js.map +1 -0
- package/dist/esm/schema/index.js +34 -0
- package/dist/esm/schema/index.js.map +1 -0
- package/dist/esm/types/client.js +25 -0
- package/dist/esm/types/client.js.map +1 -0
- package/dist/esm/types/data.js +25 -0
- package/dist/esm/types/data.js.map +1 -0
- package/dist/esm/types/events.js +116 -0
- package/dist/esm/types/events.js.map +1 -0
- package/dist/esm/types/index.js +34 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/types/pipeline.js +25 -0
- package/dist/esm/types/pipeline.js.map +1 -0
- package/dist/esm/types/task.js +112 -0
- package/dist/esm/types/task.js.map +1 -0
- package/dist/types/client.d.ts +395 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/constants.d.ts +34 -0
- package/dist/types/constants.d.ts.map +1 -0
- package/dist/types/core/DAPBase.d.ts +140 -0
- package/dist/types/core/DAPBase.d.ts.map +1 -0
- package/dist/types/core/DAPClient.d.ts +83 -0
- package/dist/types/core/DAPClient.d.ts.map +1 -0
- package/dist/types/core/TransportBase.d.ts +94 -0
- package/dist/types/core/TransportBase.d.ts.map +1 -0
- package/dist/types/core/TransportWebSocket.d.ts +78 -0
- package/dist/types/core/TransportWebSocket.d.ts.map +1 -0
- package/dist/types/exceptions/index.d.ts +81 -0
- package/dist/types/exceptions/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/schema/Doc.d.ts +69 -0
- package/dist/types/schema/Doc.d.ts.map +1 -0
- package/dist/types/schema/DocFilter.d.ts +101 -0
- package/dist/types/schema/DocFilter.d.ts.map +1 -0
- package/dist/types/schema/DocGroup.d.ts +113 -0
- package/dist/types/schema/DocGroup.d.ts.map +1 -0
- package/dist/types/schema/DocMetadata.d.ts +61 -0
- package/dist/types/schema/DocMetadata.d.ts.map +1 -0
- package/dist/types/schema/Question.d.ts +163 -0
- package/dist/types/schema/Question.d.ts.map +1 -0
- package/dist/types/schema/index.d.ts +34 -0
- package/dist/types/schema/index.d.ts.map +1 -0
- package/dist/types/types/client.d.ts +140 -0
- package/dist/types/types/client.d.ts.map +1 -0
- package/dist/types/types/data.d.ts +95 -0
- package/dist/types/types/data.d.ts.map +1 -0
- package/dist/types/types/events.d.ts +246 -0
- package/dist/types/types/events.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +34 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/types/pipeline.d.ts +61 -0
- package/dist/types/types/pipeline.d.ts.map +1 -0
- package/dist/types/types/task.d.ts +265 -0
- package/dist/types/types/task.d.ts.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2024 Aparavi Development Team
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*/
|
|
26
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
29
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
30
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
31
|
+
}
|
|
32
|
+
Object.defineProperty(o, k2, desc);
|
|
33
|
+
}) : (function(o, m, k, k2) {
|
|
34
|
+
if (k2 === undefined) k2 = k;
|
|
35
|
+
o[k2] = m[k];
|
|
36
|
+
}));
|
|
37
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
38
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
39
|
+
}) : function(o, v) {
|
|
40
|
+
o["default"] = v;
|
|
41
|
+
});
|
|
42
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
43
|
+
var ownKeys = function(o) {
|
|
44
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
45
|
+
var ar = [];
|
|
46
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
47
|
+
return ar;
|
|
48
|
+
};
|
|
49
|
+
return ownKeys(o);
|
|
50
|
+
};
|
|
51
|
+
return function (mod) {
|
|
52
|
+
if (mod && mod.__esModule) return mod;
|
|
53
|
+
var result = {};
|
|
54
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
55
|
+
__setModuleDefault(result, mod);
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
})();
|
|
59
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
+
exports.AparaviCLI = void 0;
|
|
61
|
+
exports.main = main;
|
|
62
|
+
/**
|
|
63
|
+
* Aparavi Unified CLI Client.
|
|
64
|
+
*
|
|
65
|
+
* This module provides a comprehensive command-line interface for managing Aparavi pipelines,
|
|
66
|
+
* uploading files, monitoring task status, and controlling pipeline execution using the
|
|
67
|
+
* Debug Adapter Protocol (DAP).
|
|
68
|
+
*
|
|
69
|
+
* Features:
|
|
70
|
+
* - Start and manage Aparavi data processing pipelines
|
|
71
|
+
* - Upload files with parallel processing and progress tracking
|
|
72
|
+
* - Monitor real-time pipeline status and metrics
|
|
73
|
+
* - Stop running pipelines gracefully
|
|
74
|
+
* - Environment-based configuration via .env files
|
|
75
|
+
*
|
|
76
|
+
* Configuration:
|
|
77
|
+
* The client supports configuration via .env file with the following variables:
|
|
78
|
+
* - APARAVI_APIKEY: Your Aparavi API key (required for authentication)
|
|
79
|
+
* - APARAVI_URI: The Aparavi server URI (defaults to wss://eaas.aparavi.com)
|
|
80
|
+
* - APARAVI_PIPELINE: Path to your default pipeline configuration file
|
|
81
|
+
* - APARAVI_TOKEN: Task token for existing pipelines
|
|
82
|
+
*
|
|
83
|
+
* Commands:
|
|
84
|
+
* - start: Start a new pipeline from configuration file
|
|
85
|
+
* - upload: Upload files to a pipeline (with --pipeline or --token)
|
|
86
|
+
* - status: Monitor real-time status of a running pipeline
|
|
87
|
+
* - stop: Terminate a running pipeline gracefully
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```bash
|
|
91
|
+
* # Start a pipeline
|
|
92
|
+
* aparavi start --pipeline ./my-pipeline.json --apikey YOUR_KEY
|
|
93
|
+
*
|
|
94
|
+
* # Upload files with progress tracking
|
|
95
|
+
* aparavi upload files/*.csv --pipeline ./pipeline.json --max-concurrent 10
|
|
96
|
+
*
|
|
97
|
+
* # Monitor pipeline status
|
|
98
|
+
* aparavi status --token TASK_TOKEN
|
|
99
|
+
*
|
|
100
|
+
* # Stop a pipeline
|
|
101
|
+
* aparavi stop --token TASK_TOKEN
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
const fs = __importStar(require("fs"));
|
|
105
|
+
const path = __importStar(require("path"));
|
|
106
|
+
const glob = __importStar(require("glob"));
|
|
107
|
+
const process = __importStar(require("process"));
|
|
108
|
+
const commander_1 = require("commander");
|
|
109
|
+
const client_1 = require("../src/client");
|
|
110
|
+
// ANSI Color and Control Codes for terminal formatting
|
|
111
|
+
const ANSI_RESET = '\x1b[0m';
|
|
112
|
+
const ANSI_RED = '\x1b[91m';
|
|
113
|
+
const ANSI_GREEN = '\x1b[92m';
|
|
114
|
+
const ANSI_YELLOW = '\x1b[93m';
|
|
115
|
+
const ANSI_BLUE = '\x1b[94m';
|
|
116
|
+
const ANSI_GRAY = '\x1b[90m';
|
|
117
|
+
const ANSI_CLEAR_SCREEN = '\x1b[2J';
|
|
118
|
+
const ANSI_CURSOR_HOME = '\x1b[1;1H';
|
|
119
|
+
// Global character mapping
|
|
120
|
+
let CHR_TL = '┌';
|
|
121
|
+
let CHR_TR = '┐';
|
|
122
|
+
let CHR_BL = '└';
|
|
123
|
+
let CHR_BR = '┘';
|
|
124
|
+
let CHR_HORIZ = '─';
|
|
125
|
+
let CHR_VERT = '│';
|
|
126
|
+
let CHR_BLOCK = '█';
|
|
127
|
+
let CHR_LIGHT_BLOCK = '░';
|
|
128
|
+
let CHR_CHECK = '✓';
|
|
129
|
+
let CHR_CROSS = '✗';
|
|
130
|
+
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[mK]/g;
|
|
131
|
+
class Box {
|
|
132
|
+
constructor(title, lines, width = 75) {
|
|
133
|
+
this.title = title;
|
|
134
|
+
this.lines = lines || [];
|
|
135
|
+
this.width = width;
|
|
136
|
+
}
|
|
137
|
+
visualLength(text) {
|
|
138
|
+
return text.replace(ANSI_ESCAPE_PATTERN, '').length;
|
|
139
|
+
}
|
|
140
|
+
boxTop() {
|
|
141
|
+
const titlePart = ` ${this.title} `;
|
|
142
|
+
const remainingWidth = (this.width - 3) - titlePart.length;
|
|
143
|
+
return CHR_TL + CHR_HORIZ + titlePart + CHR_HORIZ.repeat(Math.max(0, remainingWidth)) + CHR_TR;
|
|
144
|
+
}
|
|
145
|
+
boxMiddle(content) {
|
|
146
|
+
const visualWidth = this.visualLength(content);
|
|
147
|
+
const availableWidth = this.width - 3;
|
|
148
|
+
let finalContent = content;
|
|
149
|
+
if (visualWidth > availableWidth) {
|
|
150
|
+
finalContent = content.substring(0, availableWidth - 3) + '...';
|
|
151
|
+
}
|
|
152
|
+
const padding = availableWidth - this.visualLength(finalContent);
|
|
153
|
+
return CHR_VERT + ' ' + finalContent + ' '.repeat(Math.max(0, padding)) + CHR_VERT;
|
|
154
|
+
}
|
|
155
|
+
boxBottom() {
|
|
156
|
+
return CHR_BL + CHR_HORIZ.repeat(this.width - 2) + CHR_BR;
|
|
157
|
+
}
|
|
158
|
+
render() {
|
|
159
|
+
if (this.lines.length === 0) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const output = [];
|
|
163
|
+
output.push(this.boxTop());
|
|
164
|
+
for (const line of this.lines) {
|
|
165
|
+
output.push(this.boxMiddle(line));
|
|
166
|
+
}
|
|
167
|
+
output.push(this.boxBottom());
|
|
168
|
+
return output;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
class BoxMonitor {
|
|
172
|
+
constructor(cli, commandTitle, width, height) {
|
|
173
|
+
this.boxes = [];
|
|
174
|
+
this.lastLineCount = 0;
|
|
175
|
+
this.screenCleared = false;
|
|
176
|
+
this.commandStatus = ['Initializing...'];
|
|
177
|
+
this.cli = cli;
|
|
178
|
+
this.commandTitle = commandTitle;
|
|
179
|
+
if (width === undefined || height === undefined) {
|
|
180
|
+
const [detectedWidth, detectedHeight] = this.detectTerminalSize();
|
|
181
|
+
this.width = width ?? detectedWidth;
|
|
182
|
+
this.height = height ?? detectedHeight;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.width = width;
|
|
186
|
+
this.height = height;
|
|
187
|
+
}
|
|
188
|
+
this.isTerminal = this.isTerminalCheck();
|
|
189
|
+
}
|
|
190
|
+
detectTerminalSize() {
|
|
191
|
+
try {
|
|
192
|
+
const size = process.stdout.getWindowSize();
|
|
193
|
+
const width = size[0];
|
|
194
|
+
const height = size[1];
|
|
195
|
+
if (width >= 20 && width <= 300 && height >= 10 && height <= 100) {
|
|
196
|
+
return [Math.max(width - 1, 20), Math.max(height - 2, 10)];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Ignore errors
|
|
201
|
+
}
|
|
202
|
+
return [79, 41];
|
|
203
|
+
}
|
|
204
|
+
isTerminalCheck() {
|
|
205
|
+
return process.stdout.isTTY && process.stderr.isTTY;
|
|
206
|
+
}
|
|
207
|
+
updateTerminalSize() {
|
|
208
|
+
const [width, height] = this.detectTerminalSize();
|
|
209
|
+
this.width = width;
|
|
210
|
+
this.height = height;
|
|
211
|
+
}
|
|
212
|
+
clear() {
|
|
213
|
+
this.boxes = [];
|
|
214
|
+
}
|
|
215
|
+
clearScreen() {
|
|
216
|
+
this.screenCleared = false;
|
|
217
|
+
this.lastLineCount = 0;
|
|
218
|
+
}
|
|
219
|
+
setCommandStatus(status) {
|
|
220
|
+
if (typeof status === 'string') {
|
|
221
|
+
this.commandStatus = [status];
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.commandStatus = status;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
addBox(title, lines) {
|
|
228
|
+
if (lines.length > 0) {
|
|
229
|
+
const box = new Box(title, lines, this.width);
|
|
230
|
+
this.boxes.push(box);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
positionToTop() {
|
|
234
|
+
process.stdout.write(ANSI_CURSOR_HOME);
|
|
235
|
+
}
|
|
236
|
+
draw() {
|
|
237
|
+
this.updateTerminalSize();
|
|
238
|
+
if (!this.screenCleared) {
|
|
239
|
+
process.stdout.write(ANSI_CLEAR_SCREEN);
|
|
240
|
+
this.screenCleared = true;
|
|
241
|
+
}
|
|
242
|
+
this.positionToTop();
|
|
243
|
+
const allBoxes = [];
|
|
244
|
+
if (this.commandStatus.length > 0) {
|
|
245
|
+
const commandBox = new Box(this.commandTitle, this.commandStatus, this.width);
|
|
246
|
+
allBoxes.push(commandBox);
|
|
247
|
+
}
|
|
248
|
+
allBoxes.push(...this.boxes);
|
|
249
|
+
const allLines = [];
|
|
250
|
+
for (let i = 0; i < allBoxes.length; i++) {
|
|
251
|
+
const boxLines = allBoxes[i].render();
|
|
252
|
+
allLines.push(...boxLines);
|
|
253
|
+
if (i < allBoxes.length - 1 && boxLines.length > 0) {
|
|
254
|
+
allLines.push(' '.repeat(this.width));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const line of allLines) {
|
|
258
|
+
console.log(line + ' ');
|
|
259
|
+
}
|
|
260
|
+
const currentLineCount = allLines.length;
|
|
261
|
+
if (currentLineCount < this.lastLineCount) {
|
|
262
|
+
for (let i = currentLineCount; i < this.lastLineCount; i++) {
|
|
263
|
+
console.log(' '.repeat(this.width));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.lastLineCount = currentLineCount;
|
|
267
|
+
process.stdout.write('');
|
|
268
|
+
}
|
|
269
|
+
formatSize(sizeBytes) {
|
|
270
|
+
if (sizeBytes === 0)
|
|
271
|
+
return '0 B';
|
|
272
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
273
|
+
let unitIndex = 0;
|
|
274
|
+
let size = sizeBytes;
|
|
275
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
276
|
+
size /= 1024;
|
|
277
|
+
unitIndex++;
|
|
278
|
+
}
|
|
279
|
+
if (unitIndex === 0) {
|
|
280
|
+
return `${Math.floor(size)} ${units[unitIndex]}`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
onEvent(message) {
|
|
287
|
+
// Override in subclasses
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
class GenericMonitor extends BoxMonitor {
|
|
291
|
+
constructor(cli, commandTitle, width, height) {
|
|
292
|
+
super(cli, commandTitle, width, height);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
class StatusMonitor extends BoxMonitor {
|
|
296
|
+
constructor(cli, token, width, height) {
|
|
297
|
+
super(cli, 'Aparavi Task Monitor', width, height);
|
|
298
|
+
this.token = token;
|
|
299
|
+
this.setCommandStatus(`Token: ${this.token}`);
|
|
300
|
+
}
|
|
301
|
+
formatDuration(startTime, endTime) {
|
|
302
|
+
if (startTime === 0)
|
|
303
|
+
return 'Not started';
|
|
304
|
+
const end = endTime || Date.now() / 1000;
|
|
305
|
+
const totalSeconds = Math.floor(end - startTime);
|
|
306
|
+
if (totalSeconds < 60) {
|
|
307
|
+
return `${totalSeconds}secs`;
|
|
308
|
+
}
|
|
309
|
+
else if (totalSeconds < 3600) {
|
|
310
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
311
|
+
const seconds = totalSeconds % 60;
|
|
312
|
+
return `${minutes}min, ${seconds}secs`;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
316
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
317
|
+
const seconds = totalSeconds % 60;
|
|
318
|
+
return `${hours}hr, ${minutes}min, ${seconds}secs`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
getStateDisplay(state) {
|
|
322
|
+
const stateMap = {
|
|
323
|
+
0: ['Offline', ANSI_GRAY],
|
|
324
|
+
1: ['Offline', ANSI_GRAY],
|
|
325
|
+
2: ['Initializing', ANSI_BLUE],
|
|
326
|
+
3: ['Online', ANSI_GREEN],
|
|
327
|
+
4: ['Stopping', ANSI_YELLOW],
|
|
328
|
+
5: ['Offline', ANSI_GRAY],
|
|
329
|
+
6: ['Offline', ANSI_GRAY]
|
|
330
|
+
};
|
|
331
|
+
return stateMap[state] || ['Unknown', ANSI_RESET];
|
|
332
|
+
}
|
|
333
|
+
hasCountData(status) {
|
|
334
|
+
return ((status.totalSize || 0) > 0 ||
|
|
335
|
+
(status.totalCount || 0) > 0 ||
|
|
336
|
+
(status.completedSize || 0) > 0 ||
|
|
337
|
+
(status.completedCount || 0) > 0 ||
|
|
338
|
+
(status.failedSize || 0) > 0 ||
|
|
339
|
+
(status.failedCount || 0) > 0 ||
|
|
340
|
+
(status.rateSize || 0) > 0 ||
|
|
341
|
+
(status.rateCount || 0) > 0);
|
|
342
|
+
}
|
|
343
|
+
hasMetricsData(status) {
|
|
344
|
+
const metrics = status.metrics || {};
|
|
345
|
+
return Object.values(metrics).some(value => typeof value === 'number' && value > 0);
|
|
346
|
+
}
|
|
347
|
+
onEvent(message) {
|
|
348
|
+
const eventType = message.event || '';
|
|
349
|
+
if (eventType !== 'apaevt_status_update') {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const status = message.body || {};
|
|
353
|
+
this.displayStatus(status);
|
|
354
|
+
}
|
|
355
|
+
displayStatus(status) {
|
|
356
|
+
this.clear();
|
|
357
|
+
if (status) {
|
|
358
|
+
const pipelineLines = this.buildPipelineLines(status);
|
|
359
|
+
this.addBox('Pipeline Status', pipelineLines);
|
|
360
|
+
const metricsLines = this.buildMetricsLines(status);
|
|
361
|
+
this.addBox('Metrics', metricsLines);
|
|
362
|
+
const errorLines = this.buildErrorLines(status.errors || [], 'Error');
|
|
363
|
+
this.addBox('Errors', errorLines);
|
|
364
|
+
const warningLines = this.buildErrorLines(status.warnings || [], 'Warning');
|
|
365
|
+
this.addBox('Warnings', warningLines);
|
|
366
|
+
const noteLines = this.buildNoteLines(status.notes || []);
|
|
367
|
+
this.addBox('Notes', noteLines);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
this.addBox('Status', ['No status available']);
|
|
371
|
+
}
|
|
372
|
+
this.draw();
|
|
373
|
+
}
|
|
374
|
+
buildPipelineLines(status) {
|
|
375
|
+
const lines = [];
|
|
376
|
+
if (status.name) {
|
|
377
|
+
lines.push(status.name);
|
|
378
|
+
lines.push('');
|
|
379
|
+
}
|
|
380
|
+
if (status.status) {
|
|
381
|
+
lines.push(status.status);
|
|
382
|
+
lines.push('');
|
|
383
|
+
}
|
|
384
|
+
const state = status.state || 0;
|
|
385
|
+
const [stateName, stateColor] = this.getStateDisplay(state);
|
|
386
|
+
lines.push(`State: ${stateColor}${stateName}${ANSI_RESET}`);
|
|
387
|
+
const startTime = status.startTime || 0;
|
|
388
|
+
if (startTime > 0) {
|
|
389
|
+
const startStr = new Date(startTime * 1000).toLocaleString();
|
|
390
|
+
lines.push(`Started: ${startStr}`);
|
|
391
|
+
const endTime = status.completed ? (status.endTime || 0) : undefined;
|
|
392
|
+
const duration = this.formatDuration(startTime, endTime);
|
|
393
|
+
lines.push(`Elapsed: ${duration}`);
|
|
394
|
+
}
|
|
395
|
+
if (this.hasCountData(status)) {
|
|
396
|
+
lines.push('');
|
|
397
|
+
const dataTypes = [
|
|
398
|
+
['total', 'Total'],
|
|
399
|
+
['completed', 'Completed'],
|
|
400
|
+
['failed', 'Failed']
|
|
401
|
+
];
|
|
402
|
+
for (const [keyBase, label] of dataTypes) {
|
|
403
|
+
const count = status[`${keyBase}Count`] || 0;
|
|
404
|
+
const size = status[`${keyBase}Size`] || 0;
|
|
405
|
+
if (count > 0 || size > 0) {
|
|
406
|
+
lines.push(`${label}: ${count} items (${this.formatSize(size)})`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const rateSize = status.rateSize || 0;
|
|
410
|
+
const rateCount = status.rateCount || 0;
|
|
411
|
+
if (rateSize > 0 || rateCount > 0) {
|
|
412
|
+
lines.push(`Rate: ${this.formatSize(rateSize)}/s (${rateCount}/s items)`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return lines.length > 0 ? lines : ['No pipeline data available'];
|
|
416
|
+
}
|
|
417
|
+
buildMetricsLines(status) {
|
|
418
|
+
if (!this.hasMetricsData(status)) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
const lines = [];
|
|
422
|
+
const metrics = status.metrics || {};
|
|
423
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
424
|
+
if (typeof value === 'number' && value > 0) {
|
|
425
|
+
const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
426
|
+
lines.push(`${label}: ${value}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return lines;
|
|
430
|
+
}
|
|
431
|
+
buildErrorLines(items, errorType) {
|
|
432
|
+
if (!items || items.length === 0) {
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
const lines = [];
|
|
436
|
+
const color = errorType === 'Error' ? ANSI_RED : ANSI_YELLOW;
|
|
437
|
+
for (const item of items.slice(-5)) {
|
|
438
|
+
const parts = item.split('*');
|
|
439
|
+
if (parts.length >= 3) {
|
|
440
|
+
const errType = parts[0].trim();
|
|
441
|
+
const message = parts[1].replace(/`/g, '').trim();
|
|
442
|
+
const fileInfo = parts[2].trim();
|
|
443
|
+
const filename = fileInfo.includes('\\') || fileInfo.includes('/') ?
|
|
444
|
+
path.basename(fileInfo) : fileInfo;
|
|
445
|
+
lines.push(`${color}${errType}${ANSI_RESET}: ${message}`);
|
|
446
|
+
if (filename) {
|
|
447
|
+
lines.push(` -> ${filename}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
lines.push(`${color}• ${ANSI_RESET}${item}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return lines;
|
|
455
|
+
}
|
|
456
|
+
buildNoteLines(items) {
|
|
457
|
+
if (!items || items.length === 0) {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
return items.slice(-5).map(item => `• ${item}`);
|
|
461
|
+
}
|
|
462
|
+
displayConnecting(url, attempt) {
|
|
463
|
+
this.clear();
|
|
464
|
+
const retry = attempt > 0 ? ` (attempt ${attempt})` : '';
|
|
465
|
+
const connectionLines = [`Connecting to ${url}${retry}...`];
|
|
466
|
+
this.addBox('Connection Status', connectionLines);
|
|
467
|
+
this.addBox('Controls', ['Press Ctrl+C to stop monitoring...']);
|
|
468
|
+
this.draw();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
class UploadProgressMonitor extends BoxMonitor {
|
|
472
|
+
constructor(cli, width, height) {
|
|
473
|
+
super(cli, 'Aparavi File Upload', width, height);
|
|
474
|
+
this.totalFiles = 0;
|
|
475
|
+
this.activeUploads = new Map();
|
|
476
|
+
this.completedUploads = new Map();
|
|
477
|
+
this.failedUploads = new Map();
|
|
478
|
+
this.setCommandStatus('Preparing upload...');
|
|
479
|
+
}
|
|
480
|
+
createProgressBar(percent, width = 30) {
|
|
481
|
+
const filledLength = Math.floor(width * percent / 100);
|
|
482
|
+
const bar = CHR_BLOCK.repeat(filledLength) + CHR_LIGHT_BLOCK.repeat(width - filledLength);
|
|
483
|
+
return `[${bar}] ${percent.toFixed(1).padStart(5)}%`;
|
|
484
|
+
}
|
|
485
|
+
truncateFilename(filename, maxLength) {
|
|
486
|
+
if (filename.length <= maxLength) {
|
|
487
|
+
return filename;
|
|
488
|
+
}
|
|
489
|
+
const ext = path.extname(filename);
|
|
490
|
+
const name = path.basename(filename, ext);
|
|
491
|
+
if (ext.length < maxLength - 3) {
|
|
492
|
+
const available = maxLength - ext.length - 3;
|
|
493
|
+
return `${name.substring(0, available)}...${ext}`;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
return `${filename.substring(0, maxLength - 3)}...`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
setTotalFiles(totalFiles) {
|
|
500
|
+
this.totalFiles = totalFiles;
|
|
501
|
+
}
|
|
502
|
+
onUploadProgress(result) {
|
|
503
|
+
// Process UPLOAD_RESULT from sendFiles
|
|
504
|
+
const filepath = result.filepath || 'unknown';
|
|
505
|
+
const bytesSent = result.bytes_sent || 0;
|
|
506
|
+
const fileSize = result.file_size || 0;
|
|
507
|
+
const action = result.action;
|
|
508
|
+
const filename = path.basename(filepath);
|
|
509
|
+
if (action === 'open') {
|
|
510
|
+
// Don't show pending opens
|
|
511
|
+
}
|
|
512
|
+
else if (action === 'write') {
|
|
513
|
+
this.activeUploads.set(filename, {
|
|
514
|
+
filepath,
|
|
515
|
+
action,
|
|
516
|
+
bytes_sent: bytesSent,
|
|
517
|
+
file_size: fileSize
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
else if (action === 'close') {
|
|
521
|
+
const existing = this.activeUploads.get(filename);
|
|
522
|
+
if (existing) {
|
|
523
|
+
this.activeUploads.set(filename, {
|
|
524
|
+
...existing,
|
|
525
|
+
action,
|
|
526
|
+
bytes_sent: bytesSent,
|
|
527
|
+
file_size: fileSize
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else if (action === 'complete') {
|
|
532
|
+
this.activeUploads.delete(filename);
|
|
533
|
+
this.completedUploads.set(filename, {
|
|
534
|
+
filepath,
|
|
535
|
+
action,
|
|
536
|
+
file_size: fileSize
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
else if (action === 'error') {
|
|
540
|
+
this.activeUploads.delete(filename);
|
|
541
|
+
const errorMessage = result.error || 'Unknown error';
|
|
542
|
+
this.failedUploads.set(filename, {
|
|
543
|
+
filepath,
|
|
544
|
+
action,
|
|
545
|
+
file_size: fileSize,
|
|
546
|
+
error: errorMessage
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
this.draw();
|
|
550
|
+
}
|
|
551
|
+
onEvent(message) {
|
|
552
|
+
const eventType = message.event || '';
|
|
553
|
+
if (eventType !== 'apaevt_status_upload') {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const body = message.body || {};
|
|
557
|
+
const filepath = body.filepath || 'unknown';
|
|
558
|
+
const bytesSent = body.bytes_sent || 0;
|
|
559
|
+
const fileSize = body.file_size || 0;
|
|
560
|
+
const action = body.action;
|
|
561
|
+
const filename = path.basename(filepath);
|
|
562
|
+
if (action === 'open') {
|
|
563
|
+
// Don't show pending opens
|
|
564
|
+
}
|
|
565
|
+
else if (action === 'write') {
|
|
566
|
+
this.activeUploads.set(filename, {
|
|
567
|
+
filepath,
|
|
568
|
+
action,
|
|
569
|
+
bytes_sent: bytesSent,
|
|
570
|
+
file_size: fileSize
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
else if (action === 'close') {
|
|
574
|
+
const existing = this.activeUploads.get(filename);
|
|
575
|
+
if (existing) {
|
|
576
|
+
this.activeUploads.set(filename, {
|
|
577
|
+
...existing,
|
|
578
|
+
action,
|
|
579
|
+
bytes_sent: bytesSent,
|
|
580
|
+
file_size: fileSize
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else if (action === 'complete') {
|
|
585
|
+
this.activeUploads.delete(filename);
|
|
586
|
+
this.completedUploads.set(filename, {
|
|
587
|
+
filepath,
|
|
588
|
+
action,
|
|
589
|
+
file_size: fileSize
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else if (action === 'error') {
|
|
593
|
+
this.activeUploads.delete(filename);
|
|
594
|
+
const errorMessage = body.error || 'Unknown error';
|
|
595
|
+
this.failedUploads.set(filename, {
|
|
596
|
+
filepath,
|
|
597
|
+
action,
|
|
598
|
+
file_size: fileSize,
|
|
599
|
+
error: errorMessage
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
if (this.cli.isCancelled()) {
|
|
603
|
+
this.setCommandStatus(`${ANSI_RED}Upload cancelling...${ANSI_RESET}`);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
const totalProcessed = this.completedUploads.size + this.failedUploads.size;
|
|
607
|
+
this.setCommandStatus(`Processed ${totalProcessed} of ${this.totalFiles} files...`);
|
|
608
|
+
}
|
|
609
|
+
this.renderUploadStatus();
|
|
610
|
+
if (this.cli.isCancelled()) {
|
|
611
|
+
throw new Error('Upload cancelled');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
renderUploadStatus() {
|
|
615
|
+
this.clear();
|
|
616
|
+
// Add active uploads box
|
|
617
|
+
if (this.activeUploads.size > 0) {
|
|
618
|
+
const uploadLines = [];
|
|
619
|
+
const activeEntries = Array.from(this.activeUploads.entries());
|
|
620
|
+
for (const [filename, data] of activeEntries.slice(0, 10)) {
|
|
621
|
+
const displayName = this.truncateFilename(filename, 20).padEnd(20);
|
|
622
|
+
const action = data.action;
|
|
623
|
+
const phase = action === 'write' ? 'Writing ' : action === 'close' ? 'Finalize' : ' ';
|
|
624
|
+
const bytesSent = data.bytes_sent || 0;
|
|
625
|
+
const fileSize = data.file_size || 1;
|
|
626
|
+
const percent = fileSize > 0 ? (bytesSent / fileSize * 100) : 0;
|
|
627
|
+
const progressBar = this.createProgressBar(percent, 12);
|
|
628
|
+
const sizeInfo = `${this.formatSize(bytesSent)}/${this.formatSize(fileSize)}`;
|
|
629
|
+
uploadLines.push(`${displayName} ${phase} ${progressBar} ${sizeInfo}`);
|
|
630
|
+
}
|
|
631
|
+
if (this.activeUploads.size > 10) {
|
|
632
|
+
const remaining = this.activeUploads.size - 10;
|
|
633
|
+
uploadLines.push(`... and ${remaining} more uploads in progress`);
|
|
634
|
+
}
|
|
635
|
+
this.addBox(`Active Uploads (${this.activeUploads.size})`, uploadLines);
|
|
636
|
+
}
|
|
637
|
+
// Add summary box
|
|
638
|
+
if (this.completedUploads.size > 0 || this.failedUploads.size > 0) {
|
|
639
|
+
const summaryLines = [];
|
|
640
|
+
if (this.completedUploads.size > 0) {
|
|
641
|
+
summaryLines.push(`Completed: ${this.completedUploads.size} files`);
|
|
642
|
+
}
|
|
643
|
+
if (this.failedUploads.size > 0) {
|
|
644
|
+
summaryLines.push(`Failed: ${this.failedUploads.size} files`);
|
|
645
|
+
}
|
|
646
|
+
const totalBytes = Array.from(this.completedUploads.values())
|
|
647
|
+
.reduce((sum, data) => sum + (data.file_size || 0), 0);
|
|
648
|
+
summaryLines.push(`Total size: ${this.formatSize(totalBytes)}`);
|
|
649
|
+
this.addBox('Upload Summary', summaryLines);
|
|
650
|
+
}
|
|
651
|
+
// Add failed uploads box
|
|
652
|
+
if (this.failedUploads.size > 0) {
|
|
653
|
+
const failedLines = [];
|
|
654
|
+
const failedEntries = Array.from(this.failedUploads.entries());
|
|
655
|
+
const displayCount = failedEntries.length > 5 ? 4 : 5;
|
|
656
|
+
for (const [filename, data] of failedEntries.slice(-displayCount)) {
|
|
657
|
+
const displayName = this.truncateFilename(filename, 25);
|
|
658
|
+
const errorMsg = (data.error || '').length > 30 ?
|
|
659
|
+
`${data.error.substring(0, 30)}...` : data.error;
|
|
660
|
+
failedLines.push(`${ANSI_RED}${CHR_CROSS}${ANSI_RESET} ${displayName} - ${errorMsg}`);
|
|
661
|
+
}
|
|
662
|
+
if (failedEntries.length > 5) {
|
|
663
|
+
const remaining = failedEntries.length - 4;
|
|
664
|
+
failedLines.push(`... and ${remaining} more files have failed`);
|
|
665
|
+
}
|
|
666
|
+
this.addBox(`Failed Uploads (${this.failedUploads.size})`, failedLines);
|
|
667
|
+
}
|
|
668
|
+
// Add recently completed box
|
|
669
|
+
if (this.completedUploads.size > 0) {
|
|
670
|
+
const completedLines = [];
|
|
671
|
+
const completedEntries = Array.from(this.completedUploads.entries());
|
|
672
|
+
for (const [filename, data] of completedEntries.slice(-3)) {
|
|
673
|
+
const displayName = this.truncateFilename(filename, 35);
|
|
674
|
+
const sizeStr = this.formatSize(data.file_size || 0);
|
|
675
|
+
completedLines.push(`${ANSI_GREEN}${CHR_CHECK}${ANSI_RESET} ${displayName} (${sizeStr})`);
|
|
676
|
+
}
|
|
677
|
+
this.addBox('Recently Completed', completedLines);
|
|
678
|
+
}
|
|
679
|
+
this.draw();
|
|
680
|
+
}
|
|
681
|
+
reset() {
|
|
682
|
+
this.activeUploads.clear();
|
|
683
|
+
this.completedUploads.clear();
|
|
684
|
+
this.failedUploads.clear();
|
|
685
|
+
this.setCommandStatus('Preparing upload...');
|
|
686
|
+
this.clearScreen();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
class AparaviCLI {
|
|
690
|
+
constructor() {
|
|
691
|
+
this.uploadStats = {
|
|
692
|
+
files_processed: 0,
|
|
693
|
+
total_bytes: 0,
|
|
694
|
+
successful_uploads: 0,
|
|
695
|
+
failed_uploads: 0,
|
|
696
|
+
upload_times: []
|
|
697
|
+
};
|
|
698
|
+
this.uri = '';
|
|
699
|
+
this.connected = false;
|
|
700
|
+
this.attempt = 0;
|
|
701
|
+
this.cancelled = false;
|
|
702
|
+
this.setupSignalHandlers();
|
|
703
|
+
}
|
|
704
|
+
cancel() {
|
|
705
|
+
this.cancelled = true;
|
|
706
|
+
}
|
|
707
|
+
isCancelled() {
|
|
708
|
+
return this.cancelled;
|
|
709
|
+
}
|
|
710
|
+
setupSignalHandlers() {
|
|
711
|
+
// TODO: Enable proper signal handling
|
|
712
|
+
// const signalHandler = () => {
|
|
713
|
+
// this.cancel();
|
|
714
|
+
// };
|
|
715
|
+
// process.on('SIGINT', signalHandler);
|
|
716
|
+
// process.on('SIGTERM', signalHandler);
|
|
717
|
+
}
|
|
718
|
+
createProgram() {
|
|
719
|
+
const program = new commander_1.Command();
|
|
720
|
+
program
|
|
721
|
+
.name('aparavi')
|
|
722
|
+
.description('Aparavi Unified Pipeline and File Management CLI')
|
|
723
|
+
.version('1.3.0');
|
|
724
|
+
// Common options
|
|
725
|
+
const addCommonOptions = (cmd) => {
|
|
726
|
+
return cmd
|
|
727
|
+
.option('--host <hostname>', 'Aparavi DAP server hostname', 'eaas.aparavi.com')
|
|
728
|
+
.option('--port <port>', 'Aparavi DAP server port', '443')
|
|
729
|
+
.option('--apikey <key>', 'API key for Aparavi server authentication (can use APARAVI_APIKEY in .env or env var)', process.env.APARAVI_APIKEY);
|
|
730
|
+
};
|
|
731
|
+
// Start command
|
|
732
|
+
const startCmd = program
|
|
733
|
+
.command('start')
|
|
734
|
+
.description('Start a new pipeline')
|
|
735
|
+
.option('--pipeline <file>', 'Path to .pipeline file containing pipeline configuration (can use APARAVI_PIPELINE in .env or env var)', process.env.APARAVI_PIPELINE)
|
|
736
|
+
.option('--token <token>', 'Optional existing task token for pipeline resume/control (can use APARAVI_TOKEN in .env or env var)', process.env.APARAVI_TOKEN)
|
|
737
|
+
.option('--threads <num>', 'Number of threads to use for pipeline execution', '4')
|
|
738
|
+
.option('--args <args...>', 'Additional arguments to pass to pipeline execution')
|
|
739
|
+
.action(async (options) => {
|
|
740
|
+
// Validate required arguments - validation will happen in createAndConnectClient
|
|
741
|
+
if (!options.pipeline) {
|
|
742
|
+
console.error('Error: Pipeline file is required for start command. Use --pipeline or set APARAVI_PIPELINE in .env file');
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
this.args = {
|
|
746
|
+
command: 'start',
|
|
747
|
+
...options,
|
|
748
|
+
pipeline: options.pipeline,
|
|
749
|
+
port: parseInt(options.port),
|
|
750
|
+
threads: parseInt(options.threads)
|
|
751
|
+
};
|
|
752
|
+
const protocol = this.args.port === 443 ? 'wss' : 'ws';
|
|
753
|
+
this.uri = `${protocol}://${this.args.host}:${this.args.port}`;
|
|
754
|
+
try {
|
|
755
|
+
const exitCode = await this.cmdStart();
|
|
756
|
+
process.exit(exitCode);
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
this.cancel();
|
|
760
|
+
await this.cleanupClient();
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
addCommonOptions(startCmd);
|
|
764
|
+
// Upload command
|
|
765
|
+
const uploadCmd = program
|
|
766
|
+
.command('upload')
|
|
767
|
+
.description('Upload files using --pipeline or an existing task token')
|
|
768
|
+
.argument('<files...>', 'Files, wildcards, or directories to upload')
|
|
769
|
+
.option('--pipeline <file>', 'Pipeline file to start new task (can use APARAVI_PIPELINE in .env or env var)', process.env.APARAVI_PIPELINE)
|
|
770
|
+
.option('--token <token>', 'Existing task token to use for uploads (can use APARAVI_TOKEN in .env or env var)', process.env.APARAVI_TOKEN)
|
|
771
|
+
.option('--threads <num>', 'Number of threads to use for pipeline execution', '4')
|
|
772
|
+
.option('--max-concurrent <num>', 'Maximum number of concurrent file uploads', '5')
|
|
773
|
+
.option('--args <args...>', 'Additional arguments to pass to pipeline execution')
|
|
774
|
+
.action(async (files, options) => {
|
|
775
|
+
// Validate required arguments - validation will happen in createAndConnectClient
|
|
776
|
+
if (!options.pipeline && !options.token) {
|
|
777
|
+
console.error('Error: Either --pipeline or --token must be specified for upload command. Use --pipeline/--token or set APARAVI_PIPELINE/APARAVI_TOKEN in .env file');
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
this.args = {
|
|
781
|
+
command: 'upload',
|
|
782
|
+
...options,
|
|
783
|
+
files,
|
|
784
|
+
port: parseInt(options.port),
|
|
785
|
+
threads: parseInt(options.threads),
|
|
786
|
+
max_concurrent: parseInt(options.maxConcurrent || '5'),
|
|
787
|
+
pipeline_args: options.args
|
|
788
|
+
};
|
|
789
|
+
const protocol = this.args.port === 443 ? 'wss' : 'ws';
|
|
790
|
+
this.uri = `${protocol}://${this.args.host}:${this.args.port}`;
|
|
791
|
+
try {
|
|
792
|
+
const exitCode = await this.cmdUpload();
|
|
793
|
+
process.exit(exitCode);
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
this.cancel();
|
|
797
|
+
await this.cleanupClient();
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
addCommonOptions(uploadCmd);
|
|
801
|
+
// Status command
|
|
802
|
+
const statusCmd = program
|
|
803
|
+
.command('status')
|
|
804
|
+
.description('Monitor task status continuously')
|
|
805
|
+
.option('--token <token>', 'Task token to monitor (can use APARAVI_TOKEN in .env or env var)', process.env.APARAVI_TOKEN)
|
|
806
|
+
.action(async (options) => {
|
|
807
|
+
// Validate required arguments - validation will happen in createAndConnectClient
|
|
808
|
+
if (!options.token) {
|
|
809
|
+
console.error('Error: Token is required for status command. Use --token or set APARAVI_TOKEN in .env file');
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
this.args = {
|
|
813
|
+
command: 'status',
|
|
814
|
+
...options,
|
|
815
|
+
port: parseInt(options.port)
|
|
816
|
+
};
|
|
817
|
+
const protocol = this.args.port === 443 ? 'wss' : 'ws';
|
|
818
|
+
this.uri = `${protocol}://${this.args.host}:${this.args.port}`;
|
|
819
|
+
try {
|
|
820
|
+
const exitCode = await this.cmdStatus();
|
|
821
|
+
process.exit(exitCode);
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
this.cancel();
|
|
825
|
+
await this.cleanupClient();
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
addCommonOptions(statusCmd);
|
|
829
|
+
// Stop command
|
|
830
|
+
const stopCmd = program
|
|
831
|
+
.command('stop')
|
|
832
|
+
.description('Stop a running task')
|
|
833
|
+
.option('--token <token>', 'Task token to stop (can use APARAVI_TOKEN in .env or env var)', process.env.APARAVI_TOKEN)
|
|
834
|
+
.action(async (options) => {
|
|
835
|
+
// Validate required arguments - validation will happen in createAndConnectClient
|
|
836
|
+
if (!options.token) {
|
|
837
|
+
console.error('Error: Token is required for stop command. Use --token or set APARAVI_TOKEN in .env file');
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
this.args = {
|
|
841
|
+
command: 'stop',
|
|
842
|
+
...options,
|
|
843
|
+
port: parseInt(options.port)
|
|
844
|
+
};
|
|
845
|
+
const protocol = this.args.port === 443 ? 'wss' : 'ws';
|
|
846
|
+
this.uri = `${protocol}://${this.args.host}:${this.args.port}`;
|
|
847
|
+
try {
|
|
848
|
+
const exitCode = await this.cmdStop();
|
|
849
|
+
process.exit(exitCode);
|
|
850
|
+
}
|
|
851
|
+
finally {
|
|
852
|
+
this.cancel();
|
|
853
|
+
await this.cleanupClient();
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
addCommonOptions(stopCmd);
|
|
857
|
+
return program;
|
|
858
|
+
}
|
|
859
|
+
async handleEvent(message) {
|
|
860
|
+
if (this.monitor) {
|
|
861
|
+
this.monitor.onEvent(message);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async createAndConnectClient(onConnected, onDisconnected) {
|
|
865
|
+
const uri = `${this.uri}/task/service`;
|
|
866
|
+
this.client = new client_1.AparaviClient({
|
|
867
|
+
uri,
|
|
868
|
+
auth: this.args.apikey,
|
|
869
|
+
onEvent: this.handleEvent.bind(this),
|
|
870
|
+
onConnected,
|
|
871
|
+
onDisconnected
|
|
872
|
+
});
|
|
873
|
+
await this.client.connect();
|
|
874
|
+
return this.client;
|
|
875
|
+
}
|
|
876
|
+
async sendMonitorCommand(subscribe, token) {
|
|
877
|
+
try {
|
|
878
|
+
if (!this.client)
|
|
879
|
+
return false;
|
|
880
|
+
const arguments_ = { subscribe };
|
|
881
|
+
const monitorRequest = this.client.buildRequest('apaext_monitor', {
|
|
882
|
+
token,
|
|
883
|
+
arguments: arguments_
|
|
884
|
+
});
|
|
885
|
+
const monitorResponse = await this.client.request(monitorRequest);
|
|
886
|
+
return !this.client.didFail(monitorResponse);
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async cleanupClient() {
|
|
893
|
+
if (this.client) {
|
|
894
|
+
try {
|
|
895
|
+
await this.client.disconnect();
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
// Ignore cleanup errors
|
|
899
|
+
}
|
|
900
|
+
finally {
|
|
901
|
+
this.client = undefined;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
loadPipelineConfig(pipelineFile) {
|
|
906
|
+
if (!fs.existsSync(pipelineFile) || !fs.statSync(pipelineFile).isFile()) {
|
|
907
|
+
throw new Error(`Pipeline file not found: ${pipelineFile}`);
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
const content = fs.readFileSync(pipelineFile, 'utf-8');
|
|
911
|
+
try {
|
|
912
|
+
return JSON.parse(content);
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
throw new Error(`Invalid JSON format in ${pipelineFile}: ${error}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
catch (error) {
|
|
919
|
+
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('Invalid'))) {
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
else {
|
|
923
|
+
throw new Error(`Error reading ${pipelineFile}: ${error}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async cmdStart() {
|
|
928
|
+
try {
|
|
929
|
+
this.monitor = new GenericMonitor(this, 'Aparavi Pipeline Execution');
|
|
930
|
+
this.monitor.setCommandStatus('Loading pipeline configuration...');
|
|
931
|
+
this.monitor.draw();
|
|
932
|
+
const pipelineData = this.loadPipelineConfig(this.args.pipeline);
|
|
933
|
+
this.monitor.setCommandStatus(['Pipeline loaded successfully', 'Connecting to server...']);
|
|
934
|
+
this.monitor.draw();
|
|
935
|
+
await this.createAndConnectClient();
|
|
936
|
+
this.monitor.setCommandStatus(['Connected to server', 'Starting pipeline execution...']);
|
|
937
|
+
this.monitor.draw();
|
|
938
|
+
const taskToken = await this.client.use({
|
|
939
|
+
pipeline: pipelineData,
|
|
940
|
+
threads: this.args.threads,
|
|
941
|
+
token: this.args.token,
|
|
942
|
+
args: this.args.pipeline_args || []
|
|
943
|
+
});
|
|
944
|
+
const executionLines = [
|
|
945
|
+
'Pipeline execution started successfully',
|
|
946
|
+
`Task token: ${taskToken}`,
|
|
947
|
+
'',
|
|
948
|
+
'Use the following command to monitor status:',
|
|
949
|
+
`aparavi status --token ${taskToken} --apikey ${this.args.apikey}`
|
|
950
|
+
];
|
|
951
|
+
this.monitor.setCommandStatus(executionLines);
|
|
952
|
+
this.monitor.draw();
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
catch (error) {
|
|
956
|
+
if (!this.monitor) {
|
|
957
|
+
this.monitor = new GenericMonitor(this, 'Aparavi Pipeline Execution');
|
|
958
|
+
}
|
|
959
|
+
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('Invalid'))) {
|
|
960
|
+
this.monitor.setCommandStatus('Configuration error occurred');
|
|
961
|
+
this.monitor.addBox('Configuration Error', [error.message]);
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
this.monitor.setCommandStatus('Execution failed');
|
|
965
|
+
this.monitor.addBox('Execution Error', [String(error)]);
|
|
966
|
+
}
|
|
967
|
+
this.monitor.draw();
|
|
968
|
+
return 1;
|
|
969
|
+
}
|
|
970
|
+
finally {
|
|
971
|
+
await this.cleanupClient();
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
async cmdUpload() {
|
|
975
|
+
try {
|
|
976
|
+
this.monitor = new UploadProgressMonitor(this);
|
|
977
|
+
this.uploadStats = {
|
|
978
|
+
files_processed: 0,
|
|
979
|
+
total_bytes: 0,
|
|
980
|
+
successful_uploads: 0,
|
|
981
|
+
failed_uploads: 0,
|
|
982
|
+
upload_times: []
|
|
983
|
+
};
|
|
984
|
+
let pipelineConfig;
|
|
985
|
+
let taskToken;
|
|
986
|
+
let shouldManagePipeline = false;
|
|
987
|
+
if (this.args.pipeline) {
|
|
988
|
+
this.monitor.setCommandStatus('Loading pipeline configuration...');
|
|
989
|
+
this.monitor.draw();
|
|
990
|
+
pipelineConfig = this.loadPipelineConfig(this.args.pipeline);
|
|
991
|
+
shouldManagePipeline = true;
|
|
992
|
+
}
|
|
993
|
+
else if (this.args.token) {
|
|
994
|
+
taskToken = this.args.token;
|
|
995
|
+
this.monitor.setCommandStatus('Using existing task token...');
|
|
996
|
+
this.monitor.draw();
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
this.monitor.setCommandStatus('Configuration error');
|
|
1000
|
+
this.monitor.addBox('Upload Error', ['Either --pipeline or --token must be specified for upload command']);
|
|
1001
|
+
this.monitor.draw();
|
|
1002
|
+
return 1;
|
|
1003
|
+
}
|
|
1004
|
+
// Find and validate files
|
|
1005
|
+
this.monitor.setCommandStatus(`Discovering files from ${(this.args.files || []).length} patterns...`);
|
|
1006
|
+
this.monitor.draw();
|
|
1007
|
+
const allFiles = this.findFiles(this.args.files || []);
|
|
1008
|
+
if (allFiles.length === 0) {
|
|
1009
|
+
this.monitor.setCommandStatus('File discovery failed');
|
|
1010
|
+
this.monitor.addBox('Upload Error', ['No files found matching the specified patterns!']);
|
|
1011
|
+
this.monitor.draw();
|
|
1012
|
+
return 1;
|
|
1013
|
+
}
|
|
1014
|
+
this.monitor.setCommandStatus(`Validating ${allFiles.length} files...`);
|
|
1015
|
+
this.monitor.draw();
|
|
1016
|
+
const [validFiles, invalidFiles] = this.validateFiles(allFiles);
|
|
1017
|
+
if (invalidFiles.length > 0) {
|
|
1018
|
+
const validationErrorLines = [];
|
|
1019
|
+
const displayCount = Math.min(invalidFiles.length, 15);
|
|
1020
|
+
for (const error of invalidFiles.slice(0, displayCount)) {
|
|
1021
|
+
validationErrorLines.push(`${ANSI_RED}${CHR_CROSS}${ANSI_RESET} ${error}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (invalidFiles.length > 15) {
|
|
1024
|
+
const remaining = invalidFiles.length - 15;
|
|
1025
|
+
validationErrorLines.push(`... and ${remaining} more validation errors`);
|
|
1026
|
+
}
|
|
1027
|
+
this.monitor.setCommandStatus('File validation completed with errors');
|
|
1028
|
+
this.monitor.addBox('File Validation Errors', validationErrorLines);
|
|
1029
|
+
this.monitor.draw();
|
|
1030
|
+
// Wait briefly to show errors
|
|
1031
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1032
|
+
}
|
|
1033
|
+
if (validFiles.length === 0) {
|
|
1034
|
+
this.monitor.setCommandStatus('File validation failed');
|
|
1035
|
+
this.monitor.addBox('Upload Error', ['No valid files found!']);
|
|
1036
|
+
this.monitor.draw();
|
|
1037
|
+
return 1;
|
|
1038
|
+
}
|
|
1039
|
+
// Connect and start
|
|
1040
|
+
this.monitor.setCommandStatus('Connecting to Aparavi server...');
|
|
1041
|
+
this.monitor.draw();
|
|
1042
|
+
await this.createAndConnectClient();
|
|
1043
|
+
if (shouldManagePipeline && pipelineConfig) {
|
|
1044
|
+
this.monitor.setCommandStatus('Starting pipeline...');
|
|
1045
|
+
this.monitor.draw();
|
|
1046
|
+
let result = await this.client.use({
|
|
1047
|
+
pipeline: pipelineConfig,
|
|
1048
|
+
threads: this.args.threads,
|
|
1049
|
+
token: 'UPLOAD_TASK',
|
|
1050
|
+
args: this.args.pipeline_args || []
|
|
1051
|
+
});
|
|
1052
|
+
taskToken = result.token;
|
|
1053
|
+
}
|
|
1054
|
+
// Start upload
|
|
1055
|
+
this.monitor.setTotalFiles(validFiles.length);
|
|
1056
|
+
this.monitor.draw();
|
|
1057
|
+
const startTime = Date.now();
|
|
1058
|
+
// Convert file paths to File objects for sendFiles
|
|
1059
|
+
const fileObjects = validFiles.map(filePath => {
|
|
1060
|
+
const fs = require('fs');
|
|
1061
|
+
const stats = fs.statSync(filePath);
|
|
1062
|
+
const content = fs.readFileSync(filePath);
|
|
1063
|
+
return {
|
|
1064
|
+
file: new File([content], path.basename(filePath), {
|
|
1065
|
+
type: 'application/octet-stream',
|
|
1066
|
+
lastModified: stats.mtimeMs
|
|
1067
|
+
}),
|
|
1068
|
+
objinfo: {
|
|
1069
|
+
filepath: filePath,
|
|
1070
|
+
size: stats.size
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
// Upload files - progress events come through event subscription
|
|
1075
|
+
const results = await this.client.sendFiles(fileObjects, taskToken, this.args.max_concurrent || 5);
|
|
1076
|
+
const endTime = Date.now();
|
|
1077
|
+
// Analyze and show results
|
|
1078
|
+
this.analyzeUploadResults(results, startTime / 1000, endTime / 1000);
|
|
1079
|
+
// Cleanup pipeline if we created it
|
|
1080
|
+
if (shouldManagePipeline && taskToken) {
|
|
1081
|
+
try {
|
|
1082
|
+
this.monitor.setCommandStatus([
|
|
1083
|
+
'Upload completed successfully',
|
|
1084
|
+
'Cleaning up...',
|
|
1085
|
+
'Terminating pipeline...'
|
|
1086
|
+
]);
|
|
1087
|
+
this.monitor.draw();
|
|
1088
|
+
await this.client.terminate(taskToken);
|
|
1089
|
+
// Re-show results after cleanup
|
|
1090
|
+
this.analyzeUploadResults(results, startTime / 1000, endTime / 1000);
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
this.monitor.setCommandStatus('Upload completed with cleanup warning');
|
|
1094
|
+
this.monitor.addBox('Cleanup Warning', [`Failed to terminate pipeline: ${error}`]);
|
|
1095
|
+
this.monitor.draw();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return 0;
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
if (!this.monitor) {
|
|
1102
|
+
this.monitor = new UploadProgressMonitor(this);
|
|
1103
|
+
}
|
|
1104
|
+
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('Invalid'))) {
|
|
1105
|
+
this.monitor.setCommandStatus('Configuration error occurred');
|
|
1106
|
+
this.monitor.addBox('Configuration Error', [error.message]);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
this.monitor.setCommandStatus('Upload operation failed');
|
|
1110
|
+
this.monitor.addBox('Upload Error', [String(error)]);
|
|
1111
|
+
}
|
|
1112
|
+
this.monitor.draw();
|
|
1113
|
+
return 1;
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
await this.cleanupClient();
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
findFiles(patterns) {
|
|
1120
|
+
const files = [];
|
|
1121
|
+
for (const pattern of patterns) {
|
|
1122
|
+
const fullPath = path.resolve(pattern);
|
|
1123
|
+
try {
|
|
1124
|
+
const stat = fs.statSync(fullPath);
|
|
1125
|
+
if (stat.isFile()) {
|
|
1126
|
+
files.push(fullPath);
|
|
1127
|
+
}
|
|
1128
|
+
else if (stat.isDirectory()) {
|
|
1129
|
+
const dirFiles = glob.sync(path.join(fullPath, '**/*'), { nodir: true });
|
|
1130
|
+
files.push(...dirFiles.map(f => path.resolve(f)));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch {
|
|
1134
|
+
// Try glob pattern
|
|
1135
|
+
const matches = glob.sync(pattern, { nodir: true });
|
|
1136
|
+
files.push(...matches.map(f => path.resolve(f)));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// Remove duplicates
|
|
1140
|
+
return [...new Set(files)];
|
|
1141
|
+
}
|
|
1142
|
+
validateFiles(filesList) {
|
|
1143
|
+
const validFiles = [];
|
|
1144
|
+
const invalidFiles = [];
|
|
1145
|
+
for (const filepath of filesList) {
|
|
1146
|
+
try {
|
|
1147
|
+
if (fs.existsSync(filepath) && fs.statSync(filepath).isFile()) {
|
|
1148
|
+
// Try to read a byte to check accessibility
|
|
1149
|
+
const fd = fs.openSync(filepath, 'r');
|
|
1150
|
+
fs.closeSync(fd);
|
|
1151
|
+
validFiles.push(filepath);
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
invalidFiles.push(`File not found: ${path.basename(filepath)}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
catch (error) {
|
|
1158
|
+
invalidFiles.push(`Cannot read ${path.basename(filepath)}: ${error}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return [validFiles, invalidFiles];
|
|
1162
|
+
}
|
|
1163
|
+
analyzeUploadResults(results, startTime, endTime) {
|
|
1164
|
+
if (!this.monitor)
|
|
1165
|
+
return;
|
|
1166
|
+
this.monitor.clear();
|
|
1167
|
+
const successfulFiles = [];
|
|
1168
|
+
const failedFiles = [];
|
|
1169
|
+
for (const result of results) {
|
|
1170
|
+
const filename = path.basename(result.filepath || '');
|
|
1171
|
+
if (result.action === 'complete') {
|
|
1172
|
+
this.uploadStats.successful_uploads++;
|
|
1173
|
+
this.uploadStats.total_bytes += result.file_size || 0;
|
|
1174
|
+
this.uploadStats.upload_times.push(result.upload_time || 0);
|
|
1175
|
+
successfulFiles.push({
|
|
1176
|
+
name: filename,
|
|
1177
|
+
size: result.file_size || 0,
|
|
1178
|
+
time: result.upload_time || 0
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
failedFiles.push({
|
|
1183
|
+
name: filename,
|
|
1184
|
+
error: result.error || 'Unknown error'
|
|
1185
|
+
});
|
|
1186
|
+
this.uploadStats.failed_uploads++;
|
|
1187
|
+
}
|
|
1188
|
+
this.uploadStats.files_processed++;
|
|
1189
|
+
}
|
|
1190
|
+
// Create summary
|
|
1191
|
+
const successful = this.uploadStats.successful_uploads;
|
|
1192
|
+
const failed = this.uploadStats.failed_uploads;
|
|
1193
|
+
const totalBytes = this.uploadStats.total_bytes;
|
|
1194
|
+
const summaryLines = [
|
|
1195
|
+
`Total files processed: ${successful + failed}`,
|
|
1196
|
+
`Successful uploads: ${ANSI_GREEN}${successful}${ANSI_RESET}`
|
|
1197
|
+
];
|
|
1198
|
+
if (failed > 0) {
|
|
1199
|
+
summaryLines.push(`Failed uploads: ${ANSI_RED}${failed}${ANSI_RESET}`);
|
|
1200
|
+
}
|
|
1201
|
+
summaryLines.push(`Total data uploaded: ${this.monitor.formatSize(totalBytes)}`);
|
|
1202
|
+
if (startTime && endTime && endTime > startTime) {
|
|
1203
|
+
const elapsedSeconds = endTime - startTime;
|
|
1204
|
+
let elapsedStr;
|
|
1205
|
+
if (elapsedSeconds < 60) {
|
|
1206
|
+
elapsedStr = `${elapsedSeconds.toFixed(1)} seconds`;
|
|
1207
|
+
}
|
|
1208
|
+
else if (elapsedSeconds < 3600) {
|
|
1209
|
+
const minutes = Math.floor(elapsedSeconds / 60);
|
|
1210
|
+
const seconds = elapsedSeconds % 60;
|
|
1211
|
+
elapsedStr = `${minutes}m ${seconds.toFixed(1)}s`;
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
const hours = Math.floor(elapsedSeconds / 3600);
|
|
1215
|
+
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
|
|
1216
|
+
const seconds = elapsedSeconds % 60;
|
|
1217
|
+
elapsedStr = `${hours}h ${minutes}m ${seconds.toFixed(1)}s`;
|
|
1218
|
+
}
|
|
1219
|
+
summaryLines.push(`Total elapsed time: ${elapsedStr}`);
|
|
1220
|
+
if (totalBytes > 0 && elapsedSeconds > 0) {
|
|
1221
|
+
const throughputBps = totalBytes / elapsedSeconds;
|
|
1222
|
+
const throughputStr = this.monitor.formatSize(Math.floor(throughputBps));
|
|
1223
|
+
summaryLines.push(`Average throughput: ${throughputStr}/s`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
this.monitor.addBox('Upload Summary', summaryLines);
|
|
1227
|
+
// Show failed files if any
|
|
1228
|
+
if (failedFiles.length > 0) {
|
|
1229
|
+
const failureLines = [];
|
|
1230
|
+
for (const failedFile of failedFiles.slice(0, 10)) {
|
|
1231
|
+
const filename = failedFile.name.length > 25 ?
|
|
1232
|
+
`${failedFile.name.substring(0, 25)}...` : failedFile.name;
|
|
1233
|
+
const errorMsg = failedFile.error.length > 40 ?
|
|
1234
|
+
`${failedFile.error.substring(0, 40)}...` : failedFile.error;
|
|
1235
|
+
failureLines.push(`${ANSI_RED}${CHR_CROSS}${ANSI_RESET} ${filename} - ${errorMsg}`);
|
|
1236
|
+
}
|
|
1237
|
+
if (failedFiles.length > 10) {
|
|
1238
|
+
failureLines.push(`... and ${failedFiles.length - 10} more failures`);
|
|
1239
|
+
}
|
|
1240
|
+
this.monitor.addBox(`Failed Uploads (${failedFiles.length})`, failureLines);
|
|
1241
|
+
}
|
|
1242
|
+
// Show successful files summary
|
|
1243
|
+
if (successfulFiles.length > 0) {
|
|
1244
|
+
const successLines = [];
|
|
1245
|
+
for (const successFile of successfulFiles.slice(-5)) {
|
|
1246
|
+
const truncatedName = successFile.name.length > 35 ?
|
|
1247
|
+
`${successFile.name.substring(0, 35)}...` : successFile.name;
|
|
1248
|
+
const sizeStr = this.monitor.formatSize(successFile.size);
|
|
1249
|
+
const timeStr = `${successFile.time.toFixed(1)}s`;
|
|
1250
|
+
successLines.push(`${ANSI_GREEN}${CHR_CHECK}${ANSI_RESET} ${truncatedName} (${sizeStr}, ${timeStr})`);
|
|
1251
|
+
}
|
|
1252
|
+
if (successfulFiles.length > 5) {
|
|
1253
|
+
successLines.push(`... and ${successfulFiles.length - 5} more successful uploads`);
|
|
1254
|
+
}
|
|
1255
|
+
this.monitor.addBox('Recent Successful Uploads', successLines);
|
|
1256
|
+
}
|
|
1257
|
+
this.monitor.setCommandStatus('Completed');
|
|
1258
|
+
this.monitor.draw();
|
|
1259
|
+
}
|
|
1260
|
+
async cmdStatus() {
|
|
1261
|
+
try {
|
|
1262
|
+
if (!this.args.token) {
|
|
1263
|
+
console.error('Error: --token is required for status command');
|
|
1264
|
+
return 1;
|
|
1265
|
+
}
|
|
1266
|
+
this.monitor = new StatusMonitor(this, this.args.token);
|
|
1267
|
+
const onConnected = async (uri) => {
|
|
1268
|
+
this.connected = true;
|
|
1269
|
+
this.attempt = 0;
|
|
1270
|
+
this.monitor.displayStatus({});
|
|
1271
|
+
await this.sendMonitorCommand(true, this.args.token);
|
|
1272
|
+
};
|
|
1273
|
+
const onDisconnected = async (reason, hasError) => {
|
|
1274
|
+
this.connected = false;
|
|
1275
|
+
};
|
|
1276
|
+
// Auto-reconnection loop
|
|
1277
|
+
while (!this.isCancelled()) {
|
|
1278
|
+
if (!this.connected) {
|
|
1279
|
+
this.monitor.displayConnecting(this.uri, this.attempt);
|
|
1280
|
+
try {
|
|
1281
|
+
await this.createAndConnectClient(onConnected, onDisconnected);
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
this.attempt++;
|
|
1285
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1290
|
+
}
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
console.log('\n\nStopping monitoring...');
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
finally {
|
|
1298
|
+
// Make sure we unsubscribe
|
|
1299
|
+
if (this.client && this.connected) {
|
|
1300
|
+
try {
|
|
1301
|
+
await this.sendMonitorCommand(false, this.args.token);
|
|
1302
|
+
}
|
|
1303
|
+
catch {
|
|
1304
|
+
// Ignore unsubscribe errors
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
await this.cleanupClient();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async cmdStop() {
|
|
1311
|
+
try {
|
|
1312
|
+
if (!this.args.token) {
|
|
1313
|
+
console.error('Error: --token is required for stop command');
|
|
1314
|
+
return 1;
|
|
1315
|
+
}
|
|
1316
|
+
this.monitor = new GenericMonitor(this, 'Aparavi Task Management');
|
|
1317
|
+
this.monitor.setCommandStatus('Connecting to server...');
|
|
1318
|
+
this.monitor.draw();
|
|
1319
|
+
await this.createAndConnectClient();
|
|
1320
|
+
this.monitor.setCommandStatus(`Terminating task: ${this.args.token}`);
|
|
1321
|
+
this.monitor.draw();
|
|
1322
|
+
await this.client.terminate(this.args.token);
|
|
1323
|
+
const stopLines = [
|
|
1324
|
+
`Task ${this.args.token} terminated successfully`,
|
|
1325
|
+
'',
|
|
1326
|
+
'The task has been stopped and resources cleaned up.'
|
|
1327
|
+
];
|
|
1328
|
+
this.monitor.setCommandStatus(stopLines);
|
|
1329
|
+
this.monitor.draw();
|
|
1330
|
+
return 0;
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
if (!this.monitor) {
|
|
1334
|
+
this.monitor = new GenericMonitor(this, 'Aparavi Task Management');
|
|
1335
|
+
}
|
|
1336
|
+
this.monitor.setCommandStatus('Stop operation failed');
|
|
1337
|
+
this.monitor.addBox('Stop Error', [String(error)]);
|
|
1338
|
+
this.monitor.draw();
|
|
1339
|
+
return 1;
|
|
1340
|
+
}
|
|
1341
|
+
finally {
|
|
1342
|
+
await this.cleanupClient();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
async run() {
|
|
1346
|
+
const program = this.createProgram();
|
|
1347
|
+
// Parse command line arguments - commander will handle command routing
|
|
1348
|
+
try {
|
|
1349
|
+
await program.parseAsync(process.argv);
|
|
1350
|
+
return 0; // If we get here, a command was executed successfully
|
|
1351
|
+
}
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
if (error instanceof Error && error.message.includes('interrupted')) {
|
|
1354
|
+
console.log('\nOperation interrupted by user');
|
|
1355
|
+
return 1;
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
console.error(`Error: ${error}`);
|
|
1359
|
+
return 1;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
exports.AparaviCLI = AparaviCLI;
|
|
1365
|
+
function formatError(e) {
|
|
1366
|
+
const stack = e.stack?.split('\n');
|
|
1367
|
+
if (stack && stack.length > 1) {
|
|
1368
|
+
const frame = stack[1];
|
|
1369
|
+
const match = frame.match(/at .+?\((.+):(\d+):\d+\)/) || frame.match(/at (.+):(\d+):\d+/);
|
|
1370
|
+
if (match) {
|
|
1371
|
+
const filename = path.basename(match[1]);
|
|
1372
|
+
const lineno = match[2];
|
|
1373
|
+
return `${e.constructor.name}: ${e.message} (in ${filename}:${lineno})`;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return `${e.constructor.name}: ${e.message}`;
|
|
1377
|
+
}
|
|
1378
|
+
async function main() {
|
|
1379
|
+
try {
|
|
1380
|
+
const aparavi = new AparaviCLI();
|
|
1381
|
+
const exitCode = await aparavi.run();
|
|
1382
|
+
process.exit(exitCode);
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
if (error instanceof Error && error.message.includes('interrupted')) {
|
|
1386
|
+
console.log('\n\nOperation interrupted by user');
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
console.log(`\nOperation failed: ${formatError(error)}`);
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
// Entry point when script is run directly
|
|
1395
|
+
if (require.main === module) {
|
|
1396
|
+
main().catch(error => {
|
|
1397
|
+
console.error('Fatal error:', error);
|
|
1398
|
+
process.exit(1);
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
//# sourceMappingURL=aparavi.js.map
|