recker 1.0.4 → 1.0.6

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.
Files changed (115) hide show
  1. package/README.md +1 -1
  2. package/dist/ai/adaptive-timeout.d.ts +51 -0
  3. package/dist/ai/adaptive-timeout.d.ts.map +1 -0
  4. package/dist/ai/adaptive-timeout.js +208 -0
  5. package/dist/ai/client.d.ts +24 -0
  6. package/dist/ai/client.d.ts.map +1 -0
  7. package/dist/ai/client.js +289 -0
  8. package/dist/ai/index.d.ts +10 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/index.js +6 -0
  11. package/dist/ai/providers/anthropic.d.ts +64 -0
  12. package/dist/ai/providers/anthropic.d.ts.map +1 -0
  13. package/dist/ai/providers/anthropic.js +367 -0
  14. package/dist/ai/providers/base.d.ts +49 -0
  15. package/dist/ai/providers/base.d.ts.map +1 -0
  16. package/dist/ai/providers/base.js +145 -0
  17. package/dist/ai/providers/index.d.ts +7 -0
  18. package/dist/ai/providers/index.d.ts.map +1 -0
  19. package/dist/ai/providers/index.js +3 -0
  20. package/dist/ai/providers/openai.d.ts +65 -0
  21. package/dist/ai/providers/openai.d.ts.map +1 -0
  22. package/dist/ai/providers/openai.js +298 -0
  23. package/dist/ai/rate-limiter.d.ts +44 -0
  24. package/dist/ai/rate-limiter.d.ts.map +1 -0
  25. package/dist/ai/rate-limiter.js +212 -0
  26. package/dist/bench/generator.d.ts +19 -0
  27. package/dist/bench/generator.d.ts.map +1 -0
  28. package/dist/bench/generator.js +86 -0
  29. package/dist/bench/stats.d.ts +35 -0
  30. package/dist/bench/stats.d.ts.map +1 -0
  31. package/dist/bench/stats.js +60 -0
  32. package/dist/cache/memory-storage.d.ts.map +1 -1
  33. package/dist/cache/memory-storage.js +0 -5
  34. package/dist/cli/handler.d.ts +11 -0
  35. package/dist/cli/handler.d.ts.map +1 -0
  36. package/dist/cli/handler.js +92 -0
  37. package/dist/cli/index.d.ts +3 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +255 -0
  40. package/dist/cli/presets.d.ts +2 -0
  41. package/dist/cli/presets.d.ts.map +1 -0
  42. package/dist/cli/presets.js +67 -0
  43. package/dist/cli/tui/ai-chat.d.ts +3 -0
  44. package/dist/cli/tui/ai-chat.d.ts.map +1 -0
  45. package/dist/cli/tui/ai-chat.js +100 -0
  46. package/dist/cli/tui/load-dashboard.d.ts +3 -0
  47. package/dist/cli/tui/load-dashboard.d.ts.map +1 -0
  48. package/dist/cli/tui/load-dashboard.js +117 -0
  49. package/dist/cli/tui/shell.d.ts +27 -0
  50. package/dist/cli/tui/shell.d.ts.map +1 -0
  51. package/dist/cli/tui/shell.js +386 -0
  52. package/dist/cli/tui/websocket.d.ts +2 -0
  53. package/dist/cli/tui/websocket.d.ts.map +1 -0
  54. package/dist/cli/tui/websocket.js +87 -0
  55. package/dist/contract/index.d.ts +2 -2
  56. package/dist/contract/index.d.ts.map +1 -1
  57. package/dist/core/client.d.ts +1 -0
  58. package/dist/core/client.d.ts.map +1 -1
  59. package/dist/core/client.js +4 -2
  60. package/dist/core/request-promise.d.ts +1 -1
  61. package/dist/core/request-promise.d.ts.map +1 -1
  62. package/dist/mcp/contract.d.ts +1 -1
  63. package/dist/mcp/contract.d.ts.map +1 -1
  64. package/dist/plugins/cache.d.ts.map +1 -1
  65. package/dist/plugins/cache.js +0 -7
  66. package/dist/plugins/scrape.d.ts.map +1 -1
  67. package/dist/plugins/scrape.js +9 -14
  68. package/dist/protocols/ftp.d.ts +28 -5
  69. package/dist/protocols/ftp.d.ts.map +1 -1
  70. package/dist/protocols/ftp.js +549 -136
  71. package/dist/protocols/sftp.d.ts +4 -2
  72. package/dist/protocols/sftp.d.ts.map +1 -1
  73. package/dist/protocols/sftp.js +16 -2
  74. package/dist/protocols/telnet.d.ts +37 -5
  75. package/dist/protocols/telnet.d.ts.map +1 -1
  76. package/dist/protocols/telnet.js +434 -58
  77. package/dist/runner/request-runner.d.ts.map +1 -1
  78. package/dist/runner/request-runner.js +0 -1
  79. package/dist/scrape/document.d.ts +3 -2
  80. package/dist/scrape/document.d.ts.map +1 -1
  81. package/dist/scrape/document.js +15 -3
  82. package/dist/testing/index.d.ts +2 -0
  83. package/dist/testing/index.d.ts.map +1 -1
  84. package/dist/testing/index.js +1 -0
  85. package/dist/testing/mock-udp-server.d.ts +44 -0
  86. package/dist/testing/mock-udp-server.d.ts.map +1 -0
  87. package/dist/testing/mock-udp-server.js +188 -0
  88. package/dist/transport/base-udp.d.ts +36 -0
  89. package/dist/transport/base-udp.d.ts.map +1 -0
  90. package/dist/transport/base-udp.js +188 -0
  91. package/dist/transport/udp-response.d.ts +65 -0
  92. package/dist/transport/udp-response.d.ts.map +1 -0
  93. package/dist/transport/udp-response.js +269 -0
  94. package/dist/transport/udp.d.ts +22 -0
  95. package/dist/transport/udp.d.ts.map +1 -0
  96. package/dist/transport/udp.js +260 -0
  97. package/dist/types/ai.d.ts +268 -0
  98. package/dist/types/ai.d.ts.map +1 -0
  99. package/dist/types/ai.js +1 -0
  100. package/dist/types/udp.d.ts +138 -0
  101. package/dist/types/udp.d.ts.map +1 -0
  102. package/dist/types/udp.js +1 -0
  103. package/dist/udp/index.d.ts +6 -0
  104. package/dist/udp/index.d.ts.map +1 -0
  105. package/dist/udp/index.js +3 -0
  106. package/dist/utils/chart.d.ts +15 -0
  107. package/dist/utils/chart.d.ts.map +1 -0
  108. package/dist/utils/chart.js +94 -0
  109. package/dist/utils/colors.d.ts +27 -0
  110. package/dist/utils/colors.d.ts.map +1 -0
  111. package/dist/utils/colors.js +50 -0
  112. package/dist/utils/optional-require.d.ts +20 -0
  113. package/dist/utils/optional-require.d.ts.map +1 -0
  114. package/dist/utils/optional-require.js +105 -0
  115. package/package.json +53 -12
@@ -1,27 +1,97 @@
1
- import { Client as FTPClient } from 'basic-ftp';
1
+ import { Socket } from 'node:net';
2
+ import { connect as tlsConnect } from 'node:tls';
2
3
  import { Readable, Writable } from 'node:stream';
3
- export class FTP {
4
- client;
4
+ import { createWriteStream, createReadStream } from 'node:fs';
5
+ import { EventEmitter } from 'node:events';
6
+ const ResponseCode = {
7
+ RESTART_MARKER: 110,
8
+ SERVICE_READY_IN: 120,
9
+ DATA_CONNECTION_OPEN: 125,
10
+ FILE_STATUS_OK: 150,
11
+ OK: 200,
12
+ SUPERFLUOUS: 202,
13
+ SYSTEM_STATUS: 211,
14
+ DIRECTORY_STATUS: 212,
15
+ FILE_STATUS: 213,
16
+ HELP_MESSAGE: 214,
17
+ SYSTEM_TYPE: 215,
18
+ SERVICE_READY: 220,
19
+ SERVICE_CLOSING: 221,
20
+ DATA_CONNECTION_READY: 225,
21
+ CLOSING_DATA_CONNECTION: 226,
22
+ ENTERING_PASSIVE: 227,
23
+ ENTERING_EXTENDED_PASSIVE: 229,
24
+ USER_LOGGED_IN: 230,
25
+ AUTH_OK: 234,
26
+ FILE_ACTION_OK: 250,
27
+ PATHNAME_CREATED: 257,
28
+ NEED_PASSWORD: 331,
29
+ NEED_ACCOUNT: 332,
30
+ FILE_ACTION_PENDING: 350,
31
+ SERVICE_UNAVAILABLE: 421,
32
+ CANT_OPEN_DATA: 425,
33
+ CONNECTION_CLOSED: 426,
34
+ FILE_BUSY: 450,
35
+ LOCAL_ERROR: 451,
36
+ INSUFFICIENT_SPACE: 452,
37
+ SYNTAX_ERROR: 500,
38
+ SYNTAX_ERROR_PARAMS: 501,
39
+ NOT_IMPLEMENTED: 502,
40
+ BAD_SEQUENCE: 503,
41
+ NOT_IMPLEMENTED_PARAM: 504,
42
+ NOT_LOGGED_IN: 530,
43
+ NEED_ACCOUNT_STORE: 532,
44
+ FILE_NOT_FOUND: 550,
45
+ PAGE_TYPE_UNKNOWN: 551,
46
+ EXCEEDED_ALLOCATION: 552,
47
+ FILE_NAME_NOT_ALLOWED: 553,
48
+ };
49
+ export class FTP extends EventEmitter {
50
+ controlSocket = null;
5
51
  config;
6
52
  connected = false;
53
+ secureConnection = false;
54
+ responseBuffer = '';
7
55
  onProgress;
56
+ currentTransferBytes = 0;
57
+ socketFactory = null;
8
58
  constructor(config) {
9
- this.config = config;
10
- this.client = new FTPClient(config.timeout ?? 30000);
11
- if (config.verbose) {
12
- this.client.ftp.verbose = true;
13
- }
59
+ super();
60
+ const isImplicit = config.secure === 'implicit';
61
+ const defaultPort = isImplicit ? 990 : 21;
62
+ this.config = {
63
+ host: config.host,
64
+ port: config.port ?? defaultPort,
65
+ user: config.user ?? 'anonymous',
66
+ password: config.password ?? 'anonymous@',
67
+ secure: config.secure ?? false,
68
+ timeout: config.timeout ?? 30000,
69
+ verbose: config.verbose ?? false,
70
+ tlsOptions: config.tlsOptions,
71
+ };
72
+ this.socketFactory = config._socketFactory ?? null;
73
+ }
74
+ createSocket() {
75
+ return this.socketFactory ? this.socketFactory() : new Socket();
14
76
  }
15
77
  async connect() {
16
78
  try {
17
- const accessOptions = {
18
- host: this.config.host,
19
- port: this.config.port ?? 21,
20
- user: this.config.user ?? 'anonymous',
21
- password: this.config.password ?? 'anonymous@',
22
- secure: this.config.secure ?? false,
23
- };
24
- await this.client.access(accessOptions);
79
+ if (this.config.secure === 'implicit') {
80
+ await this.connectImplicitTLS();
81
+ }
82
+ else {
83
+ await this.connectPlain();
84
+ if (this.config.secure === true) {
85
+ await this.upgradeToTLS();
86
+ }
87
+ }
88
+ const welcome = await this.readResponse();
89
+ this.debug('Welcome: %s', welcome.message);
90
+ if (welcome.code !== ResponseCode.SERVICE_READY) {
91
+ throw new Error(`Server not ready: ${welcome.message}`);
92
+ }
93
+ await this.authenticate();
94
+ await this.sendCommand('TYPE I');
25
95
  this.connected = true;
26
96
  return {
27
97
  success: true,
@@ -29,6 +99,7 @@ export class FTP {
29
99
  };
30
100
  }
31
101
  catch (error) {
102
+ this.cleanup();
32
103
  return {
33
104
  success: false,
34
105
  message: error instanceof Error ? error.message : 'Connection failed'
@@ -36,20 +107,15 @@ export class FTP {
36
107
  }
37
108
  }
38
109
  isConnected() {
39
- return this.connected && !this.client.closed;
110
+ return this.connected && this.controlSocket !== null && !this.controlSocket.destroyed;
40
111
  }
41
112
  async list(path = '/') {
42
113
  this.ensureConnected();
43
114
  try {
44
- const files = await this.client.list(path);
45
- const items = files.map((file) => ({
46
- name: file.name,
47
- type: this.mapFileType(file.type),
48
- size: file.size,
49
- modifiedAt: file.modifiedAt,
50
- permissions: file.permissions?.toString(),
51
- rawModifiedAt: file.rawModifiedAt
52
- }));
115
+ const dataSocket = await this.openDataConnection();
116
+ await this.sendCommand(`LIST ${path}`);
117
+ const data = await this.readDataConnection(dataSocket);
118
+ const items = this.parseDirectoryListing(data);
53
119
  return {
54
120
  success: true,
55
121
  data: items
@@ -65,25 +131,11 @@ export class FTP {
65
131
  async download(remotePath, localPath) {
66
132
  this.ensureConnected();
67
133
  try {
68
- if (this.onProgress) {
69
- this.client.trackProgress((info) => {
70
- this.onProgress?.({
71
- bytes: info.bytes,
72
- bytesOverall: info.bytesOverall,
73
- name: info.name,
74
- type: 'download'
75
- });
76
- });
77
- }
78
- await this.client.downloadTo(localPath, remotePath);
79
- this.client.trackProgress();
80
- return {
81
- success: true,
82
- message: `Downloaded ${remotePath} to ${localPath}`
83
- };
134
+ const writeStream = createWriteStream(localPath);
135
+ const result = await this.downloadToStream(remotePath, writeStream);
136
+ return result;
84
137
  }
85
138
  catch (error) {
86
- this.client.trackProgress();
87
139
  return {
88
140
  success: false,
89
141
  message: error instanceof Error ? error.message : 'Download failed'
@@ -93,25 +145,42 @@ export class FTP {
93
145
  async downloadToStream(remotePath, stream) {
94
146
  this.ensureConnected();
95
147
  try {
96
- if (this.onProgress) {
97
- this.client.trackProgress((info) => {
98
- this.onProgress?.({
99
- bytes: info.bytes,
100
- bytesOverall: info.bytesOverall,
101
- name: info.name,
102
- type: 'download'
103
- });
148
+ const sizeResult = await this.size(remotePath);
149
+ const totalSize = sizeResult.data ?? 0;
150
+ const dataSocket = await this.openDataConnection();
151
+ await this.sendCommand(`RETR ${remotePath}`);
152
+ this.currentTransferBytes = 0;
153
+ const fileName = remotePath.split('/').pop() || remotePath;
154
+ await new Promise((resolve, reject) => {
155
+ dataSocket.on('data', (chunk) => {
156
+ this.currentTransferBytes += chunk.length;
157
+ stream.write(chunk);
158
+ if (this.onProgress) {
159
+ this.onProgress({
160
+ bytes: chunk.length,
161
+ bytesOverall: this.currentTransferBytes,
162
+ name: fileName,
163
+ type: 'download'
164
+ });
165
+ }
166
+ });
167
+ dataSocket.on('end', () => {
168
+ stream.end();
169
+ resolve();
104
170
  });
171
+ dataSocket.on('error', reject);
172
+ });
173
+ const response = await this.readResponse();
174
+ if (response.code !== ResponseCode.CLOSING_DATA_CONNECTION &&
175
+ response.code !== ResponseCode.FILE_ACTION_OK) {
176
+ throw new Error(response.message);
105
177
  }
106
- await this.client.downloadTo(stream, remotePath);
107
- this.client.trackProgress();
108
178
  return {
109
179
  success: true,
110
180
  message: `Downloaded ${remotePath}`
111
181
  };
112
182
  }
113
183
  catch (error) {
114
- this.client.trackProgress();
115
184
  return {
116
185
  success: false,
117
186
  message: error instanceof Error ? error.message : 'Download failed'
@@ -128,7 +197,13 @@ export class FTP {
128
197
  callback();
129
198
  }
130
199
  });
131
- await this.client.downloadTo(stream, remotePath);
200
+ const result = await this.downloadToStream(remotePath, stream);
201
+ if (!result.success) {
202
+ return {
203
+ success: false,
204
+ message: result.message
205
+ };
206
+ }
132
207
  return {
133
208
  success: true,
134
209
  data: Buffer.concat(chunks)
@@ -144,25 +219,10 @@ export class FTP {
144
219
  async upload(localPath, remotePath) {
145
220
  this.ensureConnected();
146
221
  try {
147
- if (this.onProgress) {
148
- this.client.trackProgress((info) => {
149
- this.onProgress?.({
150
- bytes: info.bytes,
151
- bytesOverall: info.bytesOverall,
152
- name: info.name,
153
- type: 'upload'
154
- });
155
- });
156
- }
157
- await this.client.uploadFrom(localPath, remotePath);
158
- this.client.trackProgress();
159
- return {
160
- success: true,
161
- message: `Uploaded ${localPath} to ${remotePath}`
162
- };
222
+ const readStream = createReadStream(localPath);
223
+ return await this.uploadFromStream(readStream, remotePath);
163
224
  }
164
225
  catch (error) {
165
- this.client.trackProgress();
166
226
  return {
167
227
  success: false,
168
228
  message: error instanceof Error ? error.message : 'Upload failed'
@@ -172,25 +232,41 @@ export class FTP {
172
232
  async uploadFromStream(stream, remotePath) {
173
233
  this.ensureConnected();
174
234
  try {
175
- if (this.onProgress) {
176
- this.client.trackProgress((info) => {
177
- this.onProgress?.({
178
- bytes: info.bytes,
179
- bytesOverall: info.bytesOverall,
180
- name: info.name,
181
- type: 'upload'
182
- });
235
+ const dataSocket = await this.openDataConnection();
236
+ await this.sendCommand(`STOR ${remotePath}`);
237
+ this.currentTransferBytes = 0;
238
+ const fileName = remotePath.split('/').pop() || remotePath;
239
+ await new Promise((resolve, reject) => {
240
+ stream.on('data', (chunk) => {
241
+ this.currentTransferBytes += chunk.length;
242
+ dataSocket.write(chunk);
243
+ if (this.onProgress) {
244
+ this.onProgress({
245
+ bytes: chunk.length,
246
+ bytesOverall: this.currentTransferBytes,
247
+ name: fileName,
248
+ type: 'upload'
249
+ });
250
+ }
183
251
  });
252
+ stream.on('end', () => {
253
+ dataSocket.end();
254
+ resolve();
255
+ });
256
+ stream.on('error', reject);
257
+ dataSocket.on('error', reject);
258
+ });
259
+ const response = await this.readResponse();
260
+ if (response.code !== ResponseCode.CLOSING_DATA_CONNECTION &&
261
+ response.code !== ResponseCode.FILE_ACTION_OK) {
262
+ throw new Error(response.message);
184
263
  }
185
- await this.client.uploadFrom(stream, remotePath);
186
- this.client.trackProgress();
187
264
  return {
188
265
  success: true,
189
266
  message: `Uploaded to ${remotePath}`
190
267
  };
191
268
  }
192
269
  catch (error) {
193
- this.client.trackProgress();
194
270
  return {
195
271
  success: false,
196
272
  message: error instanceof Error ? error.message : 'Upload failed'
@@ -198,30 +274,20 @@ export class FTP {
198
274
  }
199
275
  }
200
276
  async uploadFromBuffer(data, remotePath) {
201
- this.ensureConnected();
202
- try {
203
- const buffer = typeof data === 'string' ? Buffer.from(data) : data;
204
- const stream = Readable.from(buffer);
205
- await this.client.uploadFrom(stream, remotePath);
206
- return {
207
- success: true,
208
- message: `Uploaded to ${remotePath}`
209
- };
210
- }
211
- catch (error) {
212
- return {
213
- success: false,
214
- message: error instanceof Error ? error.message : 'Upload failed'
215
- };
216
- }
277
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
278
+ const stream = Readable.from(buffer);
279
+ return this.uploadFromStream(stream, remotePath);
217
280
  }
218
281
  async delete(remotePath) {
219
282
  this.ensureConnected();
220
283
  try {
221
- await this.client.remove(remotePath);
284
+ const response = await this.sendCommand(`DELE ${remotePath}`);
222
285
  return {
223
- success: true,
224
- message: `Deleted ${remotePath}`
286
+ success: response.code === ResponseCode.FILE_ACTION_OK,
287
+ code: response.code,
288
+ message: response.code === ResponseCode.FILE_ACTION_OK
289
+ ? `Deleted ${remotePath}`
290
+ : response.message
225
291
  };
226
292
  }
227
293
  catch (error) {
@@ -234,10 +300,29 @@ export class FTP {
234
300
  async mkdir(remotePath, recursive = true) {
235
301
  this.ensureConnected();
236
302
  try {
237
- await this.client.ensureDir(remotePath);
303
+ if (recursive) {
304
+ const parts = remotePath.split('/').filter(Boolean);
305
+ let currentPath = '';
306
+ for (const part of parts) {
307
+ currentPath += '/' + part;
308
+ try {
309
+ await this.sendCommand(`MKD ${currentPath}`);
310
+ }
311
+ catch {
312
+ }
313
+ }
314
+ return {
315
+ success: true,
316
+ message: `Created directory ${remotePath}`
317
+ };
318
+ }
319
+ const response = await this.sendCommand(`MKD ${remotePath}`);
238
320
  return {
239
- success: true,
240
- message: `Created directory ${remotePath}`
321
+ success: response.code === ResponseCode.PATHNAME_CREATED,
322
+ code: response.code,
323
+ message: response.code === ResponseCode.PATHNAME_CREATED
324
+ ? `Created directory ${remotePath}`
325
+ : response.message
241
326
  };
242
327
  }
243
328
  catch (error) {
@@ -250,10 +335,13 @@ export class FTP {
250
335
  async rmdir(remotePath) {
251
336
  this.ensureConnected();
252
337
  try {
253
- await this.client.removeDir(remotePath);
338
+ const response = await this.sendCommand(`RMD ${remotePath}`);
254
339
  return {
255
- success: true,
256
- message: `Removed directory ${remotePath}`
340
+ success: response.code === ResponseCode.FILE_ACTION_OK,
341
+ code: response.code,
342
+ message: response.code === ResponseCode.FILE_ACTION_OK
343
+ ? `Removed directory ${remotePath}`
344
+ : response.message
257
345
  };
258
346
  }
259
347
  catch (error) {
@@ -266,10 +354,17 @@ export class FTP {
266
354
  async rename(oldPath, newPath) {
267
355
  this.ensureConnected();
268
356
  try {
269
- await this.client.rename(oldPath, newPath);
357
+ const rnfrResponse = await this.sendCommand(`RNFR ${oldPath}`);
358
+ if (rnfrResponse.code !== ResponseCode.FILE_ACTION_PENDING) {
359
+ throw new Error(rnfrResponse.message);
360
+ }
361
+ const rntoResponse = await this.sendCommand(`RNTO ${newPath}`);
270
362
  return {
271
- success: true,
272
- message: `Renamed ${oldPath} to ${newPath}`
363
+ success: rntoResponse.code === ResponseCode.FILE_ACTION_OK,
364
+ code: rntoResponse.code,
365
+ message: rntoResponse.code === ResponseCode.FILE_ACTION_OK
366
+ ? `Renamed ${oldPath} to ${newPath}`
367
+ : rntoResponse.message
273
368
  };
274
369
  }
275
370
  catch (error) {
@@ -282,10 +377,12 @@ export class FTP {
282
377
  async pwd() {
283
378
  this.ensureConnected();
284
379
  try {
285
- const dir = await this.client.pwd();
380
+ const response = await this.sendCommand('PWD');
381
+ const match = response.message.match(/"([^"]+)"/);
382
+ const path = match ? match[1] : '/';
286
383
  return {
287
384
  success: true,
288
- data: dir
385
+ data: path
289
386
  };
290
387
  }
291
388
  catch (error) {
@@ -298,10 +395,13 @@ export class FTP {
298
395
  async cd(remotePath) {
299
396
  this.ensureConnected();
300
397
  try {
301
- await this.client.cd(remotePath);
398
+ const response = await this.sendCommand(`CWD ${remotePath}`);
302
399
  return {
303
- success: true,
304
- message: `Changed to ${remotePath}`
400
+ success: response.code === ResponseCode.FILE_ACTION_OK,
401
+ code: response.code,
402
+ message: response.code === ResponseCode.FILE_ACTION_OK
403
+ ? `Changed to ${remotePath}`
404
+ : response.message
305
405
  };
306
406
  }
307
407
  catch (error) {
@@ -314,9 +414,10 @@ export class FTP {
314
414
  async size(remotePath) {
315
415
  this.ensureConnected();
316
416
  try {
317
- const size = await this.client.size(remotePath);
417
+ const response = await this.sendCommand(`SIZE ${remotePath}`);
418
+ const size = parseInt(response.message.split(' ').pop() || '0', 10);
318
419
  return {
319
- success: true,
420
+ success: response.code === ResponseCode.FILE_STATUS,
320
421
  data: size
321
422
  };
322
423
  }
@@ -332,8 +433,10 @@ export class FTP {
332
433
  try {
333
434
  const parentPath = remotePath.split('/').slice(0, -1).join('/') || '/';
334
435
  const fileName = remotePath.split('/').pop() || '';
335
- const files = await this.client.list(parentPath);
336
- return files.some((f) => f.name === fileName);
436
+ const result = await this.list(parentPath);
437
+ if (!result.success || !result.data)
438
+ return false;
439
+ return result.data.some((f) => f.name === fileName);
337
440
  }
338
441
  catch {
339
442
  return false;
@@ -345,28 +448,338 @@ export class FTP {
345
448
  }
346
449
  async close() {
347
450
  this.connected = false;
348
- this.client.close();
451
+ if (this.controlSocket) {
452
+ try {
453
+ await this.sendCommand('QUIT');
454
+ }
455
+ catch {
456
+ }
457
+ this.cleanup();
458
+ }
459
+ }
460
+ getSocket() {
461
+ if (!this.controlSocket) {
462
+ throw new Error('Not connected. Call connect() first.');
463
+ }
464
+ return this.controlSocket;
465
+ }
466
+ async connectPlain() {
467
+ return new Promise((resolve, reject) => {
468
+ const socket = this.createSocket();
469
+ socket.setTimeout(this.config.timeout);
470
+ socket.on('connect', () => {
471
+ this.controlSocket = socket;
472
+ resolve();
473
+ });
474
+ socket.on('error', (err) => {
475
+ reject(err);
476
+ });
477
+ socket.on('timeout', () => {
478
+ socket.destroy();
479
+ reject(new Error('Connection timeout'));
480
+ });
481
+ socket.connect(this.config.port, this.config.host);
482
+ });
349
483
  }
350
- getClient() {
351
- return this.client;
484
+ async connectImplicitTLS() {
485
+ return new Promise((resolve, reject) => {
486
+ const options = {
487
+ host: this.config.host,
488
+ port: this.config.port,
489
+ timeout: this.config.timeout,
490
+ rejectUnauthorized: false,
491
+ ...this.config.tlsOptions,
492
+ };
493
+ const socket = tlsConnect(options, () => {
494
+ this.controlSocket = socket;
495
+ this.secureConnection = true;
496
+ resolve();
497
+ });
498
+ socket.on('error', reject);
499
+ socket.setTimeout(this.config.timeout);
500
+ socket.on('timeout', () => {
501
+ socket.destroy();
502
+ reject(new Error('Connection timeout'));
503
+ });
504
+ });
505
+ }
506
+ async upgradeToTLS() {
507
+ const authResponse = await this.sendCommand('AUTH TLS');
508
+ if (authResponse.code !== ResponseCode.AUTH_OK) {
509
+ throw new Error(`AUTH TLS failed: ${authResponse.message}`);
510
+ }
511
+ return new Promise((resolve, reject) => {
512
+ const options = {
513
+ socket: this.controlSocket,
514
+ rejectUnauthorized: false,
515
+ ...this.config.tlsOptions,
516
+ };
517
+ const tlsSocket = tlsConnect(options, () => {
518
+ this.controlSocket = tlsSocket;
519
+ this.secureConnection = true;
520
+ resolve();
521
+ });
522
+ tlsSocket.on('error', reject);
523
+ });
524
+ }
525
+ async authenticate() {
526
+ const userResponse = await this.sendCommand(`USER ${this.config.user}`);
527
+ if (userResponse.code === ResponseCode.USER_LOGGED_IN) {
528
+ return;
529
+ }
530
+ if (userResponse.code !== ResponseCode.NEED_PASSWORD) {
531
+ throw new Error(`Authentication failed: ${userResponse.message}`);
532
+ }
533
+ const passResponse = await this.sendCommand(`PASS ${this.config.password}`);
534
+ if (passResponse.code !== ResponseCode.USER_LOGGED_IN) {
535
+ throw new Error(`Authentication failed: ${passResponse.message}`);
536
+ }
537
+ if (this.secureConnection) {
538
+ await this.sendCommand('PBSZ 0');
539
+ await this.sendCommand('PROT P');
540
+ }
541
+ }
542
+ async openDataConnection() {
543
+ const pasvResponse = await this.sendCommand('PASV');
544
+ if (pasvResponse.code !== ResponseCode.ENTERING_PASSIVE) {
545
+ throw new Error(`PASV failed: ${pasvResponse.message}`);
546
+ }
547
+ const match = pasvResponse.message.match(/\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)/);
548
+ if (!match) {
549
+ throw new Error('Failed to parse PASV response');
550
+ }
551
+ const host = `${match[1]}.${match[2]}.${match[3]}.${match[4]}`;
552
+ const port = parseInt(match[5], 10) * 256 + parseInt(match[6], 10);
553
+ this.debug('PASV: %s:%d', host, port);
554
+ return new Promise((resolve, reject) => {
555
+ if (this.secureConnection) {
556
+ const options = {
557
+ host,
558
+ port,
559
+ rejectUnauthorized: false,
560
+ ...this.config.tlsOptions,
561
+ };
562
+ const socket = tlsConnect(options, () => {
563
+ resolve(socket);
564
+ });
565
+ socket.on('error', reject);
566
+ }
567
+ else {
568
+ const socket = this.createSocket();
569
+ socket.on('connect', () => resolve(socket));
570
+ socket.on('error', reject);
571
+ socket.connect(port, host);
572
+ }
573
+ });
574
+ }
575
+ async readDataConnection(socket) {
576
+ return new Promise((resolve, reject) => {
577
+ const chunks = [];
578
+ socket.on('data', (chunk) => {
579
+ chunks.push(chunk);
580
+ });
581
+ socket.on('end', async () => {
582
+ const data = Buffer.concat(chunks).toString('utf8');
583
+ try {
584
+ const response = await this.readResponse();
585
+ if (response.code !== ResponseCode.CLOSING_DATA_CONNECTION &&
586
+ response.code !== ResponseCode.FILE_ACTION_OK) {
587
+ reject(new Error(response.message));
588
+ }
589
+ else {
590
+ resolve(data);
591
+ }
592
+ }
593
+ catch (err) {
594
+ reject(err);
595
+ }
596
+ });
597
+ socket.on('error', reject);
598
+ });
599
+ }
600
+ async sendCommand(command) {
601
+ if (!this.controlSocket) {
602
+ throw new Error('Not connected');
603
+ }
604
+ const displayCommand = command.startsWith('PASS ')
605
+ ? 'PASS ****'
606
+ : command;
607
+ this.debug('>>> %s', displayCommand);
608
+ return new Promise((resolve, reject) => {
609
+ this.controlSocket.write(command + '\r\n', 'utf8', async (err) => {
610
+ if (err) {
611
+ reject(err);
612
+ return;
613
+ }
614
+ try {
615
+ const response = await this.readResponse();
616
+ if (response.code >= 400) {
617
+ reject(new Error(response.message));
618
+ }
619
+ else {
620
+ resolve(response);
621
+ }
622
+ }
623
+ catch (error) {
624
+ reject(error);
625
+ }
626
+ });
627
+ });
628
+ }
629
+ async readResponse() {
630
+ return new Promise((resolve, reject) => {
631
+ const onData = (data) => {
632
+ this.responseBuffer += data.toString('utf8');
633
+ const lines = this.responseBuffer.split('\r\n');
634
+ for (let i = 0; i < lines.length - 1; i++) {
635
+ const line = lines[i];
636
+ const match = line.match(/^(\d{3})([ -])(.*)/);
637
+ if (match) {
638
+ const code = parseInt(match[1], 10);
639
+ const isFinal = match[2] === ' ';
640
+ const message = match[3];
641
+ if (isFinal) {
642
+ this.responseBuffer = lines.slice(i + 1).join('\r\n');
643
+ this.controlSocket.removeListener('data', onData);
644
+ this.controlSocket.removeListener('error', onError);
645
+ this.debug('<<< %d %s', code, message);
646
+ resolve({ code, message: `${code} ${message}` });
647
+ return;
648
+ }
649
+ }
650
+ }
651
+ };
652
+ const onError = (err) => {
653
+ this.controlSocket.removeListener('data', onData);
654
+ reject(err);
655
+ };
656
+ this.controlSocket.on('data', onData);
657
+ this.controlSocket.on('error', onError);
658
+ onData(Buffer.alloc(0));
659
+ });
660
+ }
661
+ parseDirectoryListing(data) {
662
+ const lines = data.split('\r\n').filter(Boolean);
663
+ const items = [];
664
+ for (const line of lines) {
665
+ const item = this.parseListLine(line);
666
+ if (item) {
667
+ items.push(item);
668
+ }
669
+ }
670
+ return items;
671
+ }
672
+ parseListLine(line) {
673
+ const unixMatch = line.match(/^([d\-l])([rwx\-]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+[\d:]+)\s+(.+)$/);
674
+ if (unixMatch) {
675
+ const typeChar = unixMatch[1];
676
+ const permissions = unixMatch[2];
677
+ const size = parseInt(unixMatch[3], 10);
678
+ const rawDate = unixMatch[4];
679
+ const name = unixMatch[5];
680
+ return {
681
+ name,
682
+ type: this.mapFileType(typeChar),
683
+ size,
684
+ permissions,
685
+ rawModifiedAt: rawDate,
686
+ modifiedAt: this.parseDate(rawDate)
687
+ };
688
+ }
689
+ const dosMatch = line.match(/^(\d{2}-\d{2}-\d{2})\s+(\d{1,2}:\d{2}[AP]M)\s+(<DIR>|\d+)\s+(.+)$/i);
690
+ if (dosMatch) {
691
+ const date = dosMatch[1];
692
+ const time = dosMatch[2];
693
+ const sizeOrDir = dosMatch[3];
694
+ const name = dosMatch[4];
695
+ const isDir = sizeOrDir.toUpperCase() === '<DIR>';
696
+ return {
697
+ name,
698
+ type: isDir ? 'directory' : 'file',
699
+ size: isDir ? 0 : parseInt(sizeOrDir, 10),
700
+ rawModifiedAt: `${date} ${time}`,
701
+ modifiedAt: this.parseDosDate(date, time)
702
+ };
703
+ }
704
+ if (line.trim() && !line.includes(' ')) {
705
+ return {
706
+ name: line.trim(),
707
+ type: 'unknown',
708
+ size: 0
709
+ };
710
+ }
711
+ return null;
712
+ }
713
+ mapFileType(typeChar) {
714
+ switch (typeChar) {
715
+ case '-': return 'file';
716
+ case 'd': return 'directory';
717
+ case 'l': return 'link';
718
+ default: return 'unknown';
719
+ }
720
+ }
721
+ parseDate(rawDate) {
722
+ try {
723
+ const now = new Date();
724
+ const parts = rawDate.trim().split(/\s+/);
725
+ if (parts.length >= 3) {
726
+ const month = parts[0];
727
+ const day = parseInt(parts[1], 10);
728
+ const timeOrYear = parts[2];
729
+ if (timeOrYear.includes(':')) {
730
+ const [hours, minutes] = timeOrYear.split(':').map(Number);
731
+ const date = new Date(`${month} ${day} ${now.getFullYear()} ${hours}:${minutes}`);
732
+ if (date > now) {
733
+ date.setFullYear(date.getFullYear() - 1);
734
+ }
735
+ return date;
736
+ }
737
+ else {
738
+ return new Date(`${month} ${day} ${timeOrYear}`);
739
+ }
740
+ }
741
+ }
742
+ catch {
743
+ }
744
+ return undefined;
745
+ }
746
+ parseDosDate(date, time) {
747
+ try {
748
+ const [month, day, year] = date.split('-').map(Number);
749
+ const fullYear = year < 70 ? 2000 + year : 1900 + year;
750
+ const timeMatch = time.match(/(\d{1,2}):(\d{2})([AP]M)/i);
751
+ if (timeMatch) {
752
+ let hours = parseInt(timeMatch[1], 10);
753
+ const minutes = parseInt(timeMatch[2], 10);
754
+ const isPM = timeMatch[3].toUpperCase() === 'PM';
755
+ if (isPM && hours < 12)
756
+ hours += 12;
757
+ if (!isPM && hours === 12)
758
+ hours = 0;
759
+ return new Date(fullYear, month - 1, day, hours, minutes);
760
+ }
761
+ }
762
+ catch {
763
+ }
764
+ return undefined;
352
765
  }
353
766
  ensureConnected() {
354
- if (!this.connected || this.client.closed) {
767
+ if (!this.connected || !this.controlSocket || this.controlSocket.destroyed) {
355
768
  throw new Error('Not connected to FTP server. Call connect() first.');
356
769
  }
357
770
  }
358
- mapFileType(type) {
359
- switch (type) {
360
- case 0:
361
- return 'unknown';
362
- case 1:
363
- return 'file';
364
- case 2:
365
- return 'directory';
366
- case 3:
367
- return 'link';
368
- default:
369
- return 'unknown';
771
+ cleanup() {
772
+ if (this.controlSocket) {
773
+ this.controlSocket.destroy();
774
+ this.controlSocket = null;
775
+ }
776
+ this.connected = false;
777
+ this.secureConnection = false;
778
+ this.responseBuffer = '';
779
+ }
780
+ debug(format, ...args) {
781
+ if (this.config.verbose) {
782
+ console.log(`[FTP] ${format}`, ...args);
370
783
  }
371
784
  }
372
785
  }