nestjs-web-repl 0.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 +69 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +58 -0
- package/dist/cli/install-skill.d.ts +17 -0
- package/dist/cli/install-skill.js +35 -0
- package/package.json +23 -2
- package/skill/SKILL.md +105 -0
package/README.md
CHANGED
|
@@ -274,6 +274,21 @@ for options that need DI (e.g. reading a `ConfigService`); see the
|
|
|
274
274
|
and the types `WebReplAdapter`, `WebReplModuleOptions`, `WebReplModuleAsyncOptions`,
|
|
275
275
|
`WebReplEvent`, `SseEventType`.
|
|
276
276
|
|
|
277
|
+
## AI skill
|
|
278
|
+
|
|
279
|
+
This package ships a [Claude Code](https://claude.com/claude-code) skill that
|
|
280
|
+
teaches coding agents how to wire in and use the REPL safely. After installing
|
|
281
|
+
the package, run:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npx nestjs-web-repl install-skill
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
This writes `.claude/skills/nestjs-web-repl/SKILL.md` into your project; your
|
|
288
|
+
agent picks it up on its next session. The command never clobbers a modified
|
|
289
|
+
skill file silently — if you have edited it, re-run with `--force` to refresh it
|
|
290
|
+
after upgrading the package.
|
|
291
|
+
|
|
277
292
|
## Limitations (v1)
|
|
278
293
|
|
|
279
294
|
- **Monaco loads from a CDN** (`cdn.jsdelivr.net`) inside the `/ui` page — the
|
|
@@ -348,6 +363,60 @@ intent and do not, and cannot, technically enforce anything, nor do they bind
|
|
|
348
363
|
GitHub's own hosting. Using an AI coding assistant to help you *work with* this
|
|
349
364
|
library is entirely fine and expected.
|
|
350
365
|
|
|
366
|
+
## Releasing
|
|
367
|
+
|
|
368
|
+
Releases are **fully automated**. Merging a PR to `main` with releasable
|
|
369
|
+
[Conventional Commits](https://www.conventionalcommits.org/) triggers
|
|
370
|
+
`.github/workflows/release.yml`, which runs the full test suite and then
|
|
371
|
+
[semantic-release](https://semantic-release.gitbook.io/):
|
|
372
|
+
|
|
373
|
+
| Commit type | Version bump |
|
|
374
|
+
| ---------------------- | ----------------- |
|
|
375
|
+
| `fix:` | patch (x.y.**z**) |
|
|
376
|
+
| `feat:` | minor (x.**y**.0) |
|
|
377
|
+
| `feat!:` / `BREAKING CHANGE:` in body | major (**x**.0.0) |
|
|
378
|
+
| `docs:` `chore:` `test:` `ci:` `refactor:` | no release |
|
|
379
|
+
|
|
380
|
+
It computes the next version, updates `CHANGELOG.md`, publishes to npm
|
|
381
|
+
(**tokenless via OIDC trusted publishing, with provenance attached
|
|
382
|
+
automatically**), tags the commit, cuts a GitHub Release, and commits the
|
|
383
|
+
version/changelog bump back to `main` as `chore(release): x.y.z [skip ci]`.
|
|
384
|
+
Do not bump `version` in `package.json` by hand.
|
|
385
|
+
|
|
386
|
+
### One-time bootstrap (maintainer, once)
|
|
387
|
+
|
|
388
|
+
npm's OIDC trusted publishing cannot perform a package's *first* publish, so a
|
|
389
|
+
maintainer does this once:
|
|
390
|
+
|
|
391
|
+
1. **Create the package on npm with a placeholder:**
|
|
392
|
+
```bash
|
|
393
|
+
npm login
|
|
394
|
+
npm version 0.0.0 --no-git-tag-version # temp, do not commit
|
|
395
|
+
npm publish --access public
|
|
396
|
+
git checkout -- package.json # restore working version
|
|
397
|
+
```
|
|
398
|
+
Do **not** create a git tag for `0.0.0`; with no tags semantic-release's
|
|
399
|
+
first automated release is `1.0.0`.
|
|
400
|
+
2. **Register the Trusted Publisher** at
|
|
401
|
+
`https://www.npmjs.com/package/nestjs-web-repl/access` → *Trusted Publishers*
|
|
402
|
+
→ GitHub Actions: owner `p-dim-popov`, repository `nestjs-web-repl`, workflow
|
|
403
|
+
`release.yml` (leave environment blank). After this, no token is needed.
|
|
404
|
+
3. (Optional, after `1.0.0` ships) `npm deprecate nestjs-web-repl@0.0.0 "placeholder"`.
|
|
405
|
+
|
|
406
|
+
### Verifying the first real release
|
|
407
|
+
|
|
408
|
+
After the bootstrap, the next merge to `main` containing a `feat:`/`fix:` commit
|
|
409
|
+
should produce `1.0.0`. Confirm:
|
|
410
|
+
|
|
411
|
+
- `npm view nestjs-web-repl version` → `1.0.0`
|
|
412
|
+
- the npm package page shows a provenance / "Published via GitHub Actions" badge
|
|
413
|
+
- a `v1.0.0` git tag and a matching GitHub Release with generated notes exist
|
|
414
|
+
- `CHANGELOG.md` and a `chore(release): 1.0.0` commit are on `main`
|
|
415
|
+
- the `release.yml` run is green
|
|
416
|
+
|
|
417
|
+
If the publish step fails with an auth error, the Trusted Publisher registration
|
|
418
|
+
(step 2) is missing or its repo/workflow fields don't match exactly.
|
|
419
|
+
|
|
351
420
|
## Contributing
|
|
352
421
|
|
|
353
422
|
Contributions are welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). AI-
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.run = run;
|
|
5
|
+
const install_skill_1 = require("./install-skill");
|
|
6
|
+
const USAGE = `nestjs-web-repl — CLI
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
nestjs-web-repl install-skill [--force] Install the Claude Code skill into
|
|
10
|
+
./.claude/skills/nestjs-web-repl/
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--force Overwrite an existing, locally modified SKILL.md
|
|
14
|
+
--help Show this help
|
|
15
|
+
`;
|
|
16
|
+
function run(argv, cwd) {
|
|
17
|
+
const [subcommand, ...rest] = argv;
|
|
18
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
19
|
+
return { code: 0, out: USAGE, err: '' };
|
|
20
|
+
}
|
|
21
|
+
if (subcommand === 'install-skill') {
|
|
22
|
+
const force = rest.includes('--force');
|
|
23
|
+
try {
|
|
24
|
+
const { status, path } = (0, install_skill_1.installSkill)({ cwd, force });
|
|
25
|
+
switch (status) {
|
|
26
|
+
case 'created':
|
|
27
|
+
return { code: 0, out: `created ${path}\n`, err: '' };
|
|
28
|
+
case 'up-to-date':
|
|
29
|
+
return { code: 0, out: `up to date ${path}\n`, err: '' };
|
|
30
|
+
case 'updated':
|
|
31
|
+
return { code: 0, out: `updated ${path}\n`, err: '' };
|
|
32
|
+
case 'differs':
|
|
33
|
+
return {
|
|
34
|
+
code: 1,
|
|
35
|
+
out: '',
|
|
36
|
+
err: `differs from the shipped version: ${path}\n` +
|
|
37
|
+
`re-run with --force to overwrite\n`,
|
|
38
|
+
};
|
|
39
|
+
default: {
|
|
40
|
+
const _exhaustive = status;
|
|
41
|
+
return { code: 1, out: '', err: `internal error: unhandled status ${String(_exhaustive)}\n` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return { code: 1, out: '', err: `error: ${err.message}\n` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { code: 2, out: '', err: `unknown command: ${subcommand}\n\n${USAGE}` };
|
|
50
|
+
}
|
|
51
|
+
if (require.main === module) {
|
|
52
|
+
const result = run(process.argv.slice(2), process.cwd());
|
|
53
|
+
if (result.out)
|
|
54
|
+
process.stdout.write(result.out);
|
|
55
|
+
if (result.err)
|
|
56
|
+
process.stderr.write(result.err);
|
|
57
|
+
process.exit(result.code);
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const SKILL_TARGET_SUBPATH: string;
|
|
2
|
+
export type InstallStatus = 'created' | 'up-to-date' | 'differs' | 'updated';
|
|
3
|
+
export interface InstallSkillOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
force?: boolean;
|
|
6
|
+
sourcePath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface InstallSkillResult {
|
|
9
|
+
status: InstallStatus;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the bundled skill source. Compiled to dist/cli/install-skill.js,
|
|
14
|
+
* so the package root is two levels up; skill/SKILL.md lives there.
|
|
15
|
+
*/
|
|
16
|
+
export declare function defaultSourcePath(): string;
|
|
17
|
+
export declare function installSkill(options: InstallSkillOptions): InstallSkillResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SKILL_TARGET_SUBPATH = void 0;
|
|
4
|
+
exports.defaultSourcePath = defaultSourcePath;
|
|
5
|
+
exports.installSkill = installSkill;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
exports.SKILL_TARGET_SUBPATH = (0, path_1.join)('.claude', 'skills', 'nestjs-web-repl', 'SKILL.md');
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the bundled skill source. Compiled to dist/cli/install-skill.js,
|
|
11
|
+
* so the package root is two levels up; skill/SKILL.md lives there.
|
|
12
|
+
*/
|
|
13
|
+
function defaultSourcePath() {
|
|
14
|
+
return (0, path_1.join)(__dirname, '..', '..', 'skill', 'SKILL.md');
|
|
15
|
+
}
|
|
16
|
+
function installSkill(options) {
|
|
17
|
+
const { cwd, force = false } = options;
|
|
18
|
+
const sourcePath = options.sourcePath ?? defaultSourcePath();
|
|
19
|
+
const source = (0, fs_1.readFileSync)(sourcePath, 'utf8');
|
|
20
|
+
const path = (0, path_1.join)(cwd, exports.SKILL_TARGET_SUBPATH);
|
|
21
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
22
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
23
|
+
(0, fs_1.writeFileSync)(path, source);
|
|
24
|
+
return { status: 'created', path };
|
|
25
|
+
}
|
|
26
|
+
const current = (0, fs_1.readFileSync)(path, 'utf8');
|
|
27
|
+
if (current === source) {
|
|
28
|
+
return { status: 'up-to-date', path };
|
|
29
|
+
}
|
|
30
|
+
if (!force) {
|
|
31
|
+
return { status: 'differs', path };
|
|
32
|
+
}
|
|
33
|
+
(0, fs_1.writeFileSync)(path, source);
|
|
34
|
+
return { status: 'updated', path };
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nestjs-web-repl",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Expose a live NestJS REPL over the network (HTTP + SSE + Monaco UI).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nestjs-web-repl": "dist/cli/index.js"
|
|
10
|
+
},
|
|
8
11
|
"files": [
|
|
9
|
-
"dist"
|
|
12
|
+
"dist",
|
|
13
|
+
"skill"
|
|
10
14
|
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/p-dim-popov/nestjs-web-repl.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/p-dim-popov/nestjs-web-repl/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/p-dim-popov/nestjs-web-repl#readme",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
11
29
|
"scripts": {
|
|
12
30
|
"build": "tsc -p tsconfig.build.json",
|
|
13
31
|
"test": "vitest run --passWithNoTests",
|
|
@@ -24,11 +42,14 @@
|
|
|
24
42
|
"@nestjs/core": "^11",
|
|
25
43
|
"@nestjs/platform-express": "^11",
|
|
26
44
|
"@nestjs/testing": "^11",
|
|
45
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
46
|
+
"@semantic-release/git": "^10.0.1",
|
|
27
47
|
"@swc/core": "^1.15.43",
|
|
28
48
|
"@types/node": "^20",
|
|
29
49
|
"@types/supertest": "^6",
|
|
30
50
|
"reflect-metadata": "^0.2",
|
|
31
51
|
"rxjs": "^7",
|
|
52
|
+
"semantic-release": "^25.0.8",
|
|
32
53
|
"supertest": "^7",
|
|
33
54
|
"ts-node": "^10.9.2",
|
|
34
55
|
"typescript": "^5",
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-web-repl
|
|
3
|
+
description: Use when wiring a live, in-process REPL into a NestJS app over HTTP via the nestjs-web-repl package — adding WebReplModule to AppModule, gating the REPL behind an environment flag, or calling/debugging its POST command, SSE stream, or browser-UI routes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# nestjs-web-repl
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`nestjs-web-repl` exposes a live NestJS REPL over HTTP: a POST command endpoint,
|
|
11
|
+
a Server-Sent Events output stream, and a Monaco browser UI, backed by real
|
|
12
|
+
`@nestjs/core` REPL sessions running inside the host app.
|
|
13
|
+
|
|
14
|
+
## When to use this skill
|
|
15
|
+
|
|
16
|
+
- Adding `WebReplModule` to a NestJS `AppModule` for the first time.
|
|
17
|
+
- Reviewing or writing the code that enables/disables the REPL (the security
|
|
18
|
+
gate below is non-negotiable).
|
|
19
|
+
- Calling or troubleshooting the `POST`/SSE/browser-UI routes at runtime.
|
|
20
|
+
- Running the package's own examples (there's a `ts-node`-vs-`tsx` gotcha).
|
|
21
|
+
|
|
22
|
+
## Security first — read before wiring
|
|
23
|
+
|
|
24
|
+
The endpoints execute arbitrary code inside the running app **by design**. The
|
|
25
|
+
library ships **no authentication** (intentional). Before adding the module:
|
|
26
|
+
|
|
27
|
+
- Gate it behind an environment flag so it is OFF by default:
|
|
28
|
+
`enabled: process.env.REPL_ENABLED === 'true'`.
|
|
29
|
+
- Never enable it in production without your own authentication or network
|
|
30
|
+
restriction in front of the routes.
|
|
31
|
+
- A disabled module 404s every route and does not subscribe to the adapter —
|
|
32
|
+
keep it that way.
|
|
33
|
+
|
|
34
|
+
## Wire it up
|
|
35
|
+
|
|
36
|
+
Add the module to your `AppModule`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { Module } from '@nestjs/common';
|
|
40
|
+
import { WebReplModule } from 'nestjs-web-repl';
|
|
41
|
+
|
|
42
|
+
@Module({
|
|
43
|
+
imports: [
|
|
44
|
+
WebReplModule.forRoot({
|
|
45
|
+
enabled: process.env.REPL_ENABLED === 'true',
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
export class AppModule {}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Config-driven `enabled` (async form):
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
WebReplModule.forRootAsync({
|
|
56
|
+
imports: [ConfigModule],
|
|
57
|
+
inject: [ConfigService],
|
|
58
|
+
useFactory: (config: ConfigService) => ({
|
|
59
|
+
enabled: config.get('REPL_ENABLED') === 'true',
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Use it
|
|
65
|
+
|
|
66
|
+
Three routes are registered (default base path `repl`), keyed by a channel name:
|
|
67
|
+
|
|
68
|
+
| Route | Purpose |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `POST repl/{channel}` | Send a line of code to execute. |
|
|
71
|
+
| `GET repl/{channel}` | Server-Sent Events stream of output for the channel. |
|
|
72
|
+
| `GET repl/{channel}/ui` | Monaco editor + terminal browser UI. |
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# stream output (leave running in one terminal)
|
|
76
|
+
curl -N http://localhost:3000/repl/main
|
|
77
|
+
|
|
78
|
+
# in another terminal, run a command on the same channel
|
|
79
|
+
curl -X POST http://localhost:3000/repl/main \
|
|
80
|
+
-H 'content-type: application/json' \
|
|
81
|
+
-d '{"command":"1 + 1"}'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Open `http://localhost:3000/repl/main/ui` in a browser for the interactive UI.
|
|
85
|
+
|
|
86
|
+
## When you need more
|
|
87
|
+
|
|
88
|
+
- **Multiple app instances** (load-balanced replicas): the default in-memory
|
|
89
|
+
adapter is per-process; supply a shared adapter so a command and its output
|
|
90
|
+
reach the owning instance. See "Adapter / multi-instance" in the package
|
|
91
|
+
README.
|
|
92
|
+
- **Custom adapter:** implement the adapter interface (publish/subscribe over
|
|
93
|
+
the message topics). See "Adapter / multi-instance" in the README.
|
|
94
|
+
- **Options** (base path, TTLs, heartbeats) and **exported symbols:** see
|
|
95
|
+
"Options" and "Exports" in the README.
|
|
96
|
+
- **Runnable example gotcha:** run examples with `ts-node`, not `tsx` —
|
|
97
|
+
`tsx`/esbuild strips the decorator metadata NestJS DI needs. See "Limitations"
|
|
98
|
+
in the README.
|
|
99
|
+
|
|
100
|
+
## Common mistakes
|
|
101
|
+
|
|
102
|
+
- Enabling the REPL unconditionally (or defaulting `enabled` to `true`) —
|
|
103
|
+
always key it off an environment flag that defaults to off.
|
|
104
|
+
- Running a README example with `tsx` and hitting missing-provider/DI errors —
|
|
105
|
+
use `ts-node` instead.
|