@nightowne/tas-cli 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * CLI Helper utilities
3
+ * Shared logic for commands
4
+ */
5
+
6
+ import inquirer from 'inquirer';
7
+ import chalk from 'chalk';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { Encryptor } from '../crypto/encryption.js';
11
+
12
+ /**
13
+ * Get password from command-line option, environment variable, or interactive prompt
14
+ * @param {string} passwordOption - Password from --password flag (if provided)
15
+ * @param {boolean} allowCache - Allow caching via TAS_PASSWORD env var
16
+ * @returns {Promise<string>}
17
+ */
18
+ export async function getPassword(passwordOption, allowCache = true) {
19
+ // Priority: CLI flag > Environment variable > Interactive prompt
20
+
21
+ if (passwordOption) {
22
+ return passwordOption;
23
+ }
24
+
25
+ if (allowCache && process.env.TAS_PASSWORD) {
26
+ return process.env.TAS_PASSWORD;
27
+ }
28
+
29
+ const { password } = await inquirer.prompt([
30
+ {
31
+ type: 'password',
32
+ name: 'password',
33
+ message: 'Enter your encryption password:',
34
+ mask: '*'
35
+ }
36
+ ]);
37
+
38
+ return password;
39
+ }
40
+
41
+ /**
42
+ * Verify password against config
43
+ * @param {string} password - Password to verify
44
+ * @param {Object} config - Config object with passwordHash
45
+ * @returns {boolean}
46
+ */
47
+ export function verifyPassword(password, config) {
48
+ const encryptor = new Encryptor(password);
49
+ return encryptor.getPasswordHash() === config.passwordHash;
50
+ }
51
+
52
+ /**
53
+ * Validate config structure and content
54
+ * @param {Object} config - Config to validate
55
+ * @returns {Object} - { valid: boolean, errors: string[] }
56
+ */
57
+ export function validateConfig(config) {
58
+ const errors = [];
59
+
60
+ if (!config) {
61
+ errors.push('Config is null or undefined');
62
+ return { valid: false, errors };
63
+ }
64
+
65
+ if (!config.botToken || typeof config.botToken !== 'string') {
66
+ errors.push('Missing or invalid botToken');
67
+ }
68
+
69
+ if (!config.botToken?.includes(':')) {
70
+ errors.push('Invalid bot token format (should contain :)');
71
+ }
72
+
73
+ if (!config.chatId || (typeof config.chatId !== 'number' && typeof config.chatId !== 'string')) {
74
+ errors.push('Missing or invalid chatId');
75
+ }
76
+
77
+ if (!config.passwordHash || typeof config.passwordHash !== 'string') {
78
+ errors.push('Missing or invalid passwordHash');
79
+ }
80
+
81
+ return {
82
+ valid: errors.length === 0,
83
+ errors
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Load and validate config
89
+ * @param {string} dataDir - Data directory path
90
+ * @returns {Object|null} - Config object or null if not found
91
+ */
92
+ export function loadConfig(dataDir) {
93
+ const configPath = path.join(dataDir, 'config.json');
94
+
95
+ if (!fs.existsSync(configPath)) {
96
+ return null;
97
+ }
98
+
99
+ try {
100
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
101
+ const validation = validateConfig(config);
102
+
103
+ if (!validation.valid) {
104
+ throw new Error(`Invalid config: ${validation.errors.join(', ')}`);
105
+ }
106
+
107
+ return config;
108
+ } catch (err) {
109
+ throw new Error(`Config error: ${err.message}`);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Ensure TAS is initialized
115
+ * @param {string} dataDir - Data directory path
116
+ * @returns {Object} - Config object
117
+ */
118
+ export function requireConfig(dataDir) {
119
+ const config = loadConfig(dataDir);
120
+
121
+ if (!config) {
122
+ console.log(chalk.red('✗ TAS not initialized. Run `tas init` first.'));
123
+ process.exit(1);
124
+ }
125
+
126
+ return config;
127
+ }
128
+
129
+ /**
130
+ * Get and verify password with proper error handling
131
+ * @param {string} passwordOption - Password from CLI flag
132
+ * @param {string} dataDir - Data directory path
133
+ * @returns {Promise<string>} - Verified password
134
+ */
135
+ export async function getAndVerifyPassword(passwordOption, dataDir) {
136
+ const config = requireConfig(dataDir);
137
+ const password = await getPassword(passwordOption);
138
+
139
+ if (!verifyPassword(password, config)) {
140
+ console.log(chalk.red('✗ Incorrect password'));
141
+ process.exit(1);
142
+ }
143
+
144
+ return password;
145
+ }
@@ -8,6 +8,7 @@ import path from 'path';
8
8
 
9
9
  const gzip = promisify(zlib.gzip);
10
10
  const gunzip = promisify(zlib.gunzip);
11
+ import { PassThrough } from 'stream';
11
12
 
12
13
  // File extensions that are already compressed (skip compression for these)
13
14
  const SKIP_COMPRESSION = new Set([
@@ -71,6 +72,24 @@ export class Compressor {
71
72
  }
72
73
  }
73
74
 
75
+ /**
76
+ * Get a compression transform stream
77
+ * Returns: { stream: Transform, compressed: boolean }
78
+ */
79
+ getCompressStream(filename = '') {
80
+ if (this.shouldSkip(filename)) {
81
+ return {
82
+ stream: new PassThrough(),
83
+ compressed: false
84
+ };
85
+ }
86
+
87
+ return {
88
+ stream: zlib.createGzip({ level: 6 }),
89
+ compressed: true
90
+ };
91
+ }
92
+
74
93
  /**
75
94
  * Decompress gzip data
76
95
  */
@@ -81,4 +100,15 @@ export class Compressor {
81
100
 
82
101
  return await gunzip(data);
83
102
  }
103
+
104
+ /**
105
+ * Get a decompression transform stream
106
+ */
107
+ getDecompressStream(wasCompressed) {
108
+ if (!wasCompressed) {
109
+ return new PassThrough();
110
+ }
111
+
112
+ return zlib.createGunzip();
113
+ }
84
114
  }
@@ -0,0 +1,26 @@
1
+ import { Transform } from 'stream';
2
+
3
+ export class Throttle extends Transform {
4
+ constructor(bytesPerSecond) {
5
+ super();
6
+ this.bytesPerSecond = bytesPerSecond;
7
+ this.passed = 0;
8
+ this.startTime = Date.now();
9
+ }
10
+
11
+ _transform(chunk, encoding, callback) {
12
+ this.passed += chunk.length;
13
+ const elapsed = Date.now() - this.startTime;
14
+ const expectedTime = (this.passed / this.bytesPerSecond) * 1000;
15
+
16
+ if (expectedTime > elapsed) {
17
+ setTimeout(() => {
18
+ this.push(chunk);
19
+ callback();
20
+ }, expectedTime - elapsed);
21
+ } else {
22
+ this.push(chunk);
23
+ callback();
24
+ }
25
+ }
26
+ }