@seneca/repl
Advanced tools
Comparing version 5.1.0 to 6.0.0
#!/usr/bin/env node | ||
/* Copyright (c) 2019 voxgig and other contributors, MIT License */ | ||
/* Copyright (c) 2019-2023 voxgig and other contributors, MIT License */ | ||
'use strict' | ||
const Net = require('net') | ||
const OS = require('node:os') | ||
const FS = require('node:fs') | ||
const Path = require('node:path') | ||
const Net = require('node:net') | ||
const Readline = require('node:readline') | ||
const Http = require('node:http') | ||
const Https = require('node:https') | ||
const { Duplex } = require('node:stream') | ||
const Vorpal = require('@seneca/vorpal') | ||
const state = { | ||
connection: {}, | ||
} | ||
const vorpal = Vorpal({history:{ignore_mode:true}}) | ||
let host = '127.0.0.1' | ||
let port = 30303 | ||
const connection = {} | ||
let replAddr = process.argv[2] | ||
let portArg = process.argv[3] | ||
let url = 'telnet:' + host + ':' + port | ||
let scope = 'default' | ||
function repl(vorpal) { | ||
vorpal | ||
.mode('repl') | ||
.delimiter('-> ') | ||
.init(function(args, cb) { | ||
if(!connection.open) { | ||
this.log('No connection. Type `exit` and use `connect`.') | ||
} | ||
cb() | ||
}) | ||
.action(function(args, callback) { | ||
if(this.session && connection.remote_seneca) { | ||
this.session._modeDelimiter = connection.remote_seneca + '-> ' | ||
} | ||
// NOTE: backwards compatibility: seneca-repl localhost 30303 | ||
var cmd = args+'\n' | ||
if (null == replAddr) { | ||
replAddr = 'telnet://' + host + ':' + port | ||
} else if (null != portArg) { | ||
host = replAddr | ||
port = parseInt(portArg) | ||
replAddr = 'telnet://' + host + ':' + port | ||
} else { | ||
if (!replAddr.includes('://')) { | ||
replAddr = 'telnet://' + replAddr | ||
} | ||
} | ||
connection.sock.write(cmd) | ||
callback() | ||
}) | ||
// TODO: support other protocals - http endpoint, | ||
// lambda invoke (via sub plugin @seneca/repl-aws) | ||
try { | ||
url = new URL(replAddr) | ||
// console.log('URL', url) | ||
host = url.hostname || host | ||
port = '' === url.port ? port : parseInt(url.port) | ||
// NOTE: use URL params for additional args | ||
scope = url.searchParams.get('scope') | ||
scope = null == scope || '' === scope ? 'default' : scope | ||
} catch (e) { | ||
console.log('# CONNECTION URL ERROR: ', e.message, replAddr) | ||
process.exit(1) | ||
} | ||
function connect(vorpal) { | ||
vorpal | ||
.command('connect [host] [port]') | ||
.action(function(args, callback) { | ||
var host = args.host || 'localhost' | ||
var port = parseInt(args.port || 30303, 10) | ||
this.log('Connecting to '+host+':'+port+' ...') | ||
var log = this.log.bind(this) | ||
telnet({log:log, host:host, port:port},function(err) { | ||
if(err) { | ||
log(err) | ||
} | ||
else { | ||
vorpal.history('seneca~'+host+'~'+port) | ||
vorpal.exec('repl') | ||
} | ||
const history = [] | ||
const senecaFolder = Path.join(OS.homedir(), '.seneca') | ||
if (!FS.existsSync(senecaFolder)) { | ||
FS.mkdirSync(senecaFolder) | ||
} | ||
const historyName = encodeURIComponent(replAddr) | ||
const historyPath = Path.join(senecaFolder, 'repl-' + historyName + '.history') | ||
if (FS.existsSync(historyPath)) { | ||
const lines = FS.readFileSync(historyPath).toString() | ||
lines | ||
.split(/[\r\n]+/) | ||
.map((line) => (null != line && '' != line ? history.push(line) : null)) | ||
} | ||
let historyFile = null | ||
let spec = { | ||
log: console.log, | ||
url, | ||
host, | ||
port, | ||
scope, | ||
delay: 1111, | ||
first: true, | ||
} | ||
class RequestStream extends Duplex { | ||
constructor(spec, options) { | ||
super(options) | ||
this.spec = spec | ||
this.buffer = [] | ||
// console.log('HTTP CTOR') | ||
} | ||
_write(chunk, encoding, callback) { | ||
const cmd = chunk.toString().trim() | ||
// console.log('HTTP WRITE', cmd) | ||
// this.buffer.push('FOO'+String.fromCharCode(0)) | ||
// this._read() | ||
// return callback() | ||
const url = this.spec.url | ||
// Determine whether to use http or https based on the URL | ||
const httpClient = url.href.startsWith('https://') ? Https : Http | ||
const postData = JSON.stringify({ | ||
cmd, | ||
}) | ||
let req = httpClient | ||
.request( | ||
url.href, | ||
{ | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'Content-Length': Buffer.byteLength(postData), | ||
}, | ||
}, | ||
(response) => { | ||
let data = '' | ||
response.on('data', (chunk) => { | ||
data += chunk | ||
}) | ||
response.on('end', () => { | ||
let res = JSON.parse(data) | ||
// console.log('HE', data, res) | ||
this.buffer.push(res.out + String.fromCharCode(0)) | ||
this._read() | ||
callback() | ||
}) | ||
}, | ||
) | ||
.on('error', (err) => { | ||
// console.log('HE', err) | ||
this.buffer.push(`# ERROR: ${err}\n` + String.fromCharCode(0)) | ||
this._read() | ||
callback() | ||
}) | ||
}) | ||
req.write(postData) | ||
req.end() | ||
} | ||
_read(size) { | ||
// console.log('H READ') | ||
let chunk | ||
while ((chunk = this.buffer.shift())) { | ||
if (!this.push(chunk)) { | ||
break | ||
} | ||
} | ||
} | ||
} | ||
vorpal | ||
.delimiter('seneca: ') | ||
.use(repl) | ||
.use(connect) | ||
reconnect(spec) | ||
vorpal | ||
.show() | ||
if(2 < process.argv.length) { | ||
vorpal | ||
.exec(['connect'].concat(process.argv.slice(2)).join(' ')) | ||
function reconnect(spec) { | ||
operate(spec, function (result) { | ||
if (result) { | ||
if (false === result.connect && !spec.quit && !spec.first) { | ||
setTimeout(() => { | ||
spec.delay = Math.min(spec.delay * 1.1, 33333) | ||
reconnect(spec) | ||
}, spec.delay) | ||
} else if (result.err) { | ||
console.log('# CONNECTION ERROR:', result.err) | ||
} | ||
} else { | ||
console.log('# CONNECTION ERROR: no-result') | ||
process.exit(1) | ||
} | ||
}) | ||
} | ||
function operate(spec, done) { | ||
state.connection.first = true | ||
state.connection.quit = false | ||
// state.connection.sock = Net.connect(spec.port, spec.host) | ||
try { | ||
state.connection.sock = connect(spec) | ||
// console.log('SOCK', !!state.connection.sock) | ||
} catch (err) { | ||
// console.log('CA', err) | ||
return done({ err }) | ||
} | ||
state.connection.sock.on('connect', function () { | ||
// console.log('SOCK connect') | ||
state.connection.open = true | ||
delete state.connection.closed | ||
function telnet(spec, done) { | ||
connection.first = true | ||
connection.sock = Net.connect(spec.port, spec.host) | ||
try { | ||
historyFile = FS.openSync(historyPath, 'a') | ||
} catch (e) { | ||
// Don't save history | ||
} | ||
connection.sock.on('connect', function() { | ||
connection.open = true | ||
delete connection.closed | ||
done() | ||
state.connection.sock.write('hello\n') | ||
done({ connect: true, event: 'connect' }) | ||
}) | ||
connection.sock.on('error', function(err) { | ||
if(!connection.closed) { | ||
done(err) | ||
state.connection.sock.on('error', function (err) { | ||
// console.log('CE', err) | ||
if (state.connection.open) { | ||
return done({ event: 'error', err }) | ||
} | ||
}) | ||
connection.sock.on('close', function(err) { | ||
connection.open = false | ||
connection.closed = true | ||
spec.log('Connection closed.') | ||
vorpal.execSync('exit') | ||
state.connection.sock.on('close', function (err) { | ||
// console.log('CC', err) | ||
if (state.connection.open) { | ||
spec.log('\n\nConnection closed.') | ||
} | ||
state.connection.open = false | ||
state.connection.closed = true | ||
return done({ | ||
connect: false, | ||
event: 'close', | ||
quit: !!state.connection.quit, | ||
}) | ||
}) | ||
connection.sock.on('data', function(buffer) { | ||
var received = buffer.toString('ascii') | ||
const responseChunks = [] | ||
if(connection.first) { | ||
connection.first = false | ||
state.connection.sock.on('data', function (chunk) { | ||
const str = chunk.toString('ascii') | ||
// console.log('SOCK DATA', str) | ||
connection.remote_prompt = received | ||
connection.remote_seneca = received | ||
.replace(/^seneca\s+/,'') | ||
.replace(/->.*$/,'') | ||
if (0 < str.length && 0 === str.charCodeAt(str.length - 1)) { | ||
responseChunks.push(str) | ||
let received = responseChunks.join('') | ||
received = received.substring(0, received.length - 1) | ||
responseChunks.length = 0 | ||
spec.first = false | ||
handleResponse(received) | ||
} else if (0 < str.length) { | ||
responseChunks.push(str) | ||
} | ||
}) | ||
spec.log('Connected to '+connection.remote_seneca) | ||
} | ||
else { | ||
var rp = received.indexOf(connection.remote_prompt) | ||
if(-1 != rp) { | ||
received = received.substring(0,rp) | ||
function handleResponse(received) { | ||
if (state.connection.first) { | ||
state.connection.first = false | ||
let jsonstr = received.trim().replace(/[\r\n]/g, '') | ||
jsonstr = jsonstr.substring(1, jsonstr.length - 1) | ||
try { | ||
state.connection.remote = JSON.parse(jsonstr) | ||
} catch (err) { | ||
if (received.startsWith('# ERROR')) { | ||
console.log(received) | ||
} else { | ||
console.log('# HELLO ERROR: ', err.message, 'hello:', received) | ||
} | ||
process.exit(1) | ||
} | ||
state.connection.prompt = state.connection.remote.id + '> ' | ||
spec.log('Connected to Seneca:', state.connection.remote) | ||
if (null == state.connection.readline) { | ||
state.connection.readline = Readline.createInterface({ | ||
input: process.stdin, | ||
output: process.stdout, | ||
// prompt: 'QQQ', | ||
terminal: true, | ||
history, | ||
historySize: Number.MAX_SAFE_INTEGER, | ||
prompt: state.connection.prompt, | ||
}) | ||
state.connection.readline | ||
.on('line', (line) => { | ||
if ('quit' === line) { | ||
process.exit(0) | ||
} | ||
if (null != historyFile) { | ||
try { | ||
FS.appendFileSync(historyFile, line + OS.EOL) | ||
} catch (e) { | ||
// Don't save history | ||
} | ||
} | ||
state.connection.sock.write(line + '\n') | ||
// state.connection.readline.prompt() | ||
}) | ||
.on('error', (err) => { | ||
console.log('# READLINE ERROR:', err) | ||
process.exit(1) | ||
}) | ||
.on('close', () => { | ||
process.exit(0) | ||
}) | ||
} else { | ||
state.connection.readline.setPrompt(state.connection.prompt) | ||
} | ||
state.connection.readline.prompt() | ||
} else { | ||
received = received.replace(/\n+$/, '\n') | ||
spec.log(received) | ||
state.connection.readline.prompt() | ||
} | ||
} | ||
} | ||
// Create a duplex stream to operate the REPL | ||
function connect(spec) { | ||
let duplex = null | ||
let protocol = spec.url.protocol | ||
if ('telnet:' === protocol) { | ||
duplex = Net.connect(spec.port, spec.host) | ||
} else if ('http:' === protocol || 'https:' === protocol) { | ||
duplex = makeHttpDuplex(spec) | ||
} else { | ||
throw new Error( | ||
'unknown protocol: ' + protocol + ' for url: ' + spec.url.href, | ||
) | ||
} | ||
return duplex | ||
} | ||
// Assumes endpoint will call sys:repl,send:cmd | ||
// POST Body is: {cmd} | ||
function makeHttpDuplex(spec) { | ||
let reqstream = new RequestStream(spec) | ||
setImmediate(() => { | ||
reqstream.emit('connect') | ||
}) | ||
return reqstream | ||
} |
{ | ||
"name": "@seneca/repl", | ||
"description": "Provides a client and server REPL for Seneca microservice systems.", | ||
"version": "5.1.0", | ||
"main": "repl.js", | ||
"version": "6.0.0", | ||
"main": "dist/repl.js", | ||
"license": "MIT", | ||
@@ -20,12 +20,14 @@ "author": "Richard Rodger (https://github.com/rjrodger)", | ||
"scripts": { | ||
"test": "lab -P test -v -t 75 -r console -o stdout -r html -o test/coverage.html", | ||
"coveralls": "lab -s -P test -r lcov | coveralls", | ||
"coverage": "lab -v -P test -t 70 -r html > coverage.html", | ||
"prettier": "prettier --write --no-semi --single-quote *.js test/*.js", | ||
"build": "tsc -d", | ||
"watch": "tsc -w -d", | ||
"test": "jest --coverage", | ||
"test-some": "jest -t", | ||
"test-watch": "jest --coverage --watchAll", | ||
"prettier": "prettier --write --no-semi --single-quote bin/*.js src/*.ts test/*.js", | ||
"doc": "seneca-doc", | ||
"reset": "npm run clean && npm i && npm test", | ||
"clean": "rm -rf node_modules package-lock.json yarn.lock", | ||
"reset": "npm run clean && npm i && npm run build && npm test", | ||
"clean": "rm -rf dist node_modules package-lock.json yarn.lock", | ||
"repo-tag": "REPO_VERSION=`node -e \"console.log(require('./package').version)\"` && echo TAG: v$REPO_VERSION && git commit -a -m v$REPO_VERSION && git push && git tag v$REPO_VERSION && git push --tags;", | ||
"repo-publish": "npm run clean && npm i --registry=http://registry.npmjs.org && npm run repo-publish-quick", | ||
"repo-publish-quick": "npm run prettier && npm test && npm run repo-tag && npm publish --access public --registry=https://registry.npmjs.org" | ||
"repo-publish-quick": "npm run prettier && npm run build && npm test && npm run repo-tag && npm publish --access public --registry=https://registry.npmjs.org" | ||
}, | ||
@@ -42,21 +44,16 @@ "keywords": [ | ||
"dependencies": { | ||
"@hapi/hoek": "^10.0.1", | ||
"@seneca/vorpal": "^2.2.0", | ||
"@hapi/hoek": "^11.0.2", | ||
"inks": "^2.0.0" | ||
}, | ||
"devDependencies": { | ||
"@hapi/code": "^9.0.1", | ||
"@hapi/joi": "^17.1.1", | ||
"@hapi/lab": "^25.0.1", | ||
"acorn": "^8.8.0", | ||
"async": "^3.2.4", | ||
"coveralls": "^3.1.1", | ||
"gex": "^4.0.1", | ||
"prettier": "^2.7.1", | ||
"seneca": "plugin", | ||
"@seneca/maintain": "^0.1.0", | ||
"@seneca/entity-util": "^1.4.0", | ||
"jest": "^29.6.2", | ||
"prettier": "^3.0.0", | ||
"seneca": "^3.32.0", | ||
"seneca-doc": "^2.1.3", | ||
"seneca-entity": "^18.4.0", | ||
"seneca-mem-store": "^7.0.1", | ||
"seneca-plugin-validator": "0.6.1", | ||
"seneca-promisify": "^3.4.0" | ||
"seneca-entity": "^24.0.0", | ||
"seneca-mem-store": "^8.3.0", | ||
"seneca-promisify": "^3.6.0", | ||
"typescript": "5.1.6" | ||
}, | ||
@@ -66,5 +63,5 @@ "files": [ | ||
"README.md", | ||
"repl.js", | ||
"dist", | ||
"bin/seneca-repl-exec.js" | ||
] | ||
} |
![Seneca](http://senecajs.org/files/assets/seneca-logo.png) | ||
> A [Seneca.js][] plugin | ||
## NOTE: [Version 2 Plan](doc/version-2.md) | ||
# seneca-repl | ||
@@ -10,2 +14,5 @@ [![npm version][npm-badge]][npm-url] | ||
| ![Voxgig](https://www.voxgig.com/res/img/vgt01r.png) | This open source module is sponsored and supported by [Voxgig](https://www.voxgig.com). | | ||
|---|---| | ||
### Seneca compatibility | ||
@@ -12,0 +19,0 @@ Supports Seneca versions **3.x** and higher. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 2 instances in 1 package
68760
2
10
16
1079
213
0
1
+ Added@hapi/hoek@11.0.6(transitive)
- Removed@seneca/vorpal@^2.2.0
- Removed@hapi/hoek@10.0.1(transitive)
- Removed@seneca/inquirer@0.11.100(transitive)
- Removed@seneca/vorpal@2.2.0(transitive)
- Removedansi-escapes@1.4.04.3.2(transitive)
- Removedansi-regex@2.1.15.0.1(transitive)
- Removedansi-styles@2.2.14.3.0(transitive)
- Removedbabel-polyfill@6.26.0(transitive)
- Removedbabel-runtime@6.26.0(transitive)
- Removedchalk@1.1.33.0.0(transitive)
- Removedcli-cursor@1.0.23.1.0(transitive)
- Removedcli-width@2.2.1(transitive)
- Removedcode-point-at@1.1.0(transitive)
- Removedcolor-convert@2.0.1(transitive)
- Removedcolor-name@1.1.4(transitive)
- Removedcore-js@2.6.12(transitive)
- Removedemoji-regex@8.0.0(transitive)
- Removedescape-string-regexp@1.0.5(transitive)
- Removedexit-hook@1.1.1(transitive)
- Removedfigures@3.2.0(transitive)
- Removedhas-ansi@2.0.0(transitive)
- Removedhas-flag@4.0.0(transitive)
- Removedin-publish@2.0.1(transitive)
- Removedis-fullwidth-code-point@1.0.03.0.0(transitive)
- Removedlodash@4.17.21(transitive)
- Removedlog-update@1.0.2(transitive)
- Removedmimic-fn@2.1.0(transitive)
- Removedminimist@1.2.8(transitive)
- Removedmute-stream@0.0.5(transitive)
- Removednode-localstorage@0.6.0(transitive)
- Removednumber-is-nan@1.0.1(transitive)
- Removedonce@1.4.0(transitive)
- Removedonetime@1.1.05.1.2(transitive)
- Removedreadline2@1.0.1(transitive)
- Removedregenerator-runtime@0.10.50.11.1(transitive)
- Removedrestore-cursor@1.0.13.1.0(transitive)
- Removedrun-async@0.1.0(transitive)
- Removedrx-lite@4.0.8(transitive)
- Removedsignal-exit@3.0.7(transitive)
- Removedstring-width@4.2.3(transitive)
- Removedstrip-ansi@3.0.16.0.1(transitive)
- Removedsupports-color@2.0.07.2.0(transitive)
- Removedthrough@2.3.8(transitive)
- Removedtype-fest@0.21.3(transitive)
- Removedwrap-ansi@6.2.0(transitive)
- Removedwrappy@1.0.2(transitive)
Updated@hapi/hoek@^11.0.2