quick-sh 1.0.8 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quick-sh",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "A local script management tool for quick execution of JavaScript and Shell scripts with alias support",
5
5
  "main": "src/lib/script-manager.js",
6
6
  "bin": {
package/src/bin/cli.js CHANGED
@@ -14,6 +14,24 @@ const {
14
14
  removeRemoteScript,
15
15
  SOURCE_TYPES
16
16
  } = require('../lib/remote-manager');
17
+ const {
18
+ connectShell,
19
+ listShells,
20
+ showShellHelp,
21
+ addShell,
22
+ addShellWithPassword,
23
+ editShell,
24
+ editShellInteractive,
25
+ renameShell,
26
+ removeShell,
27
+ addShellInteractive,
28
+ renameShellInteractive,
29
+ removeShellInteractive,
30
+ setShellPassword,
31
+ removeShellPassword,
32
+ setPasswordInteractive,
33
+ listPasswords
34
+ } = require('../lib/shell-manager');
17
35
  const { initI18n, forceReinitI18n } = require('../lib/i18n');
18
36
  const packageJson = require('../../package.json');
19
37
 
@@ -170,6 +188,105 @@ if (args.length > 0) {
170
188
  return;
171
189
  }
172
190
 
191
+ // Shell 连接:q sh [name] / add / rename / remove,配置来自 ~/.quick-sh/config.json 的 shells
192
+ if (firstArg === 'sh') {
193
+ const sub = args[1];
194
+ const rest = args.slice(2);
195
+ const run = (p) => p.catch(error => {
196
+ console.error(`❌ ${error.message}`);
197
+ process.exit(1);
198
+ });
199
+ if (sub === '-h' || sub === '--help' || sub === 'help') {
200
+ showShellHelp();
201
+ return;
202
+ }
203
+ if (sub === 'add') {
204
+ const opts = {};
205
+ for (let i = 0; i < rest.length; i += 2) {
206
+ if (rest[i] && rest[i].startsWith('--') && rest[i + 1] != null) {
207
+ opts[rest[i].slice(2)] = rest[i + 1];
208
+ }
209
+ }
210
+ if (opts.name && opts.host) {
211
+ run(addShellWithPassword({
212
+ name: opts.name,
213
+ host: opts.host,
214
+ user: opts.user,
215
+ port: opts.port,
216
+ password: opts.password
217
+ }));
218
+ } else {
219
+ run(addShellInteractive());
220
+ }
221
+ return;
222
+ }
223
+ if (sub === 'edit') {
224
+ const editRest = rest.slice(0);
225
+ const name = editRest[0] && !editRest[0].startsWith('--') ? editRest.shift() : null;
226
+ const editOpts = {};
227
+ for (let i = 0; i < editRest.length; i += 2) {
228
+ if (editRest[i] && editRest[i].startsWith('--') && editRest[i + 1] != null) {
229
+ editOpts[editRest[i].slice(2)] = editRest[i + 1];
230
+ }
231
+ }
232
+ const hasOpts = Object.keys(editOpts).length > 0;
233
+ if (hasOpts && name) {
234
+ run(editShell(name, editOpts));
235
+ } else {
236
+ run(editShellInteractive(name || undefined));
237
+ }
238
+ return;
239
+ }
240
+ if (sub === 'rename') {
241
+ if (rest.length >= 2) {
242
+ run(renameShell(rest[0], rest[1]));
243
+ } else {
244
+ run(renameShellInteractive());
245
+ }
246
+ return;
247
+ }
248
+ if (sub === 'remove' || sub === 'rm') {
249
+ if (rest.length >= 1) {
250
+ run(removeShell(rest[0]));
251
+ } else {
252
+ run(removeShellInteractive());
253
+ }
254
+ return;
255
+ }
256
+ if (sub === 'password') {
257
+ const pwdSub = rest[0];
258
+ if (pwdSub === 'set') {
259
+ const name = rest[1];
260
+ if (name) {
261
+ run(setPasswordInteractive(name));
262
+ } else {
263
+ console.error('❌ q sh password set <name>');
264
+ process.exit(1);
265
+ }
266
+ } else if (pwdSub === 'remove' || pwdSub === 'rm') {
267
+ const name = rest[1];
268
+ if (name) {
269
+ run(removeShellPassword(name).then(() => console.log(require('../lib/i18n').t('shell.passwordRemoved', { name }))));
270
+ } else {
271
+ console.error('❌ q sh password remove <name>');
272
+ process.exit(1);
273
+ }
274
+ } else if (pwdSub === 'list' || pwdSub === 'ls') {
275
+ run(listPasswords());
276
+ } else {
277
+ console.error('❌ q sh password set | remove | list');
278
+ process.exit(1);
279
+ }
280
+ return;
281
+ }
282
+ if (sub) {
283
+ run(connectShell(sub));
284
+ } else {
285
+ listShells();
286
+ }
287
+ return;
288
+ }
289
+
173
290
  // 对于其他命令,直接执行脚本,避免 commander.js 解析参数
174
291
  if (firstArg && !firstArg.startsWith('-')) {
175
292
  executeScript(firstArg, args.slice(1));
package/src/lib/help.js CHANGED
@@ -11,6 +11,7 @@ ${t('help.usage')}
11
11
  q -path <dir> ${t('help.setDirectory')}
12
12
  q -lang [code] ${t('help.setLanguage')}
13
13
  q -ai [-config/-use] ${t('help.aiChat')}
14
+ q sh [name] ${t('help.shellConnect')}
14
15
 
15
16
  ${t('help.localScripts')}
16
17
  q hello ${t('help.runScript', { script: 'hello.js or hello.sh' })}
@@ -438,7 +438,8 @@ function showBrief() {
438
438
  console.log(` q <script> ${t('help.executeScript')}`);
439
439
  console.log(` q -l ${t('help.listScripts')}`);
440
440
  console.log(` q -h, --help ${t('help.showHelp')}`);
441
- console.log(` q -path <dir> ${t('help.setDirectory')}\n`);
441
+ console.log(` q -path <dir> ${t('help.setDirectory')}`);
442
+ console.log(` q sh [name] ${t('help.shellConnect')}\n`);
442
443
 
443
444
  console.log(t('help.examples'));
444
445
  console.log(` q hello ${t('help.runScript', { script: 'hello' })}`);
@@ -0,0 +1,492 @@
1
+ const { spawn, spawnSync } = require('child_process');
2
+ const readline = require('readline');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const fs = require('fs-extra');
6
+ const { readConfig, writeConfig } = require('./config');
7
+ const { t } = require('./i18n');
8
+
9
+ const PASSWORDS_FILE = path.join(os.homedir(), '.quick-sh', 'shell-passwords.json');
10
+ const CONFIG_DIR = path.join(os.homedir(), '.quick-sh');
11
+
12
+ /** Shell 配置结构(存于 ~/.quick-sh/config.json 的 shells 字段):
13
+ * "shells": {
14
+ * "server1": { "host": "192.168.1.1", "user": "root", "port": 22 },
15
+ * "dev": { "host": "dev.example.com", "user": "deploy" }
16
+ * }
17
+ * port 可选,默认 22。
18
+ */
19
+
20
+ async function getShellsConfig() {
21
+ const config = await readConfig();
22
+ return config.shells || {};
23
+ }
24
+
25
+ async function getFullConfig() {
26
+ const config = await readConfig();
27
+ if (!config.shells || typeof config.shells !== 'object') config.shells = {};
28
+ return config;
29
+ }
30
+
31
+ /** 密码单独存于 ~/.quick-sh/shell-passwords.json,格式 { "name": "password" } */
32
+ async function readShellPasswords() {
33
+ try {
34
+ if (await fs.pathExists(PASSWORDS_FILE)) {
35
+ const data = await fs.readJson(PASSWORDS_FILE);
36
+ return typeof data === 'object' && data !== null ? data : {};
37
+ }
38
+ } catch (e) {
39
+ // ignore
40
+ }
41
+ return {};
42
+ }
43
+
44
+ async function getShellPassword(name) {
45
+ const passwords = await readShellPasswords();
46
+ return passwords[name] != null ? String(passwords[name]) : null;
47
+ }
48
+
49
+ async function writeShellPasswords(passwords) {
50
+ await fs.ensureDir(CONFIG_DIR);
51
+ await fs.writeJson(PASSWORDS_FILE, passwords, { spaces: 2 });
52
+ try {
53
+ await fs.chmod(PASSWORDS_FILE, 0o600);
54
+ } catch (e) {
55
+ // ignore on Windows
56
+ }
57
+ }
58
+
59
+ async function setShellPassword(name, password) {
60
+ const passwords = await readShellPasswords();
61
+ passwords[name] = password;
62
+ await writeShellPasswords(passwords);
63
+ }
64
+
65
+ async function removeShellPassword(name) {
66
+ const passwords = await readShellPasswords();
67
+ delete passwords[name];
68
+ await writeShellPasswords(passwords);
69
+ }
70
+
71
+ function hasSshpass() {
72
+ const r = spawnSync('which', ['sshpass'], { encoding: 'utf8' });
73
+ return r.status === 0 && r.stdout && r.stdout.trim().length > 0;
74
+ }
75
+
76
+ async function connectShell(name) {
77
+ const shells = await getShellsConfig();
78
+ const one = shells[name];
79
+
80
+ if (!one) {
81
+ console.error(t('shell.notFound', { name }));
82
+ if (Object.keys(shells).length > 0) {
83
+ console.error(t('shell.available'));
84
+ Object.keys(shells).forEach(k => console.error(` • ${k}`));
85
+ }
86
+ process.exit(1);
87
+ }
88
+
89
+ const host = one.host;
90
+ if (!host) {
91
+ console.error(t('shell.noHost', { name }));
92
+ process.exit(1);
93
+ }
94
+
95
+ const user = one.user || null;
96
+ const port = one.port != null ? Number(one.port) : 22;
97
+ const target = user ? `${user}@${host}` : host;
98
+ const sshArgs = ['-p', String(port), target];
99
+
100
+ const argv = process.argv.slice(2);
101
+ const shIndex = argv.indexOf('sh');
102
+ if (shIndex !== -1 && argv.length > shIndex + 2) {
103
+ sshArgs.push(...argv.slice(shIndex + 2));
104
+ }
105
+
106
+ const password = await getShellPassword(name);
107
+ const useSshpass = password && hasSshpass();
108
+ if (password && !useSshpass) {
109
+ console.log('');
110
+ console.log('💡 ' + t('shell.passwordSavedButNoSshpass'));
111
+ console.log('');
112
+ }
113
+ const spawnArgs = useSshpass
114
+ ? ['-p', password, 'ssh', ...sshArgs]
115
+ : sshArgs;
116
+ const spawnCmd = useSshpass ? 'sshpass' : 'ssh';
117
+
118
+ const child = spawn(spawnCmd, spawnArgs, {
119
+ stdio: 'inherit',
120
+ shell: false
121
+ });
122
+
123
+ child.on('error', err => {
124
+ console.error(t('shell.sshError', { error: err.message }));
125
+ process.exit(1);
126
+ });
127
+
128
+ child.on('exit', (code, signal) => {
129
+ if (signal) {
130
+ process.kill(process.pid, signal);
131
+ } else {
132
+ process.exit(code != null ? code : 1);
133
+ }
134
+ });
135
+ }
136
+
137
+ async function setPasswordInteractive(name) {
138
+ const shells = await getShellsConfig();
139
+ if (!shells[name]) {
140
+ throw new Error(t('shell.notFound', { name }));
141
+ }
142
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
143
+ if (process.stdin.isTTY && process.platform !== 'win32') {
144
+ try {
145
+ spawnSync('stty', ['-echo'], { stdio: 'inherit' });
146
+ } catch (e) {
147
+ // ignore
148
+ }
149
+ }
150
+ const password = await new Promise(resolve => {
151
+ rl.question(t('shell.promptPassword'), ans => resolve(ans || ''));
152
+ });
153
+ if (process.stdin.isTTY && process.platform !== 'win32') {
154
+ try {
155
+ spawnSync('stty', ['echo'], { stdio: 'inherit' });
156
+ } catch (e) {
157
+ // ignore
158
+ }
159
+ }
160
+ rl.close();
161
+ if (!password) {
162
+ throw new Error(t('shell.passwordEmpty'));
163
+ }
164
+ await setShellPassword(name, password);
165
+ console.log(t('shell.passwordSet', { name }));
166
+ }
167
+
168
+ async function listPasswords() {
169
+ const passwords = await readShellPasswords();
170
+ const names = Object.keys(passwords);
171
+ if (names.length === 0) {
172
+ console.log(t('shell.passwordListEmpty'));
173
+ return;
174
+ }
175
+ console.log(t('shell.passwordListTitle'));
176
+ names.forEach(n => console.log(` • ${n}`));
177
+ }
178
+
179
+ function showShellHelp() {
180
+ console.log(t('shell.helpTitle'));
181
+ console.log('');
182
+ console.log(t('shell.helpUsage'));
183
+ console.log(` q sh ${t('shell.helpList')}`);
184
+ console.log(` q sh <name> ${t('shell.helpConnect')}`);
185
+ console.log(` q sh add ${t('shell.helpAdd')}`);
186
+ console.log(` q sh edit [name] ${t('shell.helpEdit')}`);
187
+ console.log(` q sh rename ${t('shell.helpRename')}`);
188
+ console.log(` q sh remove <name> ${t('shell.helpRemove')}`);
189
+ console.log(` q sh password set <name> ${t('shell.helpPasswordSet')}`);
190
+ console.log(` q sh password remove <name> ${t('shell.helpPasswordRemove')}`);
191
+ console.log(` q sh password list ${t('shell.helpPasswordList')}`);
192
+ console.log('');
193
+ console.log(t('shell.helpConfig'));
194
+ console.log(t('shell.helpExample'));
195
+ }
196
+
197
+ /** 新增连接。opts: { name, host, user?, port? } */
198
+ async function addShell(opts) {
199
+ const { name, host, user, port } = opts;
200
+ if (!name || !host) {
201
+ throw new Error(t('shell.addNameHostRequired'));
202
+ }
203
+ const config = await getFullConfig();
204
+ if (config.shells[name]) {
205
+ throw new Error(t('shell.addNameExists', { name }));
206
+ }
207
+ config.shells[name] = {
208
+ host,
209
+ ...(user != null && user !== '' && { user }),
210
+ ...(port != null && port !== '' && { port: Number(port) || 22 })
211
+ };
212
+ await writeConfig(config);
213
+ console.log(t('shell.addSuccess', { name }));
214
+ }
215
+
216
+ /** 新增连接并可选保存密码。opts 含 name, host, user?, port?, password? */
217
+ async function addShellWithPassword(opts) {
218
+ const { password, ...addOpts } = opts;
219
+ await addShell(addOpts);
220
+ if (password != null && password !== '') {
221
+ await setShellPassword(opts.name, password);
222
+ }
223
+ }
224
+
225
+ /** 编辑连接,新值直接覆盖旧配置;可只传部分字段与当前合并。opts: { host?, user?, port?, password? } */
226
+ async function editShell(name, opts) {
227
+ if (!name) {
228
+ throw new Error(t('shell.addNameHostRequired'));
229
+ }
230
+ const config = await getFullConfig();
231
+ const current = config.shells[name];
232
+ if (!current) {
233
+ throw new Error(t('shell.notFound', { name }));
234
+ }
235
+ const host = opts.host != null && opts.host !== '' ? opts.host : current.host;
236
+ const user = opts.user !== undefined ? opts.user : current.user;
237
+ const port = opts.port !== undefined && opts.port !== '' ? (Number(opts.port) || 22) : (current.port != null ? current.port : 22);
238
+ if (!host) {
239
+ throw new Error(t('shell.noHost', { name }));
240
+ }
241
+ config.shells[name] = {
242
+ host,
243
+ ...(user != null && user !== '' && { user }),
244
+ ...(port != null && { port })
245
+ };
246
+ await writeConfig(config);
247
+ if (opts.password !== undefined) {
248
+ if (opts.password === '' || opts.password == null) {
249
+ await removeShellPassword(name);
250
+ } else {
251
+ await setShellPassword(name, opts.password);
252
+ }
253
+ }
254
+ console.log(t('shell.editSuccess', { name }));
255
+ }
256
+
257
+ /** 交互式编辑:可指定 name 或之后选择 */
258
+ async function editShellInteractive(name) {
259
+ const shells = await getShellsConfig();
260
+ const names = Object.keys(shells);
261
+ if (names.length === 0) {
262
+ console.log(t('shell.noShells'));
263
+ return;
264
+ }
265
+ let target = name;
266
+ if (!target) {
267
+ console.log(t('shell.listTitle'));
268
+ names.forEach((n, i) => {
269
+ const c = shells[n];
270
+ console.log(` ${i + 1}. ${n.padEnd(12)} ${(c.user || '') + (c.user ? '@' : '')}${c.host || '?'}`);
271
+ });
272
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
273
+ target = await ask(rl, t('shell.promptEditWhich'));
274
+ rl.close();
275
+ if (!target) return;
276
+ target = names.includes(target) ? target : names[Number(target) - 1];
277
+ if (!target || !shells[target]) {
278
+ throw new Error(t('shell.notFound', { name: target }));
279
+ }
280
+ } else if (!shells[target]) {
281
+ throw new Error(t('shell.notFound', { name: target }));
282
+ }
283
+ const current = shells[target];
284
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
285
+ const host = await ask(rl, t('shell.promptHost'), current.host || '');
286
+ const user = await ask(rl, t('shell.promptUser'), current.user != null ? String(current.user) : 'root');
287
+ const port = await ask(rl, t('shell.promptPort'), current.port != null ? String(current.port) : '22');
288
+ const password = await askPasswordOptional(rl);
289
+ rl.close();
290
+ if (!host) throw new Error(t('shell.addNameHostRequired'));
291
+ await editShell(target, {
292
+ host,
293
+ user: user || undefined,
294
+ port: port || '22',
295
+ ...(password !== '' && { password })
296
+ });
297
+ }
298
+
299
+ /** 重命名连接 */
300
+ async function renameShell(oldName, newName) {
301
+ if (!oldName || !newName) {
302
+ throw new Error(t('shell.renameArgsRequired'));
303
+ }
304
+ const config = await getFullConfig();
305
+ if (!config.shells[oldName]) {
306
+ throw new Error(t('shell.notFound', { name: oldName }));
307
+ }
308
+ if (config.shells[newName]) {
309
+ throw new Error(t('shell.addNameExists', { name: newName }));
310
+ }
311
+ config.shells[newName] = config.shells[oldName];
312
+ delete config.shells[oldName];
313
+ await writeConfig(config);
314
+ console.log(t('shell.renameSuccess', { oldName, newName }));
315
+ }
316
+
317
+ /** 删除连接 */
318
+ async function removeShell(name) {
319
+ if (!name) {
320
+ throw new Error(t('shell.removeNameRequired'));
321
+ }
322
+ const config = await getFullConfig();
323
+ if (!config.shells[name]) {
324
+ throw new Error(t('shell.notFound', { name }));
325
+ }
326
+ delete config.shells[name];
327
+ await writeConfig(config);
328
+ console.log(t('shell.removeSuccess', { name }));
329
+ }
330
+
331
+ function ask(rl, question, defaultValue = '') {
332
+ const def = defaultValue ? ` [${defaultValue}]` : '';
333
+ return new Promise(resolve => {
334
+ rl.question(`${question}${def}: `, ans => resolve(ans.trim() || defaultValue));
335
+ });
336
+ }
337
+
338
+ /** 交互式询问可选密码(输入不显示,直接回车则跳过) */
339
+ function askPasswordOptional(rl) {
340
+ if (process.stdin.isTTY && process.platform !== 'win32') {
341
+ try {
342
+ spawnSync('stty', ['-echo'], { stdio: 'inherit' });
343
+ } catch (e) {
344
+ // ignore
345
+ }
346
+ }
347
+ return new Promise(resolve => {
348
+ rl.question(t('shell.promptPasswordOptional'), ans => {
349
+ if (process.stdin.isTTY && process.platform !== 'win32') {
350
+ try {
351
+ spawnSync('stty', ['echo'], { stdio: 'inherit' });
352
+ } catch (e) {
353
+ // ignore
354
+ }
355
+ }
356
+ resolve((ans || '').trim());
357
+ });
358
+ });
359
+ }
360
+
361
+ /** 交互式新增 */
362
+ async function addShellInteractive() {
363
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
364
+ const name = await ask(rl, t('shell.promptName'));
365
+ if (!name) {
366
+ rl.close();
367
+ throw new Error(t('shell.addNameHostRequired'));
368
+ }
369
+ const shells = await getShellsConfig();
370
+ if (shells[name]) {
371
+ rl.close();
372
+ throw new Error(t('shell.addNameExists', { name }));
373
+ }
374
+ const host = await ask(rl, t('shell.promptHost'));
375
+ if (!host) {
376
+ rl.close();
377
+ throw new Error(t('shell.addNameHostRequired'));
378
+ }
379
+ const user = await ask(rl, t('shell.promptUser'), 'root');
380
+ const port = await ask(rl, t('shell.promptPort'), '22');
381
+ const password = await askPasswordOptional(rl);
382
+ rl.close();
383
+ await addShell({ name, host, user: user || undefined, port: port || '22' });
384
+ if (password !== '') {
385
+ await setShellPassword(name, password);
386
+ console.log(t('shell.passwordSet', { name }));
387
+ }
388
+ }
389
+
390
+ /** 交互式重命名 */
391
+ async function renameShellInteractive() {
392
+ const shells = await getShellsConfig();
393
+ const names = Object.keys(shells);
394
+ if (names.length === 0) {
395
+ console.log(t('shell.noShells'));
396
+ return;
397
+ }
398
+ console.log(t('shell.listTitle'));
399
+ names.forEach((n, i) => {
400
+ const c = shells[n];
401
+ console.log(` ${i + 1}. ${n.padEnd(12)} ${(c.user || '') + (c.user ? '@' : '')}${c.host || '?'}`);
402
+ });
403
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
404
+ const oldName = await ask(rl, t('shell.promptRenameOld'));
405
+ if (!oldName) {
406
+ rl.close();
407
+ return;
408
+ }
409
+ const resolved = names.includes(oldName) ? oldName : names[Number(oldName) - 1];
410
+ if (!resolved || !shells[resolved]) {
411
+ rl.close();
412
+ throw new Error(t('shell.notFound', { name: oldName }));
413
+ }
414
+ const newName = await ask(rl, t('shell.promptRenameNew'));
415
+ rl.close();
416
+ if (!newName) throw new Error(t('shell.renameArgsRequired'));
417
+ await renameShell(resolved, newName);
418
+ }
419
+
420
+ /** 交互式删除 */
421
+ async function removeShellInteractive() {
422
+ const shells = await getShellsConfig();
423
+ const names = Object.keys(shells);
424
+ if (names.length === 0) {
425
+ console.log(t('shell.noShells'));
426
+ return;
427
+ }
428
+ console.log(t('shell.listTitle'));
429
+ names.forEach((n, i) => {
430
+ const c = shells[n];
431
+ console.log(` ${i + 1}. ${n.padEnd(12)} ${(c.user || '') + (c.user ? '@' : '')}${c.host || '?'}`);
432
+ });
433
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
434
+ const name = await ask(rl, t('shell.promptRemove'));
435
+ if (!name) {
436
+ rl.close();
437
+ return;
438
+ }
439
+ const resolved = names.includes(name) ? name : names[Number(name) - 1];
440
+ if (!resolved || !shells[resolved]) {
441
+ rl.close();
442
+ throw new Error(t('shell.notFound', { name }));
443
+ }
444
+ const confirm = await ask(rl, t('shell.promptRemoveConfirm', { name: resolved }), 'n');
445
+ rl.close();
446
+ if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') {
447
+ await removeShell(resolved);
448
+ } else {
449
+ console.log(t('shell.removeCancelled'));
450
+ }
451
+ }
452
+
453
+ function listShells() {
454
+ return getShellsConfig().then(shells => {
455
+ const names = Object.keys(shells);
456
+ if (names.length === 0) {
457
+ console.log(t('shell.noShells'));
458
+ console.log(t('shell.configHint'));
459
+ console.log(t('shell.helpHint'));
460
+ return;
461
+ }
462
+ console.log(t('shell.listTitle'));
463
+ names.forEach(name => {
464
+ const c = shells[name];
465
+ const user = c.user ? `${c.user}@` : '';
466
+ const port = c.port != null ? `:${c.port}` : '';
467
+ console.log(` • ${name.padEnd(12)} ${user}${c.host || '?'}${port}`);
468
+ });
469
+ console.log(t('shell.usageHint'));
470
+ });
471
+ }
472
+
473
+ module.exports = {
474
+ getShellsConfig,
475
+ connectShell,
476
+ listShells,
477
+ showShellHelp,
478
+ addShell,
479
+ addShellWithPassword,
480
+ editShell,
481
+ renameShell,
482
+ removeShell,
483
+ addShellInteractive,
484
+ editShellInteractive,
485
+ renameShellInteractive,
486
+ removeShellInteractive,
487
+ setShellPassword,
488
+ removeShellPassword,
489
+ setPasswordInteractive,
490
+ listPasswords,
491
+ getShellPassword
492
+ };
@@ -26,7 +26,8 @@
26
26
  "showLanguageExample": "Show current language settings",
27
27
  "aiChat": "AI chat with configuration",
28
28
  "configureAI": "Configure AI models",
29
- "useAIModel": "Use specific AI model for chat"
29
+ "useAIModel": "Use specific AI model for chat",
30
+ "shellConnect": "SSH/shell connection (config in ~/.quick-sh/config.json shells)"
30
31
  },
31
32
  "commands": {
32
33
  "pathSet": "Script path set to: {{path}}",
@@ -249,5 +250,55 @@
249
250
  "conversationHistory": "Conversation History",
250
251
  "noHistory": "No conversation history yet",
251
252
  "historyCleared": "Conversation history cleared"
253
+ },
254
+ "shell": {
255
+ "notFound": "Shell \"{{name}}\" not found in config.",
256
+ "noHost": "Shell \"{{name}}\" has no host.",
257
+ "available": "Available shell names:",
258
+ "noShells": "No shell connections configured.",
259
+ "configHint": "Add \"shells\" in ~/.quick-sh/config.json. Example: {\"shells\":{\"server1\":{\"host\":\"192.168.1.1\",\"user\":\"root\",\"port\":22}}}",
260
+ "helpHint": "Use q sh -h for help",
261
+ "listTitle": "Shell connections (from ~/.quick-sh/config.json):",
262
+ "usageHint": "Usage: q sh <name>",
263
+ "sshError": "SSH error: {{error}}",
264
+ "passwordSavedButNoSshpass": "Password is saved for this connection but sshpass was not found; you will be prompted. Install: brew install sshpass",
265
+ "helpTitle": "q sh - SSH/shell connections (config: shells in ~/.quick-sh/config.json)",
266
+ "helpUsage": "Usage:",
267
+ "helpList": "List configured connections",
268
+ "helpConnect": "Connect by name (SSH)",
269
+ "helpConfig": "Config: add a shells object in ~/.quick-sh/config.json, e.g. {\"server1\":{\"host\":\"host\",\"user\":\"root\",\"port\":22}}.",
270
+ "helpExample": "Examples: q sh, q sh server1",
271
+ "helpAdd": "Add connection (interactive or --name x --host h [--user] [--port] [--password p])",
272
+ "helpEdit": "Edit connection (interactive or edit <name> [--host] [--user] [--port] [--password])",
273
+ "helpRename": "Rename connection (interactive or rename <old> <new>)",
274
+ "helpRemove": "Remove connection (interactive or remove <name>)",
275
+ "addNameHostRequired": "Provide name and host (or use interactive: q sh add)",
276
+ "addNameExists": "Connection name \"{{name}}\" already exists",
277
+ "addSuccess": "Added connection: {{name}}",
278
+ "editSuccess": "Updated connection: {{name}}",
279
+ "renameArgsRequired": "Provide old and new name, or use interactive: q sh rename",
280
+ "renameSuccess": "Renamed: {{oldName}} → {{newName}}",
281
+ "removeNameRequired": "Provide name to remove, or use interactive: q sh remove",
282
+ "removeSuccess": "Removed connection: {{name}}",
283
+ "removeCancelled": "Remove cancelled",
284
+ "promptName": "Connection name",
285
+ "promptHost": "Host",
286
+ "promptUser": "User",
287
+ "promptPort": "Port",
288
+ "promptEditWhich": "Connection to edit (name or index)",
289
+ "promptRenameOld": "Connection to rename (name or index)",
290
+ "promptRenameNew": "New name",
291
+ "promptRemove": "Connection to remove (name or index)",
292
+ "promptRemoveConfirm": "Remove \"{{name}}\"? (y/N)",
293
+ "promptPassword": "Password (saved to ~/.quick-sh/shell-passwords.json): ",
294
+ "promptPasswordOptional": "Password (optional, press Enter to skip): ",
295
+ "passwordEmpty": "Password cannot be empty",
296
+ "passwordSet": "Password saved for connection \"{{name}}\"",
297
+ "passwordRemoved": "Password removed for connection \"{{name}}\"",
298
+ "passwordListEmpty": "No passwords saved for any connection",
299
+ "passwordListTitle": "Connections with saved password:",
300
+ "helpPasswordSet": "Set password for connection (interactive; used with sshpass when connecting)",
301
+ "helpPasswordRemove": "Remove saved password for connection",
302
+ "helpPasswordList": "List connection names that have a saved password"
252
303
  }
253
304
  }
@@ -26,7 +26,8 @@
26
26
  "showLanguageExample": "現在の言語設定を表示",
27
27
  "aiChat": "AI チャット設定",
28
28
  "configureAI": "AI モデルを設定",
29
- "useAIModel": "指定の AI モデルでチャット"
29
+ "useAIModel": "指定の AI モデルでチャット",
30
+ "shellConnect": "Shell/SSH 接続(~/.quick-sh/config.json の shells で設定)"
30
31
  },
31
32
  "commands": {
32
33
  "pathSet": "スクリプトパスが設定されました: {{path}}",
@@ -249,5 +250,55 @@
249
250
  "conversationHistory": "会話履歴",
250
251
  "noHistory": "会話履歴はまだありません",
251
252
  "historyCleared": "会話履歴をクリアしました"
253
+ },
254
+ "shell": {
255
+ "notFound": "設定にシェル \"{{name}}\" が見つかりません。",
256
+ "noHost": "シェル \"{{name}}\" に host が設定されていません。",
257
+ "available": "利用可能なシェル名:",
258
+ "noShells": "シェル接続が設定されていません。",
259
+ "configHint": "~/.quick-sh/config.json に \"shells\" を追加してください。例: {\"shells\":{\"server1\":{\"host\":\"192.168.1.1\",\"user\":\"root\",\"port\":22}}}",
260
+ "helpHint": "q sh -h でヘルプを表示",
261
+ "listTitle": "シェル接続 (~/.quick-sh/config.json より):",
262
+ "usageHint": "使い方: q sh <名前>",
263
+ "sshError": "SSH エラー: {{error}}",
264
+ "passwordSavedButNoSshpass": "この接続のパスワードは保存済みですが sshpass が見つかりません。手入力になります。インストール: brew install sshpass",
265
+ "helpTitle": "q sh - SSH/シェル接続(設定: ~/.quick-sh/config.json の shells)",
266
+ "helpUsage": "使い方:",
267
+ "helpList": "設定済み接続を一覧表示",
268
+ "helpConnect": "名前で接続(SSH)",
269
+ "helpConfig": "設定: ~/.quick-sh/config.json に shells を追加。例: {\"server1\":{\"host\":\"host\",\"user\":\"root\",\"port\":22}}",
270
+ "helpExample": "例: q sh、q sh server1",
271
+ "helpAdd": "接続を追加(対話式または --name x --host h [--user] [--port] [--password p])",
272
+ "helpEdit": "接続を編集(対話式または edit <name> [--host] [--user] [--port] [--password])",
273
+ "helpRename": "接続名を変更(対話式または rename <旧> <新>)",
274
+ "helpRemove": "接続を削除(対話式または remove <名前>)",
275
+ "addNameHostRequired": "name と host を指定するか、対話式で q sh add を実行",
276
+ "addNameExists": "接続名 \"{{name}}\" は既に存在します",
277
+ "addSuccess": "接続を追加しました: {{name}}",
278
+ "editSuccess": "接続を更新しました: {{name}}",
279
+ "renameArgsRequired": "旧名と新名を指定するか、対話式で q sh rename を実行",
280
+ "renameSuccess": "名前を変更しました: {{oldName}} → {{newName}}",
281
+ "removeNameRequired": "削除する名前を指定するか、対話式で q sh remove を実行",
282
+ "removeSuccess": "接続を削除しました: {{name}}",
283
+ "removeCancelled": "削除をキャンセルしました",
284
+ "promptName": "接続名",
285
+ "promptHost": "ホスト",
286
+ "promptUser": "ユーザー",
287
+ "promptPort": "ポート",
288
+ "promptEditWhich": "編集する接続(名前または番号)",
289
+ "promptRenameOld": "名前を変更する接続(名前または番号)",
290
+ "promptRenameNew": "新しい名前",
291
+ "promptRemove": "削除する接続(名前または番号)",
292
+ "promptRemoveConfirm": "\"{{name}}\" を削除しますか? (y/N)",
293
+ "promptPassword": "パスワード(~/.quick-sh/shell-passwords.json に保存): ",
294
+ "promptPasswordOptional": "パスワード(省略可、Enter でスキップ): ",
295
+ "passwordEmpty": "パスワードを入力してください",
296
+ "passwordSet": "接続 \"{{name}}\" のパスワードを保存しました",
297
+ "passwordRemoved": "接続 \"{{name}}\" のパスワードを削除しました",
298
+ "passwordListEmpty": "保存されたパスワードはありません",
299
+ "passwordListTitle": "パスワードを保存している接続:",
300
+ "helpPasswordSet": "接続のパスワードを設定(対話入力。sshpass で接続時に使用)",
301
+ "helpPasswordRemove": "保存したパスワードを削除",
302
+ "helpPasswordList": "パスワードを保存している接続名を一覧表示"
252
303
  }
253
304
  }
@@ -26,7 +26,8 @@
26
26
  "showLanguageExample": "显示当前语言设置",
27
27
  "aiChat": "AI聊天配置",
28
28
  "configureAI": "配置AI模型",
29
- "useAIModel": "使用指定AI模型聊天"
29
+ "useAIModel": "使用指定AI模型聊天",
30
+ "shellConnect": "Shell/SSH 连接(配置在 ~/.quick-sh/config.json 的 shells)"
30
31
  },
31
32
  "commands": {
32
33
  "pathSet": "脚本路径已设置为: {{path}}",
@@ -53,6 +54,56 @@
53
54
  "supportedLanguages": "支持的语言",
54
55
  "current": "当前"
55
56
  },
57
+ "shell": {
58
+ "notFound": "未找到连接 \"{{name}}\",请检查 ~/.quick-sh/config.json 中的 shells 配置。",
59
+ "noHost": "连接 \"{{name}}\" 未配置 host。",
60
+ "available": "可用的连接名:",
61
+ "noShells": "未配置任何 shell 连接。",
62
+ "configHint": "请在 ~/.quick-sh/config.json 中添加 shells。示例: {\"shells\":{\"server1\":{\"host\":\"192.168.1.1\",\"user\":\"root\",\"port\":22}}}",
63
+ "helpHint": "使用 q sh -h 查看帮助说明",
64
+ "listTitle": "Shell 连接 (来自 ~/.quick-sh/config.json):",
65
+ "usageHint": "用法: q sh <名称>",
66
+ "sshError": "SSH 错误: {{error}}",
67
+ "passwordSavedButNoSshpass": "已保存该连接的密码,但未检测到 sshpass,将改为手动输入。安装: brew install sshpass",
68
+ "helpTitle": "q sh - SSH/Shell 连接(配置在 ~/.quick-sh/config.json 的 shells)",
69
+ "helpUsage": "用法:",
70
+ "helpList": "列出已配置的连接",
71
+ "helpConnect": "按名称连接对应主机(SSH)",
72
+ "helpConfig": "配置:在 ~/.quick-sh/config.json 中添加 shells 对象,如 {\"server1\":{\"host\":\"host\",\"user\":\"root\",\"port\":22}}。",
73
+ "helpExample": "示例:q sh、q sh server1",
74
+ "helpAdd": "新增连接(可交互或 --name x --host h [--user] [--port] [--password p])",
75
+ "helpEdit": "编辑连接(可交互或 edit <name> [--host] [--user] [--port] [--password])",
76
+ "helpRename": "重命名连接(可交互或 rename <旧名> <新名>)",
77
+ "helpRemove": "删除连接(可交互或 remove <名称>)",
78
+ "addNameHostRequired": "请提供 name 和 host(或使用交互式 q sh add)",
79
+ "addNameExists": "连接名称 \"{{name}}\" 已存在",
80
+ "addSuccess": "已添加连接: {{name}}",
81
+ "editSuccess": "已更新连接: {{name}}",
82
+ "renameArgsRequired": "请提供旧名称和新名称,或使用交互式 q sh rename",
83
+ "renameSuccess": "已重命名: {{oldName}} → {{newName}}",
84
+ "removeNameRequired": "请提供要删除的名称,或使用交互式 q sh remove",
85
+ "removeSuccess": "已删除连接: {{name}}",
86
+ "removeCancelled": "已取消删除",
87
+ "promptName": "连接名称",
88
+ "promptHost": "主机 (host)",
89
+ "promptUser": "用户名",
90
+ "promptPort": "端口",
91
+ "promptEditWhich": "要编辑的连接(名称或序号)",
92
+ "promptRenameOld": "要重命名的连接(名称或序号)",
93
+ "promptRenameNew": "新名称",
94
+ "promptRemove": "要删除的连接(名称或序号)",
95
+ "promptRemoveConfirm": "确认删除 \"{{name}}\"? (y/N)",
96
+ "promptPassword": "密码(存于 ~/.quick-sh/shell-passwords.json): ",
97
+ "promptPasswordOptional": "密码(可选,直接回车跳过): ",
98
+ "passwordEmpty": "密码不能为空",
99
+ "passwordSet": "已保存连接 \"{{name}}\" 的密码",
100
+ "passwordRemoved": "已删除连接 \"{{name}}\" 的密码",
101
+ "passwordListEmpty": "未保存任何连接的密码",
102
+ "passwordListTitle": "已保存密码的连接:",
103
+ "helpPasswordSet": "为连接设置密码(交互输入,需 sshpass 连接时自动使用)",
104
+ "helpPasswordRemove": "删除该连接保存的密码",
105
+ "helpPasswordList": "列出已保存密码的连接名"
106
+ },
56
107
  "status": {
57
108
  "currentPath": "📁 当前脚本路径: {{path}}",
58
109
  "configuredAliases": "🔗 配置的别名:",
package/CHANGELOG.md DELETED
@@ -1,109 +0,0 @@
1
- # Changelog
2
-
3
- 所有重要的更改都会记录在此文件中。
4
-
5
- ## [1.0.8] - 2025-06-19
6
-
7
- ### 更新内容
8
-
9
- - chore: remove node_modules (392db02)
10
-
11
- ### 详细信息
12
- - **更新人**: Young6118
13
- - **更新时间**: 2025-06-19
14
- - **提交数量**: 1
15
- - **提交范围**: 392db02..392db02
16
-
17
- ---
18
-
19
-
20
- ## [1.0.7] - 2025-06-18
21
-
22
- ### 更新内容
23
-
24
- - fix: remove openai, use axios (e8ad16f)
25
-
26
- ### 详细信息
27
- - **更新人**: Young6118
28
- - **更新时间**: 2025-06-18
29
- - **提交数量**: 1
30
- - **提交范围**: e8ad16f..e8ad16f
31
-
32
- ---
33
-
34
-
35
- ## [1.0.6] - 2025-06-18
36
-
37
- ### 更新内容
38
-
39
- - 版本发布
40
-
41
- ### 详细信息
42
- - **更新人**: Young6118
43
- - **更新时间**: 2025-06-18
44
- - **提交数量**: 0
45
-
46
- ---
47
-
48
-
49
- ## [1.0.5] - 2025-06-18
50
-
51
- ### 更新内容
52
-
53
- - refactor: src directory refactor, open ai downgrade to support node 14 (eeef017)
54
-
55
- ### 详细信息
56
- - **更新人**: Young6118
57
- - **更新时间**: 2025-06-18
58
- - **提交数量**: 1
59
- - **提交范围**: eeef017..eeef017
60
-
61
- ---
62
-
63
-
64
- ## [1.0.4] - 2025-06-18
65
-
66
- ### 更新内容
67
-
68
- - 版本发布
69
-
70
- ### 详细信息
71
- - **更新人**: Young6118
72
- - **更新时间**: 2025-06-18
73
- - **提交数量**: 0
74
-
75
- ---
76
-
77
-
78
- ## [1.0.3] - 2025-06-18
79
-
80
- ### 更新内容
81
-
82
- - fix: executor spawn set cwd (9e745e3)
83
- - feat: locales path fix (48586fb)
84
-
85
- ### 详细信息
86
- - **更新人**: Young6118
87
- - **更新时间**: 2025-06-18
88
- - **提交数量**: 2
89
- - **提交范围**: 48586fb..9e745e3
90
-
91
- ---
92
-
93
-
94
- ## [1.0.2] - 2025-06-18
95
-
96
- ### 更新内容
97
-
98
- - feat: 添加npm-version工具测试输入文件 (a849dc5)
99
- - feat: 修复npm-version工具目录切换问题 (56fdf17)
100
- - feat(examples): add npm version tool (4941a89)
101
-
102
- ### 详细信息
103
- - **更新人**: Young6118
104
- - **更新时间**: 2025-06-18
105
- - **提交数量**: 3
106
-
107
- ---
108
-
109
-