@lenne.tech/cli 1.11.1 → 1.13.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.
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hoistWorkspacePnpmConfig = hoistWorkspacePnpmConfig;
4
+ /**
5
+ * pnpm workspace-scoped fields that must live at the workspace root.
6
+ * When present in sub-project package.json files, pnpm emits:
7
+ *
8
+ * WARN The field "<field>" was found in <path>. This will not take
9
+ * effect. You should configure "<field>" at the root of the workspace
10
+ * instead.
11
+ *
12
+ * Crucially, the WARN also means the values are silently ignored — CVE
13
+ * overrides defined only in projects/api/package.json never reach the
14
+ * install resolver. Hoisting them to the root fixes both the warning
15
+ * and the actual dependency-resolution behavior.
16
+ */
17
+ const WORKSPACE_SCOPED_PNPM_FIELDS = ['overrides', 'onlyBuiltDependencies', 'ignoredOptionalDependencies'];
18
+ /**
19
+ * Hoist workspace-scoped pnpm config from sub-projects into the root
20
+ * package.json. After this runs, sub-project package.json files no
21
+ * longer have `overrides`, `onlyBuiltDependencies`, or
22
+ * `ignoredOptionalDependencies`, and the root package.json contains
23
+ * the merged union.
24
+ *
25
+ * Idempotent: running twice has the same effect as running once.
26
+ *
27
+ * @param options.filesystem Gluegun filesystem tool
28
+ * @param options.projectDir Workspace root (contains pnpm-workspace.yaml)
29
+ * @param options.subProjects Sub-project dirs relative to projectDir
30
+ */
31
+ function hoistWorkspacePnpmConfig(options) {
32
+ var _a;
33
+ const { filesystem, projectDir, subProjects } = options;
34
+ const rootPkgPath = `${projectDir}/package.json`;
35
+ if (!filesystem.exists(rootPkgPath))
36
+ return;
37
+ const rootPkg = filesystem.read(rootPkgPath, 'json');
38
+ if (!rootPkg)
39
+ return;
40
+ (_a = rootPkg.pnpm) !== null && _a !== void 0 ? _a : (rootPkg.pnpm = {});
41
+ let rootChanged = false;
42
+ for (const subDir of subProjects) {
43
+ const subPkgPath = `${projectDir}/${subDir}/package.json`;
44
+ if (!filesystem.exists(subPkgPath))
45
+ continue;
46
+ const subPkg = filesystem.read(subPkgPath, 'json');
47
+ if (!(subPkg === null || subPkg === void 0 ? void 0 : subPkg.pnpm))
48
+ continue;
49
+ let subChanged = false;
50
+ for (const field of WORKSPACE_SCOPED_PNPM_FIELDS) {
51
+ const subValue = subPkg.pnpm[field];
52
+ if (subValue === undefined)
53
+ continue;
54
+ rootPkg.pnpm[field] = mergePnpmFieldValue(field, rootPkg.pnpm[field], subValue);
55
+ rootChanged = true;
56
+ delete subPkg.pnpm[field];
57
+ subChanged = true;
58
+ }
59
+ if (subChanged) {
60
+ // If the sub-project's pnpm section is now empty, drop it entirely.
61
+ if (subPkg.pnpm && Object.keys(subPkg.pnpm).length === 0) {
62
+ delete subPkg.pnpm;
63
+ }
64
+ filesystem.write(subPkgPath, `${JSON.stringify(subPkg, null, 2)}\n`);
65
+ }
66
+ }
67
+ if (rootChanged) {
68
+ filesystem.write(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
69
+ }
70
+ }
71
+ /**
72
+ * Merge two values for a pnpm workspace-scoped field.
73
+ *
74
+ * pnpm expects:
75
+ * - `overrides` → object ({pkg: version})
76
+ * - `onlyBuiltDependencies` → array of strings
77
+ * - `ignoredOptionalDependencies` → array of strings
78
+ *
79
+ * Arrays: deduplicated, alphabetically sorted union.
80
+ * Objects: sub-project values take precedence over root (sub-projects
81
+ * like nest-server-starter own the authoritative CVE override list for
82
+ * their transitive deps; the root usually only seeds cross-cutting
83
+ * handlebars/minimatch patches).
84
+ */
85
+ function mergePnpmFieldValue(field, rootValue, subValue) {
86
+ if (field === 'onlyBuiltDependencies' || field === 'ignoredOptionalDependencies') {
87
+ const rootArr = Array.isArray(rootValue) ? rootValue : [];
88
+ const subArr = Array.isArray(subValue) ? subValue : [];
89
+ return Array.from(new Set([...rootArr, ...subArr])).sort((a, b) => a.localeCompare(b));
90
+ }
91
+ const rootObj = rootValue && typeof rootValue === 'object' && !Array.isArray(rootValue)
92
+ ? rootValue
93
+ : {};
94
+ const subObj = subValue && typeof subValue === 'object' && !Array.isArray(subValue) ? subValue : {};
95
+ const merged = Object.assign(Object.assign({}, rootObj), subObj);
96
+ return Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
97
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatMarkdownTable = formatMarkdownTable;
4
+ /**
5
+ * Build an oxfmt-compatible Markdown table with padded columns so that the
6
+ * generated file passes `oxfmt --check` without a reformat pass.
7
+ *
8
+ * Column width = max(header length, longest cell length, 3). Cells are
9
+ * padded with trailing spaces; the separator row uses `-` characters of
10
+ * the same width.
11
+ *
12
+ * Character width note: uses JavaScript `.length` (UTF-16 code units),
13
+ * which matches oxfmt's own accounting for typical BMP characters used
14
+ * in VENDOR.md generation (em-dash `—`, backticks, version strings).
15
+ *
16
+ * @param headers Column headers (top row)
17
+ * @param rows Data rows; each row must have `headers.length` cells
18
+ * @returns Lines ready to concatenate with `\n`
19
+ */
20
+ function formatMarkdownTable(headers, rows) {
21
+ const columnCount = headers.length;
22
+ const widths = headers.map((h, i) => {
23
+ const cellMax = rows.reduce((max, row) => { var _a; return Math.max(max, ((_a = row[i]) !== null && _a !== void 0 ? _a : '').length); }, 0);
24
+ return Math.max(h.length, cellMax, 3);
25
+ });
26
+ const formatRow = (cells) => `| ${cells.map((cell, i) => cell.padEnd(widths[i])).join(' | ')} |`;
27
+ const lines = [formatRow(headers), `| ${widths.map((w) => '-'.repeat(w)).join(' | ')} |`];
28
+ for (const row of rows) {
29
+ const padded = Array.from({ length: columnCount }, (_, i) => { var _a; return (_a = row[i]) !== null && _a !== void 0 ? _a : ''; });
30
+ lines.push(formatRow(padded));
31
+ }
32
+ return lines;
33
+ }
@@ -11,6 +11,7 @@ Practical guide for converting a lenne.tech fullstack project from **npm mode**
11
11
  ## Table of Contents
12
12
 
13
13
  - [Prerequisites](#prerequisites)
14
+ - [Vendor Modification Policy](#vendor-modification-policy)
14
15
  - [Part 1: Convert npm → vendor](#part-1-convert-npm--vendor)
15
16
  - [Part 2: Update in vendor mode](#part-2-update-in-vendor-mode)
16
17
  - [Part 3: Roll back vendor → npm](#part-3-roll-back-vendor--npm)
@@ -32,6 +33,78 @@ Before you start, make sure:
32
33
 
33
34
  ---
34
35
 
36
+ ## Vendor Modification Policy
37
+
38
+ **Read this before you convert.** It shapes how you work in vendor mode.
39
+
40
+ Vendoring copies the framework source into your project tree:
41
+
42
+ - Backend: `projects/api/src/core/` (from `@lenne.tech/nest-server`)
43
+ - Frontend: `projects/app/app/core/` (from `@lenne.tech/nuxt-extensions`)
44
+
45
+ **This is a comprehension aid, not a fork.** The copy exists so Claude
46
+ Code (and humans) can read framework internals directly — it is **not**
47
+ an invitation to embed project-specific behavior in the framework tree.
48
+
49
+ ### When may I edit `core/`?
50
+
51
+ Only when the change is **generally useful to every consumer** of the
52
+ framework:
53
+
54
+ | ✅ Valid reason to edit `core/` | ❌ Belongs in project code instead |
55
+ |--------------------------------|------------------------------------|
56
+ | Bugfix that every consumer hits | Customer-specific business rules |
57
+ | Broad framework enhancement | Project tenant IDs, enums, branding |
58
+ | Security vulnerability fix | Proprietary integration adapters |
59
+ | TypeScript / build compatibility | Project-specific authorization logic |
60
+
61
+ **Project-specific behavior belongs outside `core/`:**
62
+
63
+ - Backend: extend/inherit from core classes, use `ICoreModuleOverrides`
64
+ on `CoreModule.forRoot()`
65
+ - Frontend: use `app/composables/`, `app/components/`,
66
+ `app/middleware/`, or plugin overrides
67
+
68
+ ### Every generic change MUST flow back upstream
69
+
70
+ If you fixed or improved something in `core/` that every consumer could
71
+ benefit from, you MUST submit it as a pull request to the corresponding
72
+ upstream repository:
73
+
74
+ | Layer | Command | Upstream |
75
+ |-------|---------|----------|
76
+ | Backend | `/lt-dev:backend:contribute-nest-server-core` | https://github.com/lenneTech/nest-server |
77
+ | Frontend | `/lt-dev:frontend:contribute-nuxt-extensions-core` | https://github.com/lenneTech/nuxt-extensions |
78
+
79
+ The contributor command analyzes your local changes, filters cosmetic
80
+ noise, categorizes commits as upstream-candidate vs. project-specific,
81
+ cherry-picks candidates onto a branch in a fresh upstream clone, and
82
+ drafts a PR body for your review. It **never auto-pushes** — you
83
+ explicitly open the PR via normal GitHub flow.
84
+
85
+ **Why this matters:** Without upstream flow-back, useful fixes rot in
86
+ one project's vendor tree and conflict on every sync. Once merged
87
+ upstream, the next `/lt-dev:*:update-*-core` sync picks the change up
88
+ as upstream-delivered and the local patch disappears — clean state for
89
+ everyone.
90
+
91
+ ### Where the policy is documented
92
+
93
+ The same policy is enforced / surfaced in multiple places so you
94
+ encounter it at every relevant touchpoint:
95
+
96
+ - `projects/api/src/core/VENDOR.md` and `projects/app/app/core/VENDOR.md`
97
+ (inside every vendor project, auto-generated by the lt CLI)
98
+ - Skills `nest-server-core-vendoring` and `nuxt-extensions-core-vendoring`
99
+ (lt-dev plugin)
100
+ - Reviewer agents `backend-reviewer` and `frontend-reviewer`
101
+ (flag non-compliant changes in code reviews)
102
+ - The contributor commands themselves
103
+
104
+ When in doubt, **ask before editing `core/`**.
105
+
106
+ ---
107
+
35
108
  ## Part 1: Convert npm → vendor
36
109
 
37
110
  ### Step 1: Check status
package/docs/commands.md CHANGED
@@ -230,7 +230,7 @@ lt server convert-mode --to <vendor|npm> [options]
230
230
 
231
231
  - **npm → vendor:**
232
232
  - Clones `@lenne.tech/nest-server` from GitHub at the specified tag (default: currently installed version)
233
- - Copies `src/core/`, `src/index.ts`, `src/core.module.ts`, `src/test/`, `src/templates/`, `src/types/`, and `LICENSE` to `<api-root>/src/core/`
233
+ - Copies `src/core/`, `src/index.ts`, `src/core.module.ts`, `src/test/`, `src/types/`, and `LICENSE` to `<api-root>/src/core/`; places upstream `src/templates/` at `<api-root>/src/templates/` (outside `core/` so the runtime E-Mail template resolver works)
234
234
  - Applies flatten-fix on `index.ts`, `core.module.ts`, `test.helper.ts`, `core-persistence-model.interface.ts`
235
235
  - Rewrites all consumer imports from `'@lenne.tech/nest-server'` to relative paths
236
236
  - Merges upstream dependencies dynamically into `package.json`
@@ -1409,6 +1409,58 @@ lt tools regex [pattern] [text]
1409
1409
 
1410
1410
  ---
1411
1411
 
1412
+ ### `lt tools crawl`
1413
+
1414
+ Crawls a website and stores each page as a Markdown file (with YAML frontmatter containing `source_url`, `download_date`, `first_downloaded`, `description`, language, word count, etc.) so it can be consumed as a Claude Code knowledge base. Optionally follows same-origin links up to a configurable depth, seeds the queue from `<origin>/sitemap.xml`, and downloads referenced images into a shared `images/` folder (deduplicated by content hash). Re-running the command against the same output directory updates existing pages while preserving their original `first_downloaded` timestamp.
1415
+
1416
+ **Alias:** `cr`
1417
+
1418
+ **Usage:**
1419
+ ```bash
1420
+ lt tools crawl <url> [options]
1421
+ ```
1422
+
1423
+ **Options:**
1424
+ - `--out <dir>` — Output directory (default: current directory). Single-page crawls write the `.md` directly here; multi-page crawls generate `<out>/README.md` plus `<out>/pages/` and `<out>/images/`.
1425
+ - `--depth <n|all>` — Link depth (default `0`). `0` = only the start page, `1` = start page + direct same-origin links, `2` = and their links, ... Use `--depth all` (or `--depth -1`, or the shortcut flag `--all`) to follow every same-origin link transitively; the crawl then stops when `--max-pages` is reached.
1426
+ - `--all` — Shortcut for `--depth all`.
1427
+ - `--render` / `--no-render` — Render each page through a headless browser before extraction (default **on**). Required for SPAs (Vue/Nuxt/React/Angular) whose content is client-rendered. Uses `playwright-core` with system Chrome / Edge first, then Playwright's bundled Chromium. Use `--no-render` for a plain HTTP fetch when you know the site is static (faster, no browser needed).
1428
+ - `--install-browser` — If `--render` finds no browser, auto-install Playwright's Chromium (one-time ~170 MB download).
1429
+ - `--prune` / `--no-prune` — After a multi-page crawl, remove any `.md` or image files inside `<out>/pages` and `<out>/images` that were not written by the current run (default **on**). Keeps the knowledge base aligned with the live site on update runs. Empty subdirectories are cleaned up too. Ignored in single-page mode. Use `--no-prune` to preserve old files.
1430
+ - `--no-images` — Disable image downloads.
1431
+ - `--no-sitemap` — Skip discovery via `<origin>/sitemap.xml`.
1432
+ - `--concurrency <n>` — Parallel HTTP requests (default `4`).
1433
+ - `--max-pages <n>` — Safety cap on total pages (default `200`).
1434
+ - `--selector <css>` — CSS selector scoping the main content (e.g. `article`, `main`).
1435
+ - `--timeout <ms>` — HTTP request timeout in ms (default `20000`).
1436
+ - `--noConfirm` — Skip confirmation prompts.
1437
+
1438
+ **Examples:**
1439
+ ```bash
1440
+ # Single page into the current directory
1441
+ lt tools crawl https://example.com/article --noConfirm
1442
+
1443
+ # Crawl start page + direct links into ./knowledge
1444
+ lt tools crawl https://example.com --out ./knowledge --depth 1 --noConfirm
1445
+
1446
+ # Full mini-site with sitemap seeding and images
1447
+ lt tools crawl https://example.com --out ./kb --depth 2 --max-pages 100 --noConfirm
1448
+
1449
+ # Crawl every reachable same-origin page (safety cap via --max-pages)
1450
+ lt tools crawl https://example.com --out ./kb --depth all --max-pages 500 --noConfirm
1451
+
1452
+ # Same, using the --all shortcut
1453
+ lt tools crawl https://example.com --out ./kb --all --max-pages 500 --noConfirm
1454
+
1455
+ # Full SPA-aware crawl (render + prune are on by default)
1456
+ lt tools crawl https://lenne.tech --all --noConfirm
1457
+
1458
+ # Opt-out: plain HTTP fetch for a known-static site, keep orphans
1459
+ lt tools crawl https://example.com --all --no-render --no-prune --noConfirm
1460
+ ```
1461
+
1462
+ ---
1463
+
1412
1464
  ## Configuration Priority
1413
1465
 
1414
1466
  All configurable commands follow this priority order (highest to lowest):
package/docs/lt.config.md CHANGED
@@ -923,6 +923,43 @@ Reinitializes npm packages.
923
923
  }
924
924
  ```
925
925
 
926
+ #### `lt tools crawl`
927
+
928
+ Crawls a website into Markdown files (knowledge base builder).
929
+
930
+ | Field | Type | Default | Description |
931
+ |-------|------|---------|-------------|
932
+ | `commands.tools.crawl.out` | `string` | `.` | Output directory |
933
+ | `commands.tools.crawl.depth` | `number \| "all"` | `0` | Link depth (0 = only start page, 1 = direct links, ..., `"all"` or `-1` = follow every same-origin link, bounded by `maxPages`) |
934
+ | `commands.tools.crawl.includeImages` | `boolean` | `true` | Download images and inline with local paths |
935
+ | `commands.tools.crawl.includeSitemap` | `boolean` | `true` | Also seed queue from `<origin>/sitemap.xml` |
936
+ | `commands.tools.crawl.concurrency` | `number` | `4` | Parallel HTTP requests |
937
+ | `commands.tools.crawl.maxPages` | `number` | `200` | Safety cap on total pages |
938
+ | `commands.tools.crawl.selector` | `string` | – | CSS selector for main content |
939
+ | `commands.tools.crawl.timeout` | `number` | `20000` | HTTP request timeout in ms |
940
+ | `commands.tools.crawl.renderJs` | `boolean` | `true` | Render pages through a headless browser (for SPAs). Uses playwright-core. Set to `false` for plain HTTP. |
941
+ | `commands.tools.crawl.prune` | `boolean` | `true` | Remove orphaned `.md` / image files after a multi-page crawl (update-in-place). Set to `false` to preserve old files. |
942
+ | `commands.tools.crawl.noConfirm` | `boolean` | `false` | Skip confirmation prompts |
943
+
944
+ **Example:**
945
+ ```json
946
+ {
947
+ "commands": {
948
+ "tools": {
949
+ "crawl": {
950
+ "out": "./knowledge",
951
+ "depth": 2,
952
+ "includeImages": true,
953
+ "includeSitemap": true,
954
+ "concurrency": 4,
955
+ "maxPages": 200,
956
+ "noConfirm": true
957
+ }
958
+ }
959
+ }
960
+ }
961
+ ```
962
+
926
963
  ---
927
964
 
928
965
  ### Metadata
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -18,7 +18,11 @@
18
18
  "lt": "bin/lt"
19
19
  },
20
20
  "scripts": {
21
- "check": "npm install && npm run format && npm run build && npm run start",
21
+ "c": "npm run check",
22
+ "cf": "npm run check:fix",
23
+ "check": "npm install && npm run format && npm run build && npm run check:start",
24
+ "check:fix": "npm install && npm audit fix && npm run format && npm run lint:fix && npm run build && npm run check:start",
25
+ "check:start": "bash scripts/check-cli-start.sh",
22
26
  "postinstall": "node bin/postinstall.js 2>/dev/null || true",
23
27
  "build": "npm run lint && npm run test && npm run clean-build && npm run compile && npm run copy-templates",
24
28
  "clean-build": "npx rimraf ./build",
@@ -53,20 +57,24 @@
53
57
  "bin"
54
58
  ],
55
59
  "dependencies": {
56
- "@aws-sdk/client-s3": "3.1029.0",
60
+ "@aws-sdk/client-s3": "3.1032.0",
57
61
  "@lenne.tech/cli-plugin-helper": "0.0.14",
58
62
  "axios": "1.15.0",
59
63
  "bcrypt": "6.0.0",
60
- "ejs": "5.0.1",
64
+ "defuddle": "0.17.0",
61
65
  "glob": "13.0.6",
62
66
  "gluegun": "5.2.2",
63
67
  "js-sha256": "0.11.1",
64
68
  "js-yaml": "4.1.1",
69
+ "jsdom": "29.0.2",
65
70
  "lodash": "4.18.1",
66
71
  "open": "11.0.0",
67
- "ts-morph": "27.0.2",
72
+ "playwright-core": "1.59.1",
73
+ "ts-morph": "28.0.0",
68
74
  "ts-node": "10.9.2",
69
- "typescript": "6.0.2"
75
+ "turndown": "7.2.4",
76
+ "turndown-plugin-gfm": "1.0.2",
77
+ "typescript": "6.0.3"
70
78
  },
71
79
  "devDependencies": {
72
80
  "@lenne.tech/eslint-config-ts": "2.1.4",
@@ -74,25 +82,32 @@
74
82
  "@types/ejs": "3.1.5",
75
83
  "@types/jest": "30.0.0",
76
84
  "@types/js-yaml": "4.0.9",
85
+ "@types/jsdom": "28.0.1",
77
86
  "@types/lodash": "4.17.24",
78
87
  "@types/node": "25.6.0",
79
- "@typescript-eslint/eslint-plugin": "8.58.1",
80
- "@typescript-eslint/parser": "8.58.1",
88
+ "@types/turndown": "5.0.6",
89
+ "@typescript-eslint/eslint-plugin": "8.58.2",
90
+ "@typescript-eslint/parser": "8.58.2",
91
+ "ejs": "5.0.2",
81
92
  "eslint": "9.39.4",
82
93
  "eslint-config-prettier": "10.1.8",
83
94
  "husky": "9.1.7",
84
95
  "jest": "30.3.0",
85
- "prettier": "3.8.2",
96
+ "prettier": "3.8.3",
86
97
  "rimraf": "6.1.3",
87
98
  "standard-version": "9.5.0",
88
99
  "ts-jest": "29.4.9"
89
100
  },
101
+ "//overrides": {
102
+ "semver@*": "Force latest semver across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep."
103
+ },
90
104
  "overrides": {
91
105
  "semver@*": "7.7.4"
92
106
  },
93
107
  "jest": {
94
108
  "testEnvironment": "node",
95
109
  "rootDir": "__tests__",
110
+ "testTimeout": 60000,
96
111
  "transform": {
97
112
  "^.+\\.tsx?$": [
98
113
  "ts-jest",