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 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,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,13 +1,31 @@
1
1
  {
2
2
  "name": "nestjs-web-repl",
3
- "version": "0.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
  ],
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.