@jalsoedesign/filezilla-cli 1.0.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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.d.ts +33 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +427 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/get.d.ts +3 -0
- package/dist/commands/get.d.ts.map +1 -0
- package/dist/commands/get.js +67 -0
- package/dist/commands/get.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +62 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +48 -0
- package/dist/types.js.map +1 -0
- package/examples/transfer-recipes.ts +184 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getDefaultSiteManager, getSiteManager } from '@jalsoedesign/filezilla-core';
|
|
2
|
+
import { listCommand } from './commands/list.js';
|
|
3
|
+
import { getCommand } from './commands/get.js';
|
|
4
|
+
import { runCheckCommand } from './commands/check.js';
|
|
5
|
+
export { checkConnection } from './commands/check.js';
|
|
6
|
+
import * as process from 'process';
|
|
7
|
+
function showHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
Usage: filezilla-js <command> [options] [args]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
list List all servers
|
|
13
|
+
get <path> [property] Get a server or a specific property value
|
|
14
|
+
check <path> Check a connection without remote mutations (check --help)
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--file <path> Specify the sitemanager.xml file path
|
|
18
|
+
--json Output in JSON format
|
|
19
|
+
--table Output in table format
|
|
20
|
+
--search <term> Search for a server by name or path
|
|
21
|
+
--recurse Return results in a nested structure (requires --json)
|
|
22
|
+
--full Return full server properties when using list command
|
|
23
|
+
--show-password Show the actual password in output (otherwise hidden)
|
|
24
|
+
--color Force color output (on by default on Windows TTY)
|
|
25
|
+
--no-color Disable color output
|
|
26
|
+
--help, -h Show this help message
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
filezilla-js list
|
|
30
|
+
filezilla-js list --json
|
|
31
|
+
filezilla-js list --table
|
|
32
|
+
filezilla-js get "My Server"
|
|
33
|
+
filezilla-js get "My Server" host
|
|
34
|
+
filezilla-js get "My Server" --json
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
function parseArgs() {
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
const options = {};
|
|
40
|
+
const positional = [];
|
|
41
|
+
for (let i = 0; i < args.length; i++) {
|
|
42
|
+
const arg = args[i];
|
|
43
|
+
if (arg === '--json') {
|
|
44
|
+
options.json = true;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === '--table') {
|
|
47
|
+
options.table = true;
|
|
48
|
+
}
|
|
49
|
+
else if (arg === '--full') {
|
|
50
|
+
options.full = true;
|
|
51
|
+
}
|
|
52
|
+
else if (arg === '--recurse') {
|
|
53
|
+
options.recurse = true;
|
|
54
|
+
}
|
|
55
|
+
else if (arg === '--show-password') {
|
|
56
|
+
options.showPassword = true;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === '--file' && i + 1 < args.length) {
|
|
59
|
+
options.file = args[++i];
|
|
60
|
+
}
|
|
61
|
+
else if (arg === '--search' && i + 1 < args.length) {
|
|
62
|
+
options.search = args[++i];
|
|
63
|
+
}
|
|
64
|
+
else if (arg === '--help' || arg === '-h') {
|
|
65
|
+
options.help = true;
|
|
66
|
+
}
|
|
67
|
+
else if (arg.startsWith('--')) {
|
|
68
|
+
// ignore unknown flags
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
positional.push(arg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { options, positional };
|
|
75
|
+
}
|
|
76
|
+
export async function run() {
|
|
77
|
+
if (process.argv[2] === 'check') {
|
|
78
|
+
await runCheckCommand(process.argv.slice(3));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const { options, positional } = parseArgs();
|
|
82
|
+
if (options.help || positional.length === 0) {
|
|
83
|
+
showHelp();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const command = positional[0];
|
|
87
|
+
const commandArgs = positional.slice(1);
|
|
88
|
+
const commands = {
|
|
89
|
+
list: listCommand,
|
|
90
|
+
get: getCommand,
|
|
91
|
+
};
|
|
92
|
+
const handler = commands[command];
|
|
93
|
+
if (!handler) {
|
|
94
|
+
console.error(`Error: Unknown command "${command}"`);
|
|
95
|
+
showHelp();
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const siteManager = options.file ?
|
|
100
|
+
getSiteManager(options.file) :
|
|
101
|
+
getDefaultSiteManager();
|
|
102
|
+
const context = {
|
|
103
|
+
siteManager,
|
|
104
|
+
args: commandArgs,
|
|
105
|
+
options,
|
|
106
|
+
};
|
|
107
|
+
await handler(context);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (options.json) {
|
|
111
|
+
console.error(JSON.stringify({ error: e.message }));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.error(`Error: ${e.message}`);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SiteManager } from '@jalsoedesign/filezilla-core';
|
|
2
|
+
export interface CommandContext {
|
|
3
|
+
siteManager: SiteManager;
|
|
4
|
+
args: string[];
|
|
5
|
+
options: {
|
|
6
|
+
json?: boolean;
|
|
7
|
+
table?: boolean;
|
|
8
|
+
file?: string;
|
|
9
|
+
help?: boolean;
|
|
10
|
+
search?: string;
|
|
11
|
+
recurse?: boolean;
|
|
12
|
+
full?: boolean;
|
|
13
|
+
showPassword?: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export type CommandHandler = (ctx: CommandContext) => Promise<void> | void;
|
|
17
|
+
export declare function formatOutput(data: string | number | boolean | any[] | Record<string, any>, options: CommandContext['options'], tableData?: {
|
|
18
|
+
columns: string[];
|
|
19
|
+
rows: (string | number | boolean | undefined)[][];
|
|
20
|
+
}): void;
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAC,MAAM,8BAA8B,CAAC;AAGzD,MAAM,WAAW,cAAc;IAC3B,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE;QACL,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,YAAY,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;CACL;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE3E,wBAAgB,YAAY,CACxB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7D,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,EAClC,SAAS,CAAC,EAAE;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,EAAE,EAAE,CAAA;CAAC,QAuDrF"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function formatOutput(data, options, tableData) {
|
|
3
|
+
if (options.json) {
|
|
4
|
+
console.log(JSON.stringify(data, null, 2));
|
|
5
|
+
}
|
|
6
|
+
else if (options.table && tableData) {
|
|
7
|
+
const colWidths = tableData.columns.map((col, i) => Math.max(col.length, ...tableData.rows.map(row => String(row[i] ?? '').length)));
|
|
8
|
+
const borderChars = {
|
|
9
|
+
top: { start: '┌', mid: '┬', end: '┐' },
|
|
10
|
+
middle: { start: '├', mid: '┼', end: '┤' },
|
|
11
|
+
bottom: { start: '└', mid: '┴', end: '┘' },
|
|
12
|
+
side: { start: '│', mid: '│', end: '│' },
|
|
13
|
+
};
|
|
14
|
+
const colored = Object.fromEntries(Object.entries(borderChars).map(([type, chars]) => [
|
|
15
|
+
type,
|
|
16
|
+
Object.fromEntries(Object.entries(chars).map(([k, v]) => [k, chalk.cyan(v)])),
|
|
17
|
+
]));
|
|
18
|
+
const buildBorder = (widths, type) => {
|
|
19
|
+
const c = colored[type];
|
|
20
|
+
return c.start + widths.map(w => chalk.cyan('─'.repeat(w + 2))).join(c.mid) + c.end;
|
|
21
|
+
};
|
|
22
|
+
const side = colored.side.mid;
|
|
23
|
+
const buildRow = (cells, widths) => side + ' ' + cells.map((cell, i) => String(cell ?? '').padEnd(widths[i])).join(' ' + side + ' ') + ' ' + side;
|
|
24
|
+
console.log(buildBorder(colWidths, 'top'));
|
|
25
|
+
console.log(side + ' ' + tableData.columns.map((col, i) => chalk.bold.white(col.padEnd(colWidths[i]))).join(' ' + side + ' ') + ' ' + side);
|
|
26
|
+
console.log(buildBorder(colWidths, 'middle'));
|
|
27
|
+
tableData.rows.forEach(row => console.log(buildRow(row, colWidths)));
|
|
28
|
+
console.log(buildBorder(colWidths, 'bottom'));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') {
|
|
32
|
+
console.log(data);
|
|
33
|
+
}
|
|
34
|
+
else if (Array.isArray(data)) {
|
|
35
|
+
data.forEach(item => console.log(chalk.green('• ') + item));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
39
|
+
let displayValue = String(value);
|
|
40
|
+
if (key === 'password' && !options.showPassword && value) {
|
|
41
|
+
displayValue = chalk.gray('(hidden)');
|
|
42
|
+
}
|
|
43
|
+
console.log(`${chalk.yellow(key.padEnd(26))}: ${displayValue}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {createReadStream} from 'node:fs';
|
|
2
|
+
import {open, stat, unlink} from 'node:fs/promises';
|
|
3
|
+
import {Transform} from 'node:stream';
|
|
4
|
+
import {pipeline} from 'node:stream/promises';
|
|
5
|
+
import type {Server} from '@jalsoedesign/filezilla-core';
|
|
6
|
+
import {SftpConnectorFactory, type SftpConnector} from '@jalsoedesign/filezilla-connector-sftp';
|
|
7
|
+
import {
|
|
8
|
+
checkAbort,
|
|
9
|
+
IntegrityError,
|
|
10
|
+
NotSupportedError,
|
|
11
|
+
publishFile,
|
|
12
|
+
ResourceLimitError,
|
|
13
|
+
toReadable,
|
|
14
|
+
type ConnectorOperationOptions,
|
|
15
|
+
type PublicationResult,
|
|
16
|
+
type TransferConnector,
|
|
17
|
+
} from '@jalsoedesign/filezilla-connector-abstract';
|
|
18
|
+
|
|
19
|
+
export interface RecipeConnector extends TransferConnector {
|
|
20
|
+
connect(options?: ConnectorOperationOptions): Promise<void>;
|
|
21
|
+
disconnect(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The caller supplies an independently verified public fingerprint and owns saved credentials. */
|
|
25
|
+
export function createPinnedSftp(server: Server, fingerprint: string): SftpConnector {
|
|
26
|
+
if (!/^SHA256:[A-Za-z0-9+/]{43}$/.test(fingerprint)) {
|
|
27
|
+
throw new TypeError('Provide a canonical, independently verified SHA256 host-key fingerprint.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return SftpConnectorFactory.fromServer(server, {
|
|
31
|
+
requireTrustPolicy: true,
|
|
32
|
+
hasTrustPolicy: challenge => challenge.fingerprint === fingerprint,
|
|
33
|
+
acceptTrustPolicy: () => false,
|
|
34
|
+
autoReconnect: false,
|
|
35
|
+
maxTransientRetries: 0,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The callback must await complete stream consumption before returning. */
|
|
40
|
+
export async function withConnectedSite<T>(
|
|
41
|
+
createConnector: () => RecipeConnector,
|
|
42
|
+
options: ConnectorOperationOptions,
|
|
43
|
+
operation: (connector: RecipeConnector) => Promise<T>,
|
|
44
|
+
): Promise<T> {
|
|
45
|
+
checkAbort(options.abortSignal);
|
|
46
|
+
|
|
47
|
+
const connector = createConnector();
|
|
48
|
+
let operationFailed = false;
|
|
49
|
+
let operationError: unknown;
|
|
50
|
+
let value: T | undefined;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await connector.connect(options);
|
|
54
|
+
|
|
55
|
+
value = await operation(connector);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
operationFailed = true;
|
|
58
|
+
operationError = error;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await connector.disconnect();
|
|
63
|
+
} catch (cleanupError) {
|
|
64
|
+
if (operationFailed) {
|
|
65
|
+
throw new AggregateError([operationError, cleanupError],
|
|
66
|
+
'The operation and connection cleanup both failed.', {cause: cleanupError});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw cleanupError;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (operationFailed) {
|
|
73
|
+
throw operationError;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return value as T;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DownloadRecipeOptions extends ConnectorOperationOptions {
|
|
80
|
+
maxBytes?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Creates a new local file exclusively. Failure removes only the file this invocation created. */
|
|
84
|
+
export async function downloadToNewFile(
|
|
85
|
+
createConnector: () => RecipeConnector,
|
|
86
|
+
remotePath: string,
|
|
87
|
+
localPath: string,
|
|
88
|
+
options: DownloadRecipeOptions = {},
|
|
89
|
+
): Promise<{bytesWritten: number}> {
|
|
90
|
+
const maxBytes = options.maxBytes ?? 1073741824;
|
|
91
|
+
|
|
92
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
93
|
+
throw new RangeError('maxBytes must be a non-negative safe integer.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let owned = false;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
return await withConnectedSite(createConnector, options, async connector => {
|
|
100
|
+
const metadata = await connector.stat(remotePath, options);
|
|
101
|
+
|
|
102
|
+
if (metadata.type !== 'file') {
|
|
103
|
+
throw new NotSupportedError('This recipe downloads regular files only.');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (metadata.size !== undefined && metadata.size > maxBytes) {
|
|
107
|
+
throw new ResourceLimitError('The download exceeds maxBytes.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const file = await open(localPath, 'wx', 0o600);
|
|
111
|
+
let bytesWritten = 0;
|
|
112
|
+
|
|
113
|
+
owned = true;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const source = toReadable(await connector.read(remotePath, options));
|
|
117
|
+
const counter = new Transform({
|
|
118
|
+
transform(chunk: Buffer, _encoding, callback) {
|
|
119
|
+
bytesWritten += chunk.length;
|
|
120
|
+
callback(bytesWritten > maxBytes ? new ResourceLimitError('The download exceeds maxBytes.') :
|
|
121
|
+
null, chunk);
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await pipeline(source, counter, file.createWriteStream({autoClose: true}), {
|
|
126
|
+
signal: options.abortSignal,
|
|
127
|
+
});
|
|
128
|
+
checkAbort(options.abortSignal);
|
|
129
|
+
|
|
130
|
+
if (metadata.size !== undefined && metadata.size !== bytesWritten) {
|
|
131
|
+
throw new IntegrityError('The remote file changed length or the download ended early.');
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
await file.close();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {bytesWritten};
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (owned) {
|
|
141
|
+
try {
|
|
142
|
+
await unlink(localPath);
|
|
143
|
+
} catch (cleanupError) {
|
|
144
|
+
throw new AggregateError([error, cleanupError],
|
|
145
|
+
'The download and local-file cleanup both failed.', {cause: cleanupError});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface PublishRecipeOptions extends ConnectorOperationOptions {
|
|
154
|
+
/** Explicit integrity value supplied by the application, never guessed by the recipe. */
|
|
155
|
+
expectedSha256?: string;
|
|
156
|
+
/** Fail unless the selected connector/server can honor this rename guarantee. */
|
|
157
|
+
requireAtomicRename?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Publishes to a new remote name with overwrite:'fail'; deployment replacement policy stays with the application. */
|
|
161
|
+
export async function publishNewFile(
|
|
162
|
+
createConnector: () => RecipeConnector,
|
|
163
|
+
localPath: string,
|
|
164
|
+
remotePath: string,
|
|
165
|
+
options: PublishRecipeOptions = {},
|
|
166
|
+
): Promise<PublicationResult> {
|
|
167
|
+
checkAbort(options.abortSignal);
|
|
168
|
+
|
|
169
|
+
const metadata = await stat(localPath);
|
|
170
|
+
|
|
171
|
+
if (!metadata.isFile()) {
|
|
172
|
+
throw new NotSupportedError('This recipe publishes regular files only.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return withConnectedSite(createConnector, options, connector => publishFile(connector, remotePath,
|
|
176
|
+
() => createReadStream(localPath), {
|
|
177
|
+
...options,
|
|
178
|
+
overwrite: 'fail',
|
|
179
|
+
totalBytes: metadata.size,
|
|
180
|
+
...(options.expectedSha256 === undefined ? {} : {
|
|
181
|
+
verify: {algorithm: 'sha256', expectedDigest: options.expectedSha256, maxBytes: metadata.size},
|
|
182
|
+
}),
|
|
183
|
+
}));
|
|
184
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jalsoedesign/filezilla-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Command-line interface for querying FileZilla sitemanager.xml files",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"filezilla-js": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"lint": "eslint src tests --no-error-on-unmatched-pattern --max-warnings 0",
|
|
17
|
+
"prepack": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@jalsoedesign/filezilla-core": "^1.0.0",
|
|
21
|
+
"chalk": "6.0.0",
|
|
22
|
+
"@jalsoedesign/filezilla-connector-abstract": "^1.0.0",
|
|
23
|
+
"@jalsoedesign/filezilla-connector-ftp": "^1.0.0",
|
|
24
|
+
"@jalsoedesign/filezilla-connector-sftp": "^1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"vitest": "5.0.0",
|
|
28
|
+
"typescript": "6.0.3",
|
|
29
|
+
"tsx": "4.23.13"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.22.2"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"examples"
|
|
39
|
+
],
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"default": "./dist/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json",
|
|
46
|
+
"./dist/*": "./dist/*"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/h2ooooooo/filezilla-js.git",
|
|
51
|
+
"directory": "packages/cli"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://h2ooooooo.github.io/filezilla-js/",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/h2ooooooo/filezilla-js/issues"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|