@qse/ssh-sftp 1.0.0 → 1.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.
package/README.md CHANGED
@@ -36,6 +36,8 @@ npm i @qse/ssh-sftp -D
36
36
 
37
37
  ### `.sftprc.json`
38
38
 
39
+ schema定义 http://www.zhidianbao.cn:8088/qsxxwapdev/edu-ssh-sftp/sftprc.schema.json
40
+
39
41
  | 字段名 | 类型 | 描述 | 默认值 |
40
42
  | ---------------- | ---------------------------------------------------- | ---------------------------------------------- | ---------------------- |
41
43
  | localPath | `string` | | `'dist'` |
package/lib/cli.js CHANGED
@@ -7,21 +7,14 @@ const {
7
7
  sshSftpShowUrl,
8
8
  sshSftpShowConfig
9
9
  } = require('./index');
10
-
11
10
  const fs = require('fs');
12
-
13
11
  const ora = require('ora');
14
-
15
12
  const {
16
13
  exitWithError
17
14
  } = require('./utils');
18
-
19
15
  const updateNotifier = require('update-notifier');
20
-
21
16
  const pkg = require('../package.json');
22
-
23
17
  const presets = require('./presets');
24
-
25
18
  updateNotifier({
26
19
  pkg,
27
20
  updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
@@ -43,48 +36,37 @@ require('yargs').usage('使用: $0 [command] \n\n代码:svn://192.168.10.168/e
43
36
  v: 'version',
44
37
  h: 'help'
45
38
  }).argv;
46
-
47
39
  function getOpts() {
48
40
  isRoot();
49
-
50
41
  if (!fs.existsSync('.sftprc.json')) {
51
42
  return exitWithError('没找到 .sftprc.json 文件,请先执行 ssh-sftp init');
52
43
  }
53
-
54
44
  const opts = fs.readFileSync('.sftprc.json', 'utf-8');
55
45
  return JSON.parse(opts);
56
46
  }
57
-
58
47
  function isRoot() {
59
48
  if (!fs.existsSync('package.json')) {
60
49
  exitWithError('请在项目的根目录运行(package.json所在的目录)');
61
50
  }
62
51
  }
63
-
64
52
  function upload({
65
53
  clear,
66
54
  yes
67
55
  }) {
68
56
  const opts = getOpts();
69
-
70
57
  if (clear === false) {
71
58
  opts.cleanRemoteFiles = false;
72
59
  }
73
-
74
60
  if (yes) {
75
61
  opts.skipPrompt = true;
76
62
  }
77
-
78
63
  sshSftp(opts);
79
64
  }
80
-
81
65
  function generateDefaultConfigJSON() {
82
66
  isRoot();
83
-
84
67
  if (fs.existsSync('.sftprc.json')) {
85
68
  return exitWithError('已存在 .sftprc.json 文件,请勿重复生成');
86
69
  }
87
-
88
70
  fs.writeFileSync('.sftprc.json', JSON.stringify({
89
71
  $schema: 'http://www.zhidianbao.cn:8088/qsxxwapdev/edu-ssh-sftp/sftprc.schema.json',
90
72
  localPath: '/path/to/localDir',
@@ -102,25 +84,20 @@ function generateDefaultConfigJSON() {
102
84
  });
103
85
  ora().succeed('.sftprc.json 生成在项目根目录');
104
86
  }
105
-
106
87
  function ls(argv) {
107
88
  if (!argv.u && !argv.d && !argv.i) {
108
89
  argv.u = true;
109
90
  argv.d = true;
110
91
  argv.i = true;
111
92
  }
112
-
113
93
  sshSftpLS(getOpts(), argv);
114
94
  }
115
-
116
95
  function showUrl() {
117
96
  sshSftpShowUrl(getOpts());
118
97
  }
119
-
120
98
  function showConfig() {
121
99
  sshSftpShowConfig(getOpts());
122
100
  }
123
-
124
101
  function showPresets() {
125
102
  console.log(JSON.stringify(presets, null, 2));
126
103
  }
package/lib/index.js CHANGED
@@ -1,33 +1,23 @@
1
1
  "use strict";
2
2
 
3
3
  const Client = require('ssh2-sftp-client');
4
-
5
4
  const ora = require('ora');
6
-
7
5
  const glob = require('glob');
8
-
9
6
  const fs = require('fs');
10
-
11
7
  const minimatch = require('minimatch');
12
-
13
8
  const inquirer = require('inquirer');
14
-
15
9
  const {
16
10
  exitWithError,
17
11
  warn
18
12
  } = require('./utils');
19
-
20
13
  const chalk = require('chalk');
21
-
22
14
  const {
23
15
  presets,
24
16
  servers
25
17
  } = require('./presets');
26
-
27
18
  const path = require('path');
28
- /** @type {Options} */
29
-
30
19
 
20
+ /** @type {Options} */
31
21
  const defualtOpts = {
32
22
  localPath: 'dist',
33
23
  noWarn: false,
@@ -36,6 +26,7 @@ const defualtOpts = {
36
26
  ignore: ['**/*.LICENSE.txt'],
37
27
  securityLock: true
38
28
  };
29
+
39
30
  /**
40
31
  * get upload files
41
32
  *
@@ -43,7 +34,6 @@ const defualtOpts = {
43
34
  * @param {string} remotePath
44
35
  * @param {string[]} ignore
45
36
  */
46
-
47
37
  function getFilesPath(localPath, remotePath, ignore) {
48
38
  const files = glob.sync(`${localPath}/**/*`, {
49
39
  ignore: getSafePattern(ignore, localPath),
@@ -56,6 +46,7 @@ function getFilesPath(localPath, remotePath, ignore) {
56
46
  };
57
47
  });
58
48
  }
49
+
59
50
  /**
60
51
  * get remote ls deep
61
52
  *
@@ -67,8 +58,6 @@ function getFilesPath(localPath, remotePath, ignore) {
67
58
  * @param {FilesOptions} [options]
68
59
  * @return {Promise<{isDir:boolean;path:string}[]>}
69
60
  */
70
-
71
-
72
61
  async function getRemoteDeepFiles(sftp, remotePath, options) {
73
62
  const {
74
63
  patterns
@@ -77,13 +66,10 @@ async function getRemoteDeepFiles(sftp, remotePath, options) {
77
66
  * @param {string} remotePath
78
67
  * @returns {Promise<string[]|string>}
79
68
  */
80
-
81
69
  async function getFiles(remotePath, data = []) {
82
70
  const list = await sftp.list(remotePath);
83
-
84
71
  for (const item of list) {
85
72
  const path = remotePath + '/' + item.name;
86
-
87
73
  if (item.type === 'd') {
88
74
  data.push({
89
75
  isDir: true,
@@ -97,30 +83,27 @@ async function getRemoteDeepFiles(sftp, remotePath, options) {
97
83
  });
98
84
  }
99
85
  }
100
-
101
86
  return data;
102
87
  }
103
-
104
88
  const ls = (await getFiles(remotePath)).filter(o => o.path);
105
-
106
89
  if (patterns.length > 0) {
107
90
  let tmp = ls;
108
91
  const safePatterns = getSafePattern(patterns, remotePath);
109
92
  tmp = tmp.filter(o => safePatterns.some(reg => minimatch(o.path, reg)));
110
93
  return tmp;
111
94
  }
112
-
113
95
  return ls;
114
96
  }
115
-
116
97
  function ensureDiff(local, remote) {
117
98
  return remote.map(file => {
118
99
  const isSame = local.some(local => local.remotePath === file.path);
119
- return { ...file,
100
+ return {
101
+ ...file,
120
102
  isSame
121
103
  };
122
104
  });
123
105
  }
106
+
124
107
  /**
125
108
  * @typedef {Object} Options
126
109
  * @property {string} [localPath]
@@ -133,12 +116,27 @@ function ensureDiff(local, remote) {
133
116
  * @property {boolean} [keepAlive]
134
117
  * @property {boolean} [noWarn]
135
118
  * @property {boolean} [skipPrompt]
119
+ * @property {{remotePath:string;connectOptions?:import('ssh2').ConnectConfig}[]} [targets]
136
120
  * @param {Options} opts
137
121
  */
138
122
 
139
-
140
123
  async function sshSftp(opts) {
141
124
  opts = parseOpts(opts);
125
+ const results = [];
126
+ // 这里只返回第一个目标的结果,是为了兼容以前的代码
127
+ for (const target of opts.targets) {
128
+ results.push(await _sshSftp({
129
+ ...opts,
130
+ ...target
131
+ }));
132
+ }
133
+ return results[0];
134
+ }
135
+
136
+ /**
137
+ * @param {Options} opts
138
+ */
139
+ async function _sshSftp(opts) {
142
140
  const {
143
141
  deployedURL,
144
142
  sftpURL
@@ -146,14 +144,11 @@ async function sshSftp(opts) {
146
144
  console.log('部署网址:', chalk.green(deployedURL));
147
145
  const spinner = ora(`连接服务器 ${sftpURL}`).start();
148
146
  const sftp = new Client();
149
-
150
147
  try {
151
148
  await sftp.connect(opts.connectOptions);
152
149
  spinner.succeed(`已连接 ${sftpURL}`);
153
-
154
150
  if (!(await sftp.exists(opts.remotePath))) {
155
151
  let confirm = false;
156
-
157
152
  if (opts.skipPrompt) {
158
153
  confirm = true;
159
154
  } else {
@@ -164,20 +159,17 @@ async function sshSftp(opts) {
164
159
  });
165
160
  confirm = ans.confirm;
166
161
  }
167
-
168
162
  if (confirm) {
169
163
  await sftp.mkdir(opts.remotePath, true);
170
164
  } else {
171
165
  process.exit();
172
166
  }
173
167
  }
174
-
175
168
  let remoteDeletefiles = [];
176
169
  let localUploadFiles = [];
177
170
  spinner.start('对比本地/远程的文件数量');
178
171
  localUploadFiles = getFilesPath(opts.localPath, opts.remotePath, opts.ignore || []);
179
172
  spinner.succeed(`本地文件数量:${localUploadFiles.length}`);
180
-
181
173
  if (opts.cleanRemoteFiles) {
182
174
  remoteDeletefiles = await getRemoteDeepFiles(sftp, opts.remotePath, {
183
175
  patterns: opts.cleanRemoteFiles === true ? [] : opts.cleanRemoteFiles
@@ -190,7 +182,6 @@ async function sshSftp(opts) {
190
182
  showDeleteFile: Symbol()
191
183
  };
192
184
  let confirm = Confirm.delete;
193
-
194
185
  if (remoteDeletefiles.length > localUploadFiles.length && !opts.skipPrompt) {
195
186
  const showSelect = async () => {
196
187
  const {
@@ -215,14 +206,11 @@ async function sshSftp(opts) {
215
206
  });
216
207
  return confirm;
217
208
  };
218
-
219
209
  do {
220
210
  confirm = await showSelect();
221
-
222
211
  if (confirm === Confirm.stop) {
223
212
  process.exit();
224
213
  }
225
-
226
214
  if (confirm === Confirm.showDeleteFile) {
227
215
  const diffFiles = ensureDiff(localUploadFiles, remoteDeletefiles);
228
216
  diffFiles.forEach(o => {
@@ -232,42 +220,33 @@ async function sshSftp(opts) {
232
220
  }
233
221
  } while (confirm === Confirm.showDeleteFile);
234
222
  }
235
-
236
223
  if (confirm === Confirm.delete) {
237
224
  spinner.start('开始删除远程文件');
238
225
  remoteDeletefiles = mergeDelete(remoteDeletefiles);
239
-
240
226
  for (const i in remoteDeletefiles) {
241
227
  const o = remoteDeletefiles[i];
242
228
  spinner.text = `[${i + 1}/${remoteDeletefiles.length}] 正在删除 ${o.path}`;
243
229
  if (o.isDir) await sftp.rmdir(o.path, true);else await sftp.delete(o.path);
244
230
  }
245
-
246
231
  spinner.succeed(`已删除 ${opts.remotePath}`);
247
232
  }
248
233
  }
249
-
250
234
  spinner.start(`开始上传 ${opts.localPath} 到 ${opts.remotePath}`);
251
-
252
235
  if (Array.isArray(opts.ignore) && opts.ignore.length > 0) {
253
236
  for (const i in localUploadFiles) {
254
237
  const o = localUploadFiles[i];
255
238
  spinner.text = `[${i}/${localUploadFiles.length}] 正在上传 ${o.localPath} 到 ${o.remotePath}`;
256
-
257
239
  if (fs.statSync(o.localPath).isDirectory()) {
258
240
  if (!(await sftp.exists(o.remotePath))) {
259
241
  await sftp.mkdir(o.remotePath);
260
242
  }
261
-
262
243
  continue;
263
244
  }
264
-
265
245
  await sftp.fastPut(o.localPath, o.remotePath);
266
246
  }
267
247
  } else {
268
248
  await sftp.uploadDir(opts.localPath, opts.remotePath);
269
249
  }
270
-
271
250
  spinner.succeed(`已上传 ${opts.localPath} 到 ${opts.remotePath}`);
272
251
  return {
273
252
  sftp,
@@ -275,7 +254,6 @@ async function sshSftp(opts) {
275
254
  };
276
255
  } catch (error) {
277
256
  spinner.fail('异常中断');
278
-
279
257
  if (error.message.includes('sftpConnect')) {
280
258
  exitWithError(`登录失败,请检查 connectOptions 配置项\n原始信息:${error.message}`);
281
259
  } else {
@@ -283,51 +261,51 @@ async function sshSftp(opts) {
283
261
  }
284
262
  } finally {
285
263
  if (!opts.keepAlive) {
286
- sftp.end();
264
+ await sftp.end();
287
265
  }
288
266
  }
289
267
  }
268
+
290
269
  /**
291
270
  * @param {Options} opts
292
271
  * @param {{d:boolean,u:boolean,i:boolean}} lsOpts
293
272
  */
294
-
295
-
296
273
  async function sshSftpLS(opts, lsOpts) {
297
274
  opts = parseOpts(opts);
275
+ for (const target of opts.targets) {
276
+ await _sshSftpLS({
277
+ ...opts,
278
+ ...target
279
+ }, lsOpts);
280
+ }
281
+ }
282
+ async function _sshSftpLS(opts, lsOpts) {
298
283
  const sftp = new Client();
299
-
300
284
  try {
301
285
  await sftp.connect(opts.connectOptions);
302
-
303
286
  if (lsOpts.d) {
304
287
  if (opts.cleanRemoteFiles) {
305
288
  const ls = await getRemoteDeepFiles(sftp, opts.remotePath, {
306
289
  patterns: opts.cleanRemoteFiles === true ? [] : opts.cleanRemoteFiles
307
290
  });
308
291
  console.log(`删除文件 ${opts.remotePath}(${ls.length}):`);
309
-
310
292
  for (const o of ls) {
311
293
  console.log(` - ${o.path}`);
312
294
  }
313
295
  }
314
296
  }
315
-
316
297
  if (lsOpts.i && Array.isArray(opts.ignore) && opts.ignore.length > 0) {
317
298
  let ls = glob.sync(`${opts.localPath}/**/*`);
318
299
  ls = ls.filter(s => getSafePattern(opts.ignore, opts.localPath).some(reg => minimatch(s, reg)));
319
300
  console.log(`忽略文件 (${ls.length}):`);
320
-
321
301
  for (const s of ls) {
322
302
  console.log(` - ${s}`);
323
303
  }
324
304
  }
325
-
326
305
  if (lsOpts.u) {
327
306
  if (opts.ignore && opts.ignore.length > 0) {
328
307
  const ls = getFilesPath(opts.localPath, opts.remotePath, opts.ignore);
329
308
  console.log(`上传文件 (${ls.length}): `);
330
-
331
309
  for (const o of ls) {
332
310
  console.log(` + ${o.localPath}`);
333
311
  }
@@ -338,14 +316,13 @@ async function sshSftpLS(opts, lsOpts) {
338
316
  } catch (error) {
339
317
  console.error(error);
340
318
  } finally {
341
- sftp.end();
319
+ await sftp.end();
342
320
  }
343
321
  }
322
+
344
323
  /**
345
324
  * @param {{isDir:boolean;path:string}[]} files
346
325
  */
347
-
348
-
349
326
  function mergeDelete(files) {
350
327
  let dirs = files.filter(o => o.isDir);
351
328
  dirs.forEach(({
@@ -355,122 +332,125 @@ function mergeDelete(files) {
355
332
  });
356
333
  return files;
357
334
  }
335
+
358
336
  /**
359
337
  * @param {string[]} patterns
360
338
  * @param {string} prefixPath
361
339
  */
362
-
363
-
364
340
  function getSafePattern(patterns, prefixPath) {
365
341
  const safePatterns = patterns.map(s => s.replace(/^[\.\/]*/, prefixPath + '/')).reduce((acc, s) => [...acc, s, s + '/**/*'], []);
366
342
  return safePatterns;
367
343
  }
344
+
368
345
  /**
369
346
  * @param {Options} opts
370
347
  */
371
-
372
-
373
348
  function parseOpts(opts) {
374
349
  opts = Object.assign({}, defualtOpts, opts);
375
350
  process.env.__SFTP_NO_WARN = opts.noWarn;
376
-
377
351
  const pkg = require(path.resolve('package.json'));
378
-
379
352
  if (!pkg.name) {
380
353
  exitWithError('package.json 中的 name 字段不能为空');
381
354
  }
382
-
383
355
  if (pkg.name.startsWith('@')) {
384
356
  // 如果包含scope需要无视掉
385
357
  pkg.name = pkg.name.replace(/@.*\//, '');
386
358
  }
387
-
388
359
  if (opts.preset && opts.preset.server) {
389
360
  if (!servers[opts.preset.server]) exitWithError('未知的 preset.server');
390
- const server = { ...servers[opts.preset.server]
361
+ const server = {
362
+ ...servers[opts.preset.server]
391
363
  };
392
364
  opts.connectOptions = server;
393
365
  }
394
-
395
366
  if (opts.preset && opts.preset.context) {
396
367
  if (!presets[opts.preset.context]) exitWithError('未知的 preset.context');
397
- const preset = { ...presets[opts.preset.context]
368
+ const preset = {
369
+ ...presets[opts.preset.context]
398
370
  };
399
371
  const folder = opts.preset.folder || pkg.name;
400
372
  preset.remotePath = path.join(preset.remotePath, folder);
401
373
  opts = Object.assign({}, preset, opts);
402
374
  }
403
-
404
375
  if (!fs.existsSync(opts.localPath)) {
405
376
  exitWithError(`localPath 配置错误,未找到需要上传的文件夹(${opts.localPath})`);
406
377
  }
407
-
408
378
  if (!fs.statSync(opts.localPath).isDirectory()) {
409
379
  exitWithError('localPath 配置错误,必须是一个文件夹');
410
380
  }
411
-
412
- if (!opts.remotePath) {
413
- exitWithError('remotePath 未配置');
381
+ opts.connectOptions = {
382
+ ...opts.connectOptions,
383
+ readyTimeout: 2 * 60 * 1000
384
+ };
385
+ opts.targets = opts.targets || [];
386
+ if (opts.targets.length === 0 && !opts.remotePath) {
387
+ exitWithError('remotePath 或 targets 未配置');
388
+ }
389
+ if (opts.targets.length === 0) {
390
+ opts.targets.push({
391
+ remotePath: opts.remotePath,
392
+ connectOptions: opts.connectOptions
393
+ });
394
+ } else {
395
+ opts.targets.forEach(target => {
396
+ target.connectOptions = target.connectOptions || opts.connectOptions;
397
+ });
398
+ }
399
+ if (opts.targets.length > 1 && opts.keepAlive) {
400
+ exitWithError('keepAlive 选项在多目标上传时不支持,请设置为 false');
414
401
  }
415
-
416
402
  if (typeof opts.securityLock !== 'boolean') {
417
403
  opts.securityLock = true;
418
404
  }
419
-
420
405
  if (opts.securityLock === false) {
421
406
  warn('请确保自己清楚关闭安全锁(securityLock)后的风险');
422
407
  } else {
423
- if (!opts.remotePath.includes(pkg.name)) {
424
- exitWithError([`remotePath 中不包含项目名称`, `为防止错误上传/删除和保证服务器目录可读性,你必须让remotePath中包含你的项目名称`, `remotePath:${opts.remotePath}`, `项目名称:${pkg.name} // 源自 package.json 中的 name 字段,忽略scope字段`, `\n你可以设置 "securityLock": false 来关闭这个验证`].join('\n'));
408
+ for (const target of opts.targets) {
409
+ if (!target.remotePath.includes(pkg.name)) {
410
+ exitWithError([`remotePath 中不包含项目名称`, `为防止错误上传/删除和保证服务器目录可读性,你必须让remotePath中包含你的项目名称`, `remotePath:${target.remotePath}`, `项目名称:${pkg.name} // 源自 package.json 中的 name 字段,忽略scope字段`, `\n你可以设置 "securityLock": false 来关闭这个验证`].join('\n'));
411
+ }
425
412
  }
426
413
  }
427
-
428
414
  if (opts.ignore) {
429
415
  opts.ignore = [opts.ignore].flat(1).filter(Boolean);
430
416
  }
431
-
432
417
  if (opts.cleanRemoteFiles === true) {
433
418
  opts.cleanRemoteFiles = [];
434
419
  }
435
-
436
420
  if (opts.cleanRemoteFiles) {
437
421
  opts.cleanRemoteFiles = [opts.cleanRemoteFiles].flat(1).filter(Boolean);
438
422
  }
439
-
440
423
  return opts;
441
424
  }
442
-
443
- function getDeployURL(opts) {
444
- const _r = opts.connectOptions;
445
- const sftpURL = `sftp://${_r.username}:${_r.password}@${_r.host}:${_r.port}/${opts.remotePath}`;
425
+ function getDeployURL(target) {
426
+ const _r = target.connectOptions;
427
+ const sftpURL = `sftp://${_r.username}:${_r.password}@${_r.host}:${_r.port}/${target.remotePath}`;
446
428
  let deployedURL = '未知';
447
-
448
429
  for (const context in presets) {
449
430
  const preset = presets[context];
450
- if (!opts.remotePath.startsWith(preset.remotePath)) continue;
451
- const fullPath = opts.remotePath.replace(preset.remotePath, '').slice(1);
431
+ if (!target.remotePath.startsWith(preset.remotePath)) continue;
432
+ const fullPath = target.remotePath.replace(preset.remotePath, '').slice(1);
452
433
  deployedURL = ['http://www.zhidianbao.cn:8088', context, fullPath, ''].join('/');
453
434
  break;
454
435
  }
455
-
456
436
  return {
457
437
  sftpURL,
458
438
  deployedURL
459
439
  };
460
440
  }
441
+
461
442
  /**
462
443
  * @param {Options} opts
463
444
  */
464
-
465
-
466
445
  function sshSftpShowUrl(opts) {
467
446
  opts = parseOpts(opts);
468
- const {
469
- deployedURL
470
- } = getDeployURL(opts);
471
- console.log('部署网址:', chalk.green(deployedURL));
447
+ for (const target of opts.targets) {
448
+ const {
449
+ deployedURL
450
+ } = getDeployURL(target);
451
+ console.log('部署网址:', chalk.green(deployedURL));
452
+ }
472
453
  }
473
-
474
454
  function sshSftpShowConfig(opts) {
475
455
  opts = parseOpts(opts);
476
456
  console.log(JSON.stringify(opts, null, 2));
@@ -481,7 +461,6 @@ function sshSftpShowConfig(opts) {
481
461
  i: true
482
462
  });
483
463
  }
484
-
485
464
  module.exports = sshSftp;
486
465
  module.exports.sshSftp = sshSftp;
487
466
  module.exports.sshSftpLS = sshSftpLS;
package/lib/utils.js CHANGED
@@ -1,40 +1,32 @@
1
1
  "use strict";
2
2
 
3
3
  const chalk = require('chalk');
4
+
4
5
  /**
5
6
  * @param {string} message
6
7
  * @return {never}
7
8
  */
8
-
9
-
10
9
  function exitWithError(message) {
11
10
  console.log(`${chalk.bgRed.white.bold(' ERROR ')} ${chalk.redBright(message)}`);
12
11
  process.exit();
13
12
  }
14
-
15
13
  function warn(message) {
16
14
  if (process.env.__SFTP_NO_WARN) return;
17
15
  console.log(`${chalk.bgYellow.white.bold(' WARN ')} ${chalk.yellow.bold(message)}`);
18
16
  }
19
-
20
17
  function splitOnFirst(string, separator) {
21
18
  if (!(typeof string === 'string' && typeof separator === 'string')) {
22
19
  throw new TypeError('Expected the arguments to be of type `string`');
23
20
  }
24
-
25
21
  if (string === '' || separator === '') {
26
22
  return [];
27
23
  }
28
-
29
24
  const separatorIndex = string.indexOf(separator);
30
-
31
25
  if (separatorIndex === -1) {
32
26
  return [];
33
27
  }
34
-
35
28
  return [string.slice(0, separatorIndex), string.slice(separatorIndex + separator.length)];
36
29
  }
37
-
38
30
  module.exports.splitOnFirst = splitOnFirst;
39
31
  module.exports.exitWithError = exitWithError;
40
32
  module.exports.warn = warn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qse/ssh-sftp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "教育代码部署工具",
5
5
  "main": "lib/index.js",
6
6
  "author": "Ironkinoko <kinoko_main@outlook.com>",
@@ -8,7 +8,7 @@
8
8
  "scripts": {
9
9
  "start": "dumi dev",
10
10
  "docs:build": "dumi build && cp sftprc.schema.json docs-dist",
11
- "docs:deploy": "ssh-sftp",
11
+ "docs:deploy": "node ./src/cli.js",
12
12
  "build": "father-build",
13
13
  "deploy": "yarn docs:build && yarn docs:deploy && rm -rf docs-dist",
14
14
  "release": "yarn build && npm publish && rm -rf lib",