nestjs-web-repl 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
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ export interface RunResult {
3
+ code: number;
4
+ out: string;
5
+ err: string;
6
+ }
7
+ export declare function run(argv: string[], cwd: string): RunResult;
@@ -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,12 +1,16 @@
1
1
  {
2
2
  "name": "nestjs-web-repl",
3
- "version": "1.0.0",
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
  ],
11
15
  "repository": {
12
16
  "type": "git",
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.