@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.
- package/build/commands/claude/shortcuts.js +5 -0
- package/build/commands/config/validate.js +36 -2
- package/build/commands/fullstack/init.js +21 -0
- package/build/commands/fullstack/update.js +1 -4
- package/build/commands/git/reset.js +2 -2
- package/build/commands/git/update.js +3 -3
- package/build/commands/server/add-property.js +1 -3
- package/build/commands/server/module.js +1 -1
- package/build/commands/tools/crawl.js +307 -0
- package/build/config/vendor-runtime-deps.json +1 -5
- package/build/extensions/api-mode.js +123 -5
- package/build/extensions/frontend-helper.js +59 -32
- package/build/extensions/git.js +4 -5
- package/build/extensions/server.js +80 -42
- package/build/lib/browser-fetcher.js +139 -0
- package/build/lib/crawler.js +661 -0
- package/build/lib/frontend-framework-detection.js +1 -2
- package/build/lib/hoist-workspace-pnpm-config.js +97 -0
- package/build/lib/markdown-table.js +33 -0
- package/docs/VENDOR-MODE-WORKFLOW.md +73 -0
- package/docs/commands.md +53 -1
- package/docs/lt.config.md +37 -0
- package/package.json +24 -9
|
@@ -30,6 +30,11 @@ const CLAUDE_SHORTCUTS = [
|
|
|
30
30
|
command: 'claude --dangerously-skip-permissions --resume',
|
|
31
31
|
description: 'Select and resume previous session',
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
alias: 'cf',
|
|
35
|
+
command: 'LT_PLUGIN_HOOKS_SKIP=1 claude --dangerously-skip-permissions',
|
|
36
|
+
description: 'Start Claude Code in fast mode (skip lenne.tech plugin detect hooks)',
|
|
37
|
+
},
|
|
33
38
|
];
|
|
34
39
|
/**
|
|
35
40
|
* Install Claude Code shell shortcuts
|
|
@@ -114,6 +114,22 @@ const KNOWN_KEYS = {
|
|
|
114
114
|
path: 'string',
|
|
115
115
|
},
|
|
116
116
|
},
|
|
117
|
+
tools: {
|
|
118
|
+
crawl: {
|
|
119
|
+
concurrency: 'number',
|
|
120
|
+
depth: 'number|all',
|
|
121
|
+
includeImages: 'boolean',
|
|
122
|
+
includeSitemap: 'boolean',
|
|
123
|
+
maxPages: 'number',
|
|
124
|
+
noConfirm: 'boolean',
|
|
125
|
+
out: 'string',
|
|
126
|
+
prune: 'boolean',
|
|
127
|
+
renderJs: 'boolean',
|
|
128
|
+
selector: 'string',
|
|
129
|
+
timeout: 'number',
|
|
130
|
+
},
|
|
131
|
+
noConfirm: 'boolean',
|
|
132
|
+
},
|
|
117
133
|
typescript: {
|
|
118
134
|
create: { author: 'string', noConfirm: 'boolean', updatePackages: 'boolean' },
|
|
119
135
|
},
|
|
@@ -165,8 +181,26 @@ function validateConfig(config, knownKeys, path = '') {
|
|
|
165
181
|
}
|
|
166
182
|
// Validate type
|
|
167
183
|
if (typeof expectedType === 'string') {
|
|
168
|
-
// Simple type check
|
|
169
|
-
if (expectedType
|
|
184
|
+
// Simple type check. `'a|b'` means union (e.g. "number|all").
|
|
185
|
+
if (expectedType.includes('|')) {
|
|
186
|
+
const tokens = expectedType.split('|').map((t) => t.trim());
|
|
187
|
+
const ok = tokens.some((token) => {
|
|
188
|
+
if (token === 'string')
|
|
189
|
+
return typeof value === 'string';
|
|
190
|
+
if (token === 'number')
|
|
191
|
+
return typeof value === 'number';
|
|
192
|
+
if (token === 'boolean')
|
|
193
|
+
return typeof value === 'boolean';
|
|
194
|
+
if (token === 'array')
|
|
195
|
+
return Array.isArray(value);
|
|
196
|
+
// Everything else is treated as a string literal enum member.
|
|
197
|
+
return value === token;
|
|
198
|
+
});
|
|
199
|
+
if (!ok) {
|
|
200
|
+
result.errors.push(`${currentPath}: expected ${tokens.join(' | ')}, got ${typeof value}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (expectedType === 'string' && typeof value !== 'string') {
|
|
170
204
|
result.errors.push(`${currentPath}: expected string, got ${typeof value}`);
|
|
171
205
|
}
|
|
172
206
|
else if (expectedType === 'boolean' && typeof value !== 'boolean') {
|
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
12
13
|
/**
|
|
13
14
|
* Create a new fullstack workspace
|
|
14
15
|
*/
|
|
@@ -456,6 +457,14 @@ const NewCommand = {
|
|
|
456
457
|
else {
|
|
457
458
|
serverSpinner.warn('Nest Server Starter not integrated');
|
|
458
459
|
}
|
|
460
|
+
// Hoist workspace-scoped pnpm config out of sub-projects. pnpm only
|
|
461
|
+
// honors `pnpm.overrides`, `pnpm.onlyBuiltDependencies`, and
|
|
462
|
+
// `pnpm.ignoredOptionalDependencies` at the workspace root; leaving
|
|
463
|
+
// them in projects/api/package.json or projects/app/package.json
|
|
464
|
+
// causes `WARN The field … was found in … This will not take
|
|
465
|
+
// effect. You should configure … at the root of the workspace
|
|
466
|
+
// instead.` and silently disables CVE overrides.
|
|
467
|
+
(0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({ filesystem, projectDir, subProjects: ['projects/api', 'projects/app'] });
|
|
459
468
|
// Install all packages
|
|
460
469
|
const installSpinner = spin('Install all packages');
|
|
461
470
|
try {
|
|
@@ -467,6 +476,18 @@ const NewCommand = {
|
|
|
467
476
|
installSpinner.fail(`Failed to install packages: ${err.message}`);
|
|
468
477
|
return;
|
|
469
478
|
}
|
|
479
|
+
// Post-install format pass. processApiMode (run earlier in
|
|
480
|
+
// setupServerForFullstack) and convertAppCloneToVendored rewrite
|
|
481
|
+
// source files, leaving whitespace artifacts that oxfmt flags in
|
|
482
|
+
// `pnpm run format:check` (multi-line arrays/imports after region
|
|
483
|
+
// stripping, import-path rewrites that now fit single-line). The
|
|
484
|
+
// formatter is only available after install, so we normalize here.
|
|
485
|
+
if (apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
|
|
486
|
+
yield toolbox.apiMode.formatProject(`${projectDir}/projects/api`);
|
|
487
|
+
}
|
|
488
|
+
if (isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
|
|
489
|
+
yield toolbox.apiMode.formatProject(`${projectDir}/projects/app`);
|
|
490
|
+
}
|
|
470
491
|
// Create initial commit after everything is set up
|
|
471
492
|
try {
|
|
472
493
|
yield system.run(`cd ${projectDir} && git add . && git commit -m "Initial commit"`);
|
|
@@ -126,10 +126,7 @@ const NewCommand = {
|
|
|
126
126
|
info(colors.dim('─'.repeat(60)));
|
|
127
127
|
info('');
|
|
128
128
|
// Detect frontend project
|
|
129
|
-
const appCandidates = [
|
|
130
|
-
(0, path_1.join)(cwd, 'projects', 'app'),
|
|
131
|
-
(0, path_1.join)(cwd, 'packages', 'app'),
|
|
132
|
-
].filter((p) => Boolean(p));
|
|
129
|
+
const appCandidates = [(0, path_1.join)(cwd, 'projects', 'app'), (0, path_1.join)(cwd, 'packages', 'app')].filter((p) => Boolean(p));
|
|
133
130
|
let appDir;
|
|
134
131
|
for (const candidate of appCandidates) {
|
|
135
132
|
if (filesystem.exists((0, path_1.join)(candidate, 'nuxt.config.ts')) || filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
|
|
@@ -45,8 +45,8 @@ const NewCommand = {
|
|
|
45
45
|
error('No current branch!');
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
// Check remote
|
|
49
|
-
const remoteBranch = yield system.run(`git ls-remote --heads origin ${branch}`);
|
|
48
|
+
// Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
|
|
49
|
+
const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git ls-remote --heads origin ${branch} 2>/dev/null || true`);
|
|
50
50
|
if (!remoteBranch) {
|
|
51
51
|
error(`No remote branch ${branch} found!`);
|
|
52
52
|
return;
|
|
@@ -47,8 +47,8 @@ const NewCommand = {
|
|
|
47
47
|
info('');
|
|
48
48
|
info(`Current branch: ${branch}`);
|
|
49
49
|
info('');
|
|
50
|
-
// Fetch to see incoming changes
|
|
51
|
-
yield run('git fetch');
|
|
50
|
+
// Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline)
|
|
51
|
+
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
|
|
52
52
|
// Check for incoming commits
|
|
53
53
|
const incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline 2>/dev/null || echo ""`);
|
|
54
54
|
const commits = (incomingCommits === null || incomingCommits === void 0 ? void 0 : incomingCommits.trim().split('\n').filter((c) => c)) || [];
|
|
@@ -78,7 +78,7 @@ const NewCommand = {
|
|
|
78
78
|
const timer = startTimer();
|
|
79
79
|
// Update
|
|
80
80
|
const updateSpin = spin(`Update branch ${branch}`);
|
|
81
|
-
yield run('git fetch && git pull --rebase');
|
|
81
|
+
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git pull --rebase');
|
|
82
82
|
updateSpin.succeed();
|
|
83
83
|
// Install packages (unless skipped) with correctly detected package manager (supports monorepo lockfiles)
|
|
84
84
|
if (!skipInstall) {
|
|
@@ -434,9 +434,7 @@ const NewCommand = {
|
|
|
434
434
|
// path to src/core whose depth depends on the model file location.
|
|
435
435
|
// We search for BOTH forms so this works regardless of how the file
|
|
436
436
|
// was originally generated.
|
|
437
|
-
const vendoredSpec = (0, framework_detection_1.isVendoredProject)(path)
|
|
438
|
-
? (0, framework_detection_1.getFrameworkImportSpecifier)(path, modelPath)
|
|
439
|
-
: null;
|
|
437
|
+
const vendoredSpec = (0, framework_detection_1.isVendoredProject)(path) ? (0, framework_detection_1.getFrameworkImportSpecifier)(path, modelPath) : null;
|
|
440
438
|
let existingImports = moduleFile.getImportDeclaration('@lenne.tech/nest-server');
|
|
441
439
|
if (!existingImports && vendoredSpec) {
|
|
442
440
|
existingImports = moduleFile.getImportDeclaration(vendoredSpec);
|
|
@@ -410,7 +410,7 @@ const NewCommand = {
|
|
|
410
410
|
// plus the `from '@nestjs/common'` clause. The replacement wedges
|
|
411
411
|
// `, forwardRef` in between.
|
|
412
412
|
yield patching.patch(serverModule, {
|
|
413
|
-
insert:
|
|
413
|
+
insert: '$1, forwardRef$2',
|
|
414
414
|
replace: /(import\s*\{\s*[^}]*?)(\s*\}\s*from\s+['"]@nestjs\/common['"])/,
|
|
415
415
|
});
|
|
416
416
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const crawler_1 = require("../../lib/crawler");
|
|
14
|
+
/**
|
|
15
|
+
* Crawl a website (optionally following same-origin links up to a
|
|
16
|
+
* configurable depth) and store the content as Markdown files for use
|
|
17
|
+
* as a Claude Code knowledge base. Inspired by ../../../../chrome-md:
|
|
18
|
+
* shares the defuddle + Turndown extraction pipeline but runs headless
|
|
19
|
+
* from Node and follows links / sitemaps automatically.
|
|
20
|
+
*/
|
|
21
|
+
const NewCommand = {
|
|
22
|
+
alias: ['cr'],
|
|
23
|
+
description: 'Crawl site to Markdown',
|
|
24
|
+
hidden: false,
|
|
25
|
+
name: 'crawl',
|
|
26
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
27
|
+
var _a, _b, _c, _d, _e;
|
|
28
|
+
const { config, filesystem, helper, parameters, print: { error, info, spin, success, warning }, prompt: { confirm }, tools, } = toolbox;
|
|
29
|
+
if (tools.helpJson({
|
|
30
|
+
aliases: ['cr'],
|
|
31
|
+
description: 'Crawl a website into Markdown files (for Claude Code knowledge bases)',
|
|
32
|
+
name: 'crawl',
|
|
33
|
+
options: [
|
|
34
|
+
{
|
|
35
|
+
description: 'Start URL (absolute http/https URL)',
|
|
36
|
+
flag: '--url',
|
|
37
|
+
required: true,
|
|
38
|
+
type: 'string',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
default: '.',
|
|
42
|
+
description: 'Output directory (created if missing)',
|
|
43
|
+
flag: '--out',
|
|
44
|
+
type: 'string',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
default: 0,
|
|
48
|
+
description: 'Link depth. 0 = only start page; 1 = + direct links; N = up to N hops; "all" (or -1) = follow every same-origin link until --max-pages is reached',
|
|
49
|
+
flag: '--depth',
|
|
50
|
+
type: 'number|all',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
default: true,
|
|
54
|
+
description: 'Download images and inline them with local paths',
|
|
55
|
+
flag: '--images',
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
default: true,
|
|
60
|
+
description: 'Also seed queue from <origin>/sitemap.xml',
|
|
61
|
+
flag: '--sitemap',
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
default: 4,
|
|
66
|
+
description: 'Parallel HTTP requests',
|
|
67
|
+
flag: '--concurrency',
|
|
68
|
+
type: 'number',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
default: 200,
|
|
72
|
+
description: 'Maximum number of pages to crawl (safety cap)',
|
|
73
|
+
flag: '--max-pages',
|
|
74
|
+
type: 'number',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
description: 'CSS selector for the main content container',
|
|
78
|
+
flag: '--selector',
|
|
79
|
+
type: 'string',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
default: 20000,
|
|
83
|
+
description: 'HTTP request timeout in ms',
|
|
84
|
+
flag: '--timeout',
|
|
85
|
+
type: 'number',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
default: false,
|
|
89
|
+
description: 'Shortcut for --depth all (follows every same-origin link until --max-pages)',
|
|
90
|
+
flag: '--all',
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
default: true,
|
|
95
|
+
description: "Render pages through a headless browser before extracting (for SPAs like Vue/Nuxt/React/Angular). Uses playwright-core with system Chrome / Edge, falling back to Playwright's bundled chromium. Disable with --no-render for plain HTTP fetches.",
|
|
96
|
+
flag: '--render',
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
default: false,
|
|
101
|
+
description: 'If --render cannot find any browser, auto-install Playwright chromium (one-time ~170 MB download).',
|
|
102
|
+
flag: '--install-browser',
|
|
103
|
+
type: 'boolean',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
default: true,
|
|
107
|
+
description: 'After a multi-page crawl, remove any .md or image files inside <outDir>/pages and <outDir>/images that were not written by this run. Disable with --no-prune to preserve old files.',
|
|
108
|
+
flag: '--prune',
|
|
109
|
+
type: 'boolean',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
default: false,
|
|
113
|
+
description: 'Skip confirmation prompts',
|
|
114
|
+
flag: '--noConfirm',
|
|
115
|
+
type: 'boolean',
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
})) {
|
|
119
|
+
return 'crawl';
|
|
120
|
+
}
|
|
121
|
+
tools.nonInteractiveHint('lt tools crawl <url> --out <dir> --depth 1 --noConfirm');
|
|
122
|
+
const ltConfig = config.loadConfig();
|
|
123
|
+
const commandConfig = (_b = (_a = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _a === void 0 ? void 0 : _a.tools) === null || _b === void 0 ? void 0 : _b.crawl;
|
|
124
|
+
// URL: positional argument > --url > interactive prompt.
|
|
125
|
+
const urlInput = parameters.first ||
|
|
126
|
+
parameters.options.url ||
|
|
127
|
+
(yield helper.getInput(undefined, { name: 'Website URL', showError: false }));
|
|
128
|
+
if (!urlInput) {
|
|
129
|
+
error('No URL provided');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const url = normalizeSeedUrl(urlInput);
|
|
133
|
+
try {
|
|
134
|
+
new URL(url);
|
|
135
|
+
}
|
|
136
|
+
catch (_f) {
|
|
137
|
+
error(`Invalid URL: ${urlInput}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const depthRaw = config.getValue({
|
|
141
|
+
// `--all` is a convenience shortcut for `--depth all`. It wins
|
|
142
|
+
// over a numeric `--depth` so users can combine both.
|
|
143
|
+
cliValue: parameters.options.all === true ? 'all' : parameters.options.depth,
|
|
144
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.depth,
|
|
145
|
+
defaultValue: 0,
|
|
146
|
+
});
|
|
147
|
+
const depth = parseDepth(depthRaw);
|
|
148
|
+
const includeImages = config.getValue({
|
|
149
|
+
cliValue: parameters.options.images === false ? false : undefined,
|
|
150
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.includeImages,
|
|
151
|
+
defaultValue: true,
|
|
152
|
+
});
|
|
153
|
+
const includeSitemap = config.getValue({
|
|
154
|
+
cliValue: parameters.options.sitemap === false ? false : undefined,
|
|
155
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.includeSitemap,
|
|
156
|
+
defaultValue: true,
|
|
157
|
+
});
|
|
158
|
+
const concurrency = Number(config.getValue({
|
|
159
|
+
cliValue: parameters.options.concurrency,
|
|
160
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.concurrency,
|
|
161
|
+
defaultValue: 4,
|
|
162
|
+
}));
|
|
163
|
+
const maxPages = Number(config.getValue({
|
|
164
|
+
cliValue: (_c = parameters.options.maxPages) !== null && _c !== void 0 ? _c : parameters.options['max-pages'],
|
|
165
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.maxPages,
|
|
166
|
+
defaultValue: 200,
|
|
167
|
+
}));
|
|
168
|
+
const timeout = Number(config.getValue({
|
|
169
|
+
cliValue: parameters.options.timeout,
|
|
170
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.timeout,
|
|
171
|
+
defaultValue: 20000,
|
|
172
|
+
}));
|
|
173
|
+
const selector = config.getValue({
|
|
174
|
+
cliValue: parameters.options.selector,
|
|
175
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.selector,
|
|
176
|
+
});
|
|
177
|
+
// `--render` and `--prune` default ON — the common case is a
|
|
178
|
+
// full SPA-aware knowledge-base crawl that stays in sync on
|
|
179
|
+
// updates. `--no-render` / `--no-prune` opt out explicitly.
|
|
180
|
+
const renderJs = config.getValue({
|
|
181
|
+
cliValue: parameters.options.render === false ? false : undefined,
|
|
182
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.renderJs,
|
|
183
|
+
defaultValue: true,
|
|
184
|
+
});
|
|
185
|
+
const installBrowser = parameters.options['install-browser'] === true || parameters.options.installBrowser === true;
|
|
186
|
+
const pruneOrphans = config.getValue({
|
|
187
|
+
cliValue: parameters.options.prune === false ? false : undefined,
|
|
188
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.prune,
|
|
189
|
+
defaultValue: true,
|
|
190
|
+
});
|
|
191
|
+
const outDir = (0, path_1.resolve)(config.getValue({
|
|
192
|
+
cliValue: (_d = parameters.options.out) !== null && _d !== void 0 ? _d : parameters.options.output,
|
|
193
|
+
configValue: commandConfig === null || commandConfig === void 0 ? void 0 : commandConfig.out,
|
|
194
|
+
defaultValue: filesystem.cwd(),
|
|
195
|
+
}) || filesystem.cwd());
|
|
196
|
+
const noConfirm = config.getNoConfirm({
|
|
197
|
+
cliValue: parameters.options.noConfirm,
|
|
198
|
+
commandConfig,
|
|
199
|
+
config: ltConfig,
|
|
200
|
+
parentConfig: (_e = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _e === void 0 ? void 0 : _e.tools,
|
|
201
|
+
});
|
|
202
|
+
info('');
|
|
203
|
+
info(`Crawling: ${url}`);
|
|
204
|
+
info(`Output: ${outDir}`);
|
|
205
|
+
info(`Depth: ${depth === 'all' ? 'all (bounded by --max-pages)' : depth}`);
|
|
206
|
+
info(`Sitemap: ${includeSitemap ? 'yes' : 'no'}`);
|
|
207
|
+
info(`Images: ${includeImages ? 'yes' : 'no'}`);
|
|
208
|
+
info(`Parallel: ${concurrency}`);
|
|
209
|
+
info(`Max: ${maxPages} pages`);
|
|
210
|
+
info(`Render: ${renderJs ? 'yes (headless browser)' : 'no (raw HTTP)'}`);
|
|
211
|
+
info(`Prune: ${pruneOrphans ? 'yes (remove orphaned pages/images)' : 'no'}`);
|
|
212
|
+
if (selector)
|
|
213
|
+
info(`Selector: ${selector}`);
|
|
214
|
+
info('');
|
|
215
|
+
if (!noConfirm && !(yield confirm('Start crawl?'))) {
|
|
216
|
+
return 'crawl cancelled';
|
|
217
|
+
}
|
|
218
|
+
const spinner = spin('Crawling...');
|
|
219
|
+
const result = yield (0, crawler_1.crawlSite)({
|
|
220
|
+
autoInstallBrowser: installBrowser,
|
|
221
|
+
concurrency,
|
|
222
|
+
depth,
|
|
223
|
+
includeImages,
|
|
224
|
+
includeSitemap,
|
|
225
|
+
maxPages,
|
|
226
|
+
onLog: (msg) => {
|
|
227
|
+
spinner.text = msg;
|
|
228
|
+
},
|
|
229
|
+
outDir,
|
|
230
|
+
prune: pruneOrphans,
|
|
231
|
+
renderJs,
|
|
232
|
+
selector,
|
|
233
|
+
timeout,
|
|
234
|
+
url,
|
|
235
|
+
}).catch((err) => {
|
|
236
|
+
spinner.fail('Crawl failed');
|
|
237
|
+
error(err.message);
|
|
238
|
+
return null;
|
|
239
|
+
});
|
|
240
|
+
if (!result) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
spinner.succeed(`Crawl complete: ${result.pages.length} page(s)`);
|
|
244
|
+
info('');
|
|
245
|
+
if (result.indexFile) {
|
|
246
|
+
success(`Overview: ${result.indexFile}`);
|
|
247
|
+
}
|
|
248
|
+
for (const page of result.pages.slice(0, 10)) {
|
|
249
|
+
info(` - ${page.relativePath} (${page.url})`);
|
|
250
|
+
}
|
|
251
|
+
if (result.pages.length > 10) {
|
|
252
|
+
info(` ... and ${result.pages.length - 10} more`);
|
|
253
|
+
}
|
|
254
|
+
if (result.pruned.length > 0) {
|
|
255
|
+
info(`Pruned ${result.pruned.length} orphaned file(s)`);
|
|
256
|
+
for (const path of result.pruned.slice(0, 5)) {
|
|
257
|
+
info(` - ${path}`);
|
|
258
|
+
}
|
|
259
|
+
if (result.pruned.length > 5)
|
|
260
|
+
info(` ... and ${result.pruned.length - 5} more`);
|
|
261
|
+
}
|
|
262
|
+
if (result.skipped.length > 0) {
|
|
263
|
+
warning(`Skipped ${result.skipped.length} URL(s) (non-HTML or foreign origin)`);
|
|
264
|
+
}
|
|
265
|
+
if (result.errors.length > 0) {
|
|
266
|
+
warning(`${result.errors.length} error(s):`);
|
|
267
|
+
for (const err of result.errors.slice(0, 5)) {
|
|
268
|
+
warning(` - ${err.url}: ${err.reason}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!toolbox.parameters.options.fromGluegunMenu) {
|
|
272
|
+
process.exit();
|
|
273
|
+
}
|
|
274
|
+
return `crawled ${result.pages.length} pages`;
|
|
275
|
+
}),
|
|
276
|
+
};
|
|
277
|
+
function normalizeSeedUrl(raw) {
|
|
278
|
+
const trimmed = raw.trim();
|
|
279
|
+
if (/^https?:\/\//i.test(trimmed))
|
|
280
|
+
return trimmed;
|
|
281
|
+
return `https://${trimmed}`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Parse the --depth parameter. Accepts positive integers, the string
|
|
285
|
+
* "all", and negative values (treated as "all"). Invalid values fall
|
|
286
|
+
* back to `0` so the crawl still runs against the seed URL.
|
|
287
|
+
*/
|
|
288
|
+
function parseDepth(raw) {
|
|
289
|
+
if (raw === undefined || raw === null)
|
|
290
|
+
return 0;
|
|
291
|
+
if (typeof raw === 'string') {
|
|
292
|
+
const normalized = raw.trim().toLowerCase();
|
|
293
|
+
if (normalized === 'all' || normalized === '-1')
|
|
294
|
+
return 'all';
|
|
295
|
+
const n = Number(normalized);
|
|
296
|
+
if (!Number.isFinite(n))
|
|
297
|
+
return 0;
|
|
298
|
+
return n < 0 ? 'all' : Math.floor(n);
|
|
299
|
+
}
|
|
300
|
+
if (typeof raw === 'number') {
|
|
301
|
+
if (!Number.isFinite(raw))
|
|
302
|
+
return 'all';
|
|
303
|
+
return raw < 0 ? 'all' : Math.floor(raw);
|
|
304
|
+
}
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
exports.default = NewCommand;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./vendor-runtime-deps.schema.json",
|
|
3
3
|
"description": "Upstream @lenne.tech/nest-server devDependencies that are actually needed at runtime in a consumer project. When vendoring, these entries are promoted from upstream devDependencies into the project's dependencies so production runtime has them available.",
|
|
4
|
-
"runtimeHelpers": [
|
|
5
|
-
"find-file-up"
|
|
6
|
-
]
|
|
4
|
+
"runtimeHelpers": ["find-file-up"]
|
|
7
5
|
}
|
|
8
|
-
</content>
|
|
9
|
-
</invoke>
|
|
@@ -55,6 +55,50 @@ class ApiMode {
|
|
|
55
55
|
this.filesystem.remove((0, path_1.join)(projectPath, 'scripts', 'strip-api-mode-markers.mjs'));
|
|
56
56
|
// Remove strip-markers script from package.json
|
|
57
57
|
this.removeScriptFromPackageJson(projectPath, 'strip-markers');
|
|
58
|
+
// NOTE: auto-format of the stripped files happens separately in
|
|
59
|
+
// `formatProject()`, which MUST be called by the caller AFTER
|
|
60
|
+
// `pnpm install`. At this point the project's formatter (oxfmt) is
|
|
61
|
+
// not yet on disk, so running it here would silently no-op.
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run the project's `format` (or `format:fix`) npm script, if it exists.
|
|
66
|
+
* Call this AFTER the project's dependencies have been installed —
|
|
67
|
+
* otherwise the formatter (e.g. oxfmt) isn't available yet and the
|
|
68
|
+
* pass silently no-ops.
|
|
69
|
+
*
|
|
70
|
+
* Used after region stripping to normalize whitespace artifacts the
|
|
71
|
+
* formatter would otherwise flag in `format:check` (e.g. collapsing
|
|
72
|
+
* `providers: [\n X,\n]` to `providers: [X]` once graphql items were
|
|
73
|
+
* removed). Failures are non-fatal so a misbehaving formatter never
|
|
74
|
+
* blocks init.
|
|
75
|
+
*/
|
|
76
|
+
formatProject(projectPath) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
var _a, _b, _c;
|
|
79
|
+
const pkgPath = (0, path_1.join)(projectPath, 'package.json');
|
|
80
|
+
const pkgRaw = this.filesystem.read(pkgPath);
|
|
81
|
+
if (!pkgRaw)
|
|
82
|
+
return;
|
|
83
|
+
let pkg;
|
|
84
|
+
try {
|
|
85
|
+
pkg = JSON.parse(pkgRaw);
|
|
86
|
+
}
|
|
87
|
+
catch (_d) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const scripts = (_a = pkg.scripts) !== null && _a !== void 0 ? _a : {};
|
|
91
|
+
const formatScript = scripts.format ? 'format' : scripts['format:fix'] ? 'format:fix' : null;
|
|
92
|
+
if (!formatScript)
|
|
93
|
+
return;
|
|
94
|
+
const { pm, system } = this.toolbox;
|
|
95
|
+
const runner = (_c = (_b = pm === null || pm === void 0 ? void 0 : pm.run) === null || _b === void 0 ? void 0 : _b.call(pm, formatScript, pm.detect(projectPath))) !== null && _c !== void 0 ? _c : `pnpm run ${formatScript}`;
|
|
96
|
+
try {
|
|
97
|
+
yield system.run(`cd "${projectPath}" && ${runner}`);
|
|
98
|
+
}
|
|
99
|
+
catch (_e) {
|
|
100
|
+
// Non-fatal: the user can run format manually if this misbehaves.
|
|
101
|
+
}
|
|
58
102
|
});
|
|
59
103
|
}
|
|
60
104
|
/**
|
|
@@ -143,13 +187,77 @@ class ApiMode {
|
|
|
143
187
|
if (!content) {
|
|
144
188
|
continue;
|
|
145
189
|
}
|
|
146
|
-
|
|
190
|
+
// Special-case config.env.ts in REST mode: simply deleting the
|
|
191
|
+
// `graphQl: { … }` property is not enough — `CoreModule.forRoot`
|
|
192
|
+
// treats `graphQl === undefined` as enabled and tries to build
|
|
193
|
+
// the GraphQL schema anyway (which then fails on core models
|
|
194
|
+
// like CoreHealthCheckResult that reference the JSON scalar).
|
|
195
|
+
// Replace each stripped `// #region graphql … // #endregion
|
|
196
|
+
// graphql` block with an explicit `graphQl: false,` so GraphQL
|
|
197
|
+
// is cleanly disabled.
|
|
198
|
+
let processed;
|
|
199
|
+
if (removeMarker === 'graphql' && file.endsWith('/config.env.ts')) {
|
|
200
|
+
processed = this.replaceGraphqlRegionsWithDisabled(content, keepMarker);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
processed = this.processFileRegions(content, removeMarker, keepMarker);
|
|
204
|
+
}
|
|
147
205
|
if (processed !== content) {
|
|
148
206
|
this.filesystem.write(file, processed);
|
|
149
207
|
}
|
|
150
208
|
}
|
|
151
209
|
}
|
|
152
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Like `processFileRegions` with removeMarker='graphql', but a
|
|
213
|
+
* stripped region that contains a `graphQl:` property assignment is
|
|
214
|
+
* replaced with `graphQl: false,` (at the region's indent) instead of
|
|
215
|
+
* being deleted outright. Other graphql-regions (e.g. wrapping
|
|
216
|
+
* `execAfterInit: 'pnpm run docs:bootstrap'`) are deleted as usual.
|
|
217
|
+
*
|
|
218
|
+
* Rationale: `CoreModule.forRoot` treats `options.graphQl === undefined`
|
|
219
|
+
* as enabled, so dropping the property silently keeps GraphQL active
|
|
220
|
+
* and later crashes when the schema references scalars that were
|
|
221
|
+
* purged in REST mode.
|
|
222
|
+
*
|
|
223
|
+
* Preserves keepMarker behaviour (strip marker lines only, keep content).
|
|
224
|
+
*/
|
|
225
|
+
replaceGraphqlRegionsWithDisabled(content, keepMarker) {
|
|
226
|
+
const lines = content.split('\n');
|
|
227
|
+
const result = [];
|
|
228
|
+
let inRegion = false;
|
|
229
|
+
let regionIndent = '';
|
|
230
|
+
let regionBuffer = [];
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (trimmed === '// #region graphql') {
|
|
234
|
+
inRegion = true;
|
|
235
|
+
regionIndent = line.slice(0, line.indexOf('//'));
|
|
236
|
+
regionBuffer = [];
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (trimmed === '// #endregion graphql') {
|
|
240
|
+
inRegion = false;
|
|
241
|
+
// Only emit a replacement if the stripped block actually
|
|
242
|
+
// contained a `graphQl:` assignment.
|
|
243
|
+
if (regionBuffer.some((l) => /\bgraphQl\s*:/.test(l))) {
|
|
244
|
+
result.push(`${regionIndent}graphQl: false,`);
|
|
245
|
+
}
|
|
246
|
+
regionBuffer = [];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inRegion) {
|
|
250
|
+
regionBuffer.push(line);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Keep-marker lines (e.g. `// #region rest`) are dropped; content between them stays.
|
|
254
|
+
if (trimmed === `// #region ${keepMarker}` || trimmed === `// #endregion ${keepMarker}`) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
result.push(line);
|
|
258
|
+
}
|
|
259
|
+
return result.join('\n');
|
|
260
|
+
}
|
|
153
261
|
/**
|
|
154
262
|
* Process region markers in file content
|
|
155
263
|
*/
|
|
@@ -261,8 +369,20 @@ class ApiMode {
|
|
|
261
369
|
}
|
|
262
370
|
catch (_b) {
|
|
263
371
|
// If ts-morph is not available or fails, fall back to regex
|
|
264
|
-
this.modifyConfigEnvForRestFallback(projectPath);
|
|
265
372
|
}
|
|
373
|
+
// Safety net: always run the regex fallback too. ts-morph only
|
|
374
|
+
// traverses direct ObjectLiteralExpression properties, so configs
|
|
375
|
+
// that wrap env-blocks in helper functions (e.g. `local:
|
|
376
|
+
// localConfig(...)`) are skipped silently. The regex is idempotent
|
|
377
|
+
// — if ts-morph already replaced `graphQl: {...}` with
|
|
378
|
+
// `graphQl: false` it's a no-op, but if ts-morph missed a wrapped
|
|
379
|
+
// occurrence the regex catches it. Without this, REST-mode
|
|
380
|
+
// projects built from a starter that lacks explicit
|
|
381
|
+
// `// #region graphql` markers would end up with `graphQl:
|
|
382
|
+
// undefined`, which `CoreModule.forRoot` treats as ENABLED, and
|
|
383
|
+
// the GraphQL schema build crashes on core models that still
|
|
384
|
+
// reference the JSON scalar.
|
|
385
|
+
this.modifyConfigEnvForRestFallback(projectPath);
|
|
266
386
|
});
|
|
267
387
|
}
|
|
268
388
|
/**
|
|
@@ -389,9 +509,7 @@ class ApiMode {
|
|
|
389
509
|
importLineSet.add(j);
|
|
390
510
|
}
|
|
391
511
|
}
|
|
392
|
-
const codeContent = lines
|
|
393
|
-
.map((line, idx) => (importLineSet.has(idx) ? '' : line))
|
|
394
|
-
.join('\n');
|
|
512
|
+
const codeContent = lines.map((line, idx) => (importLineSet.has(idx) ? '' : line)).join('\n');
|
|
395
513
|
// Check each import
|
|
396
514
|
const linesToRemove = new Set();
|
|
397
515
|
for (const imp of importLines) {
|