@seneca/repl
Advanced tools
Comparing version 6.1.2 to 6.2.0
@@ -11,2 +11,3 @@ const { Duplex } = require('stream') | ||
let region = def.region || 'us-east-1' | ||
let id = def.id || 'invoke' | ||
let name = def.name | ||
@@ -18,2 +19,3 @@ | ||
this.processing = false | ||
this.id = id | ||
} | ||
@@ -36,3 +38,3 @@ | ||
send: 'cmd', | ||
id: 'invoke', | ||
id: this.id, | ||
cmd, | ||
@@ -58,3 +60,14 @@ }, | ||
this.buffer.push(body.out + String.fromCharCode(0)) | ||
let out = '' | ||
if (500 === res.statusCode) { | ||
out = | ||
'# ERROR: ' + body.error$ | ||
? body.error$.code + ' ' + (body.error$?.message || '') | ||
: 'unknown' | ||
} else { | ||
out = body.out | ||
} | ||
this.buffer.push(out + String.fromCharCode(0)) | ||
} else { | ||
@@ -71,3 +84,3 @@ this.buffer.push( | ||
(err) => { | ||
// console.log('err', err) | ||
console.log('err', err) | ||
this.buffer.push( | ||
@@ -113,2 +126,3 @@ `# ERROR invoking Lambda function: ${err}` + String.fromCharCode(0), | ||
const region = spec.url.searchParams.get('region') | ||
const id = spec.url.searchParams.get('id') | ||
@@ -118,3 +132,3 @@ duplex = new LambdaInvokeStream({ | ||
region, | ||
// region: 'eu-west-1' | ||
id, | ||
}) | ||
@@ -121,0 +135,0 @@ |
@@ -26,3 +26,3 @@ #!/usr/bin/env node | ||
let url = 'telnet:' + host + ':' + port | ||
let scope = 'default' | ||
let id = 'default' | ||
@@ -54,4 +54,4 @@ // NOTE: backwards compatibility: seneca-repl localhost 30303 | ||
// NOTE: use URL params for additional args | ||
scope = url.searchParams.get('scope') | ||
scope = null == scope || '' === scope ? 'default' : scope | ||
id = url.searchParams.get('id') | ||
id = null == id || '' === id ? 'web' : id | ||
} catch (e) { | ||
@@ -77,3 +77,5 @@ console.log('# CONNECTION URL ERROR: ', e.message, replAddr) | ||
.split(/[\r\n]+/) | ||
.reverse() | ||
.map((line) => (null != line && '' != line ? history.push(line) : null)) | ||
console.log('H', history) | ||
} | ||
@@ -88,4 +90,4 @@ | ||
port, | ||
scope, | ||
delay: 1111, | ||
id, | ||
delay: 31111, | ||
first: true, | ||
@@ -111,5 +113,8 @@ } | ||
const postData = JSON.stringify({ | ||
id: this.spec.id, | ||
cmd, | ||
}) | ||
// console.log('PD', postData) | ||
let req = httpClient | ||
@@ -133,7 +138,15 @@ .request( | ||
response.on('end', () => { | ||
// console.log('DATA', data) | ||
let res = JSON.parse(data) | ||
// console.log('res', res) | ||
// console.log('HE', data, res) | ||
if (res.ok) { | ||
this.buffer.push(res.out + String.fromCharCode(0)) | ||
} else { | ||
this.buffer.push( | ||
(res.err || '# ERROR: unknown') + String.fromCharCode(0), | ||
) | ||
} | ||
this.buffer.push(res.out + String.fromCharCode(0)) | ||
this.processing = false | ||
@@ -167,6 +180,2 @@ this._read() | ||
} | ||
if (this.buffer.length === 0) { | ||
this.push(null) | ||
} | ||
} | ||
@@ -205,3 +214,3 @@ } | ||
// console.log('CA', err) | ||
return done({ err }) | ||
return done && done({ err }) | ||
} | ||
@@ -222,3 +231,3 @@ | ||
state.connection.sock.write('hello\n') | ||
done({ connect: true, event: 'connect' }) | ||
done && done({ connect: true, event: 'connect' }) | ||
}) | ||
@@ -229,3 +238,3 @@ | ||
if (state.connection.open) { | ||
return done({ event: 'error', err }) | ||
return done && done({ event: 'error', err }) | ||
} | ||
@@ -242,7 +251,10 @@ }) | ||
return done({ | ||
connect: false, | ||
event: 'close', | ||
quit: !!state.connection.quit, | ||
}) | ||
return ( | ||
done && | ||
done({ | ||
connect: false, | ||
event: 'close', | ||
quit: !!state.connection.quit, | ||
}) | ||
) | ||
}) | ||
@@ -270,2 +282,3 @@ | ||
if (state.connection.first) { | ||
let first = true | ||
state.connection.first = false | ||
@@ -280,3 +293,6 @@ | ||
} catch (err) { | ||
if (received.startsWith('# ERROR')) { | ||
if (received.startsWith('# ERROR') || first) { | ||
received = received.startsWith('# ERROR') | ||
? received | ||
: '# ERROR: ' + received | ||
console.log(received) | ||
@@ -307,3 +323,11 @@ } else { | ||
.on('line', (line) => { | ||
if ('quit' === line) { | ||
// console.log('LINE', line) | ||
if (state.connection.closed) { | ||
return setImmediate(() => { | ||
operate(spec) | ||
}) | ||
} | ||
if ('quit' === line || 'exit' === line) { | ||
process.exit(0) | ||
@@ -310,0 +334,0 @@ } |
@@ -44,12 +44,13 @@ "use strict"; | ||
}; | ||
/* | ||
const QuitCmd: Cmd = (spec: CmdSpec) => { | ||
const { context, respond } = spec | ||
respond() | ||
} | ||
*/ | ||
const ListCmd = (spec) => { | ||
const { context, argstr, respond } = spec; | ||
let parts = argstr.trim().split(/\s+/); | ||
// console.log('PARTS', parts) | ||
if (0 < parts.length) { | ||
if (parts[0].match(/^plugins?$/)) { | ||
return respond(null, Object.keys(context.seneca.list_plugins())); | ||
} | ||
} | ||
let narrow = context.seneca.util.Jsonic(argstr); | ||
respond(null, context.seneca.list(narrow)); | ||
return respond(null, context.seneca.list(narrow)); | ||
}; | ||
@@ -59,2 +60,8 @@ const FindCmd = (spec) => { | ||
let narrow = context.seneca.util.Jsonic(argstr); | ||
if ('string' === typeof narrow) { | ||
// console.log('FP', narrow) | ||
let plugin = context.seneca.find_plugin(narrow); | ||
// console.log('FP p', plugin) | ||
return respond(null, plugin); | ||
} | ||
respond(null, context.seneca.find(narrow)); | ||
@@ -61,0 +68,0 @@ }; |
@@ -135,2 +135,5 @@ "use strict"; | ||
let cmd = msg.cmd; | ||
if (!cmd.endsWith('\n')) { | ||
cmd += '\n'; | ||
} | ||
let out = []; | ||
@@ -142,3 +145,3 @@ // TODO: dedup this | ||
replInst.output.removeListener('data', listener); | ||
reply({ out: out.join('') }); | ||
reply({ ok: true, out: out.join('') }); | ||
} | ||
@@ -260,3 +263,3 @@ out.push(chunk.toString()); | ||
// TODO: there should be a seneca.tree() | ||
tree: 'seneca.root.private$.actrouter', | ||
// tree: 'seneca.root.private$.actrouter', | ||
}), | ||
@@ -263,0 +266,0 @@ inspect: (0, gubu_1.Open)({}), |
{ | ||
"name": "@seneca/repl", | ||
"description": "Provides a client and server REPL for Seneca microservice systems.", | ||
"version": "6.1.2", | ||
"version": "6.2.0", | ||
"main": "dist/repl.js", | ||
@@ -6,0 +6,0 @@ "license": "MIT", |
296
readme.md
@@ -17,16 +17,39 @@ ![Seneca](http://senecajs.org/files/assets/seneca-logo.png) | ||
### Seneca compatibility | ||
Supports Seneca versions **3.x** and higher. | ||
## Install | ||
To install, simply use npm. Remember you will need to install [Seneca.js][] if you haven't already. | ||
This is Seneca plugin, so you'll also need the Seneca framework installed to use the REPL. | ||
```sh | ||
> npm install seneca | ||
> npm install @seneca/repl | ||
$ npm install seneca | ||
$ npm install @seneca/repl | ||
``` | ||
To use the REPL client on the command line, you should install globally: | ||
``` | ||
$ npm i -g seneca @seneca/repl | ||
$ seneca-repl # now works! | ||
``` | ||
### Installing optional components | ||
This plugin can provide a REPL for AWS Lambda functions (via | ||
`invoke`). You will need to install the AWS SDK so the REPL client can | ||
use it to connect to your lambda function. | ||
``` | ||
$ npm i -g @aws-sdk/client-lambda | ||
``` | ||
## Usage | ||
Add the REPL as a plugin to your Seneca instance. By default the | ||
plugin will listen on localhost port 30303. | ||
```js | ||
@@ -49,18 +72,163 @@ var Seneca = require('seneca') | ||
To access the REPL, run the `seneca-repl` command provided by this | ||
module. Install this as a global module for easy access: | ||
plugin. | ||
``` | ||
$ npm install -g @seneca/repl | ||
$ seneca-repl | ||
``` | ||
Provide the host (default `localhost`) and port (default `30303`): | ||
You can specify the target Seneca server using a URI | ||
``` | ||
$ seneca-repl remote-host 12345 | ||
$ seneca-repl telnet://localhost:30303 # same as default | ||
``` | ||
The `seneca-repl` command provides a convenient REPL interface including line editing and history. In production settings you'll want to create an SSH tunnel or similar | ||
for this purpose. | ||
NOTE: If the connection drops, the `seneca-repl` client will attempt | ||
to reconnect at regular intervals. This means you can stop and start | ||
your development server without needing to restart the REPL. | ||
Skip ahead to the [Commands](#commands) section if this is all you | ||
need. | ||
### REPL over Seneca Message | ||
You can submit REPL commands using the message | ||
`sys:repl,send:cmd`. This message requires an `id` property to | ||
indicate the REPL instance to use: | ||
``` | ||
const Seneca = require('seneca') | ||
const seneca = Seneca() | ||
.use('promisify') // npm install @seneca/promisify | ||
.use('repl') | ||
.act('sys:repl,use:repl,id:foo') | ||
await seneca.ready() | ||
let res = await seneca.post('sys:repl,send:cmd,id:foo', { | ||
cmd: '1+1' | ||
}) | ||
// Prints { ok: true, out:'4\n' } | ||
console.log(res) | ||
``` | ||
You can use this to expose a REPL connector in custom | ||
environments. This plugin provides a REPL over HTTP, and over AWS | ||
Lambda invocations. Review the implementation code for these if you | ||
want to write your own REPL connector. | ||
### REPL over HTTP(S) | ||
Opening a local port is usually only possible for local development, | ||
so you can also expose the REPL via a HTTP endpoint. This can be | ||
useful to debug build or staging systems. This is **NOT** recommended | ||
for production. | ||
> **WARNING** | ||
> This is a security risk. Your app will need to apply additional | ||
> constraints to prevent arbitrary message submission via the REPL. | ||
On the server, use the `sys:repl,use:repl` message to start a new REPL | ||
inside the Seneca instance. Do this on startup (without a REPL | ||
instance, a REPL connection will not operate). You will need to | ||
special an identifier for this REPL. | ||
```js | ||
seneca.act('sys:repl,use:repl,id:web') | ||
``` | ||
Next you will need to call the `sys:repl,send:cmd` message when your | ||
chosen HTTP endpoint for the REPL is called. For _express_, this might look like: | ||
``` | ||
const Express = require('express') | ||
const BodyParser = require('body-parser') | ||
const Seneca = require('seneca') | ||
const app = express() | ||
const seneca = Seneca() | ||
seneca | ||
.use('repl') | ||
.act('sys:repl,use:repl,id:web') | ||
app.use(bodyParser.json()) | ||
// Accepts body = {cmd:'...repl cmd goes here...'} | ||
app.post('/seneca-repl', (req, res) => { | ||
const body = req.body | ||
seneca.act( | ||
{ sys: 'repl', send: 'cmd', id: 'web', cmd: body.cmd }, | ||
function (err, result) { | ||
if (err) { | ||
return res.status(500).json({ ok: false, error: err.message }) | ||
} | ||
return res.json(result.out) | ||
}) | ||
}) | ||
app.listen(8080) | ||
``` | ||
On the command line, access the REPL using a HTTP URL: | ||
``` | ||
$ seneca-repl http://localhost:8888/seneca-repl?id=web | ||
``` | ||
By default, HTTP URLs with use `web` as the identifier. | ||
# REPL over AWS Lambda Invoke | ||
To expose a REPL from an AWS Lambda function using Seneca, use the set | ||
up code for the HTTP example in the Lambda itself. | ||
For the REPL client, you will need to install the | ||
[@aws-sdk/client-lambda](https://www.npmjs.com/package/@aws-sdk/client-lambda) | ||
package, and correctly configure your AWS access using the | ||
`$AWS_PROFILE` environment variable. | ||
Connect to your Lambda function REPL using: | ||
``` | ||
seneca-repl "aws://lambda/FUNCTION?region=REGION&id=invoke" | ||
``` | ||
where `FUNCTION` is the name of the Lambda function,`REGION` is the | ||
AWS region, such as `us-east-1` (the default). The default REPL id | ||
value is _invoke_. NOTE: make sure to quote or escape the `&`. | ||
This mechanism uses AWS Lambda invocation mechanism and thus relies on | ||
AWS for security. Special care should be taken with Lambdas that are | ||
externally exposed to prevent external requests from calling the | ||
Seneca REPL messages. | ||
> **WARNING** | ||
> This is a security risk. Your app will need to apply additional | ||
> constraints to prevent arbitrary message submission via the REPL. | ||
This can be useful to debug build or staging systems, but is **NOT** | ||
recommended for production, unless used with specifically access | ||
controlled Lambda functions. | ||
## Interactive Interface | ||
The `seneca-repl` command provides a convenient REPL interface | ||
including line editing and history. In remote settings you'll want | ||
to create an SSH tunnel or similar for this purpose. | ||
Alternatively you can telnet to the port: | ||
@@ -85,35 +253,67 @@ | ||
The repl evaluates JavaScript directly. See the | ||
[Node.js repl docs](https://nodejs.org/dist/latest-v6.x/docs/api/repl.html) | ||
for more. You also have a `seneca` instance available: | ||
The repl evaluates JavaScript directly: | ||
``` | ||
seneca x.y.z [seneca-id] -> seneca.toString() | ||
> 1+1 | ||
2 | ||
``` | ||
You also have a `seneca` instance available: | ||
``` | ||
> seneca.id | ||
'SENECA-ID' | ||
``` | ||
You can submit messages directly using | ||
[jsonic](https://github.com/rjrodger/jsonic) format: | ||
[jsonic](https://github.com/rjrodger/jsonic) format (JSON, but not strict!): | ||
``` | ||
seneca x.y.z [seneca-id] -> role:seneca,cmd:stats | ||
IN 000000: { role: 'seneca', cmd: 'stats' } # ftlbto0vvizm/6qt4gg83fylm cmd:stats,role:seneca (4aybxhxseldu) action_seneca_stats | ||
OUT 000000: { start: '2017-03-15T13:15:36.016Z', | ||
act: { calls: 3, done: 3, fails: 0, cache: 0 }, | ||
> role:seneca,cmd:stats | ||
{ | ||
start: '2023-08-01T17:37:39.880Z', | ||
act: { calls: 122, done: 121, fails: 8, cache: 0 }, | ||
actmap: undefined, | ||
now: '2017-03-15T13:17:15.313Z', | ||
uptime: 99297 } | ||
now: '2023-08-01T17:49:17.316Z', | ||
uptime: 697436 | ||
} | ||
``` | ||
The message and response are printed, along with a sequence number. If | ||
the Seneca instance is a client of other Seneca services, the message | ||
will be sent to the other services, and marked as transported. | ||
This is *very* useful for local debugging. | ||
It is often convenient to run a Seneca repl as a separate service, | ||
acting as a client to all the other Seneca services. This gives you a | ||
central point of control for your system. | ||
To access entity data, use the `list$`, `load$`, `save$` and `remove$` | ||
commands: | ||
There are some command aliases for common actions: | ||
``` | ||
> list$ foo | ||
[ | ||
{ entity$: '-/-/foo', ...}, | ||
{ entity$: '-/-/foo', ...}, | ||
... | ||
] | ||
``` | ||
* `list <pin>`: list local patterns, optionally narrowed by `pin` | ||
* `tree`: show local patterns in tree format | ||
These all accept the parameters: | ||
* entity canon (required): `zone/base/name` | ||
* query (optional): `{field:value,...}` | ||
NOTE: this is a Node.js REPL, so you also get some of the features of a Node.js REPL: | ||
* The value of the last response is placed into the `_` variable | ||
* You can use standard movement shortcuts like `Ctrl-A`, `Ctrl-E`, etc | ||
* Command history | ||
### Available Commands | ||
* `list <pin>|plugin`: | ||
* `<pin>`: list local message patterns, optionally narrowed by `pin` (e.g. `foo:1`) | ||
* `plugin`: list all plugins by full name | ||
* `find <pin>|<plugin-name>`: | ||
* `<pin`: find an _exact_ matching message pattern definition (e.g. `sys:entity,cmd:load`) | ||
* `<plugin-name>`: find a plugin definition | ||
* `list$ canon <query>`: list entity data (like `seneca.entity(canon).list$(query)`) | ||
* `load$ canon <query>`: load entity data (like `seneca.entity(canon).load$(query)`) | ||
* `save$ canon <data>`: save entity data (like `seneca.entity(canon).save$(data)`) | ||
* `remove$ canon <query>`: remove entity data (like `seneca.entity(canon).remove$(query)`) | ||
* `entity$ canon`: describe an entity (like `seneca.entity(canon)`) | ||
* `stats`: print local statistics | ||
@@ -123,7 +323,5 @@ * `stats full`: print full local statistics | ||
* `last`: run last command again | ||
* `history`: print command history | ||
* `set <path> <value>`: set a seneca option, e.g: `set debug.deprecation true` | ||
* `get <path>`: get a seneca option | ||
* `alias <name> <cmd>`: define a new alias | ||
* `trace`: toggle IN/OUT tracing of submitted messages | ||
* `log`: toggle printing of remote log entries in test format (NOTE: these are unfiltered) | ||
@@ -134,3 +332,13 @@ * `log match <literal>`: when logging is enabled, only print lines matching the provided literal string | ||
### History | ||
The command history is saved to text files in a `.seneca` in your home | ||
folder. History is unique to each target server. You can also add | ||
additional URL parameters to isolate a server history: | ||
``` | ||
$ seneca-repl localhost?project=foo # separate history for foo server | ||
$ seneca-repl localhost?project=bar # separate history for bar server | ||
``` | ||
<!--START:options--> | ||
@@ -148,3 +356,3 @@ | ||
seneca.use('doc', { name: value, ... }) | ||
seneca.use('repl', { name: value, ... }) | ||
@@ -169,2 +377,5 @@ | ||
* [add:cmd,sys:repl](#-addcmdsysrepl-) | ||
* [echo:true,sys:repl](#-echotruesysrepl-) | ||
* [send:cmd,sys:repl](#-sendcmdsysrepl-) | ||
* [sys:repl,use:repl](#-sysrepluserepl-) | ||
@@ -186,4 +397,25 @@ | ||
---------- | ||
### « `echo:true,sys:repl` » | ||
No description provided. | ||
---------- | ||
### « `send:cmd,sys:repl` » | ||
No description provided. | ||
---------- | ||
### « `sys:repl,use:repl` » | ||
No description provided. | ||
---------- | ||
<!--END:action-desc--> | ||
@@ -190,0 +422,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
79495
1238
445
1
17