@lenne.tech/cli 1.32.1 → 1.34.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/codex/codex.js +23 -0
- package/build/commands/codex/plugins.js +103 -0
- package/build/commands/codex/shortcuts.js +110 -0
- package/build/commands/dev/test.js +10 -4
- package/build/commands/dev/up.js +9 -3
- package/build/commands/frontend/nuxt.js +4 -7
- package/build/commands/fullstack/init.js +26 -11
- package/build/commands/fullstack/update.js +13 -0
- package/build/commands/ticket/start.js +6 -4
- package/build/extensions/frontend-helper.js +49 -59
- package/build/extensions/server.js +220 -32
- package/build/lib/check-freshness-hooks.js +53 -0
- package/build/lib/codex-cli.js +56 -0
- package/build/lib/codex-plugin-utils.js +102 -0
- package/build/lib/dev-package-manager.js +89 -0
- package/build/lib/dev-patches.js +4 -1
- package/build/lib/dev-test-session.js +30 -16
- package/build/lib/dev-ticket.js +12 -6
- package/build/lib/heal-check-wrapper.js +59 -0
- package/build/lib/hoist-workspace-pnpm-config.js +105 -92
- package/build/lib/vendor-claude-md.js +7 -2
- package/build/templates/check/check.mjs +512 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.FrontendHelper = void 0;
|
|
13
|
+
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
13
14
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
14
15
|
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
15
16
|
/**
|
|
@@ -140,49 +141,54 @@ class FrontendHelper {
|
|
|
140
141
|
return __awaiter(this, arguments, void 0, function* (dest, options = {}) {
|
|
141
142
|
const { system, templateHelper } = this.toolbox;
|
|
142
143
|
const { branch, copyPath, linkPath, skipInstall } = options;
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
// Run install if not skipped and not a symlink
|
|
164
|
-
if (!skipInstall && result.method !== 'link') {
|
|
165
|
-
try {
|
|
166
|
-
const { pm } = this.toolbox;
|
|
167
|
-
yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
|
|
168
|
-
}
|
|
169
|
-
catch (err) {
|
|
170
|
-
return { method: result.method, path: dest, success: false };
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return { method: result.method, path: result.path, success: true };
|
|
144
|
+
// Source the Nuxt app from the nuxt-base-starter repo (priority:
|
|
145
|
+
// link > copy > clone), mirroring the backend (nest-server-starter)
|
|
146
|
+
// flow. The default — no link/copy/branch — clones the repo's default
|
|
147
|
+
// branch, so a fresh init always integrates the CURRENT GitHub template.
|
|
148
|
+
//
|
|
149
|
+
// This deliberately replaces the former `create-nuxt-base@latest` npx
|
|
150
|
+
// path: that package is marked `private` and is no longer published
|
|
151
|
+
// (nuxt-base-starter commit b534af2 — "distribute via lt CLI fullstack
|
|
152
|
+
// template"), so `@latest` was frozen at an outdated copy and the
|
|
153
|
+
// bundled template drifted from GitHub. Cloning the repo directly is
|
|
154
|
+
// the same mechanism the backend already uses and removes that drift.
|
|
155
|
+
const result = yield templateHelper.setup(dest, {
|
|
156
|
+
branch,
|
|
157
|
+
copyPath,
|
|
158
|
+
isNuxt: true,
|
|
159
|
+
linkPath,
|
|
160
|
+
repoUrl: linkPath || copyPath ? undefined : 'https://github.com/lenneTech/nuxt-base-starter.git',
|
|
161
|
+
});
|
|
162
|
+
if (!result.success) {
|
|
163
|
+
return { method: result.method, path: result.path, success: false };
|
|
174
164
|
}
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
165
|
+
// After a clone, flatten the wrapper layout so `dest` IS the Nuxt app
|
|
166
|
+
// (the cloned root carries the scaffolder; the app lives in
|
|
167
|
+
// `nuxt-base-template/` — see flattenNuxtBaseTemplate). Skip on link
|
|
168
|
+
// mode: a symlink points at the user's checkout and must not be torn out.
|
|
169
|
+
if (result.method === 'clone') {
|
|
170
|
+
yield this.flattenNuxtBaseTemplate(dest);
|
|
171
|
+
}
|
|
172
|
+
// Normalize the sub-project package name to a stable, valid workspace
|
|
173
|
+
// name ("app"). The template ships as `nuxt-base-template`; the former
|
|
174
|
+
// create-nuxt-base path renamed it the same way. Skip link mode — a
|
|
175
|
+
// symlinked checkout is shared with upstream and must not be mutated.
|
|
176
|
+
if (result.method !== 'link') {
|
|
180
177
|
yield this.fixPackageName(dest);
|
|
181
|
-
return { method: 'npx', path: dest, success: true };
|
|
182
178
|
}
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
// Run install unless skipped or a symlink. Fullstack init installs at
|
|
180
|
+
// the monorepo level (skipInstall: true); standalone `lt frontend nuxt`
|
|
181
|
+
// installs here (create-nuxt-base used to handle that).
|
|
182
|
+
if (!skipInstall && result.method !== 'link') {
|
|
183
|
+
try {
|
|
184
|
+
const { pm } = this.toolbox;
|
|
185
|
+
yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
|
|
186
|
+
}
|
|
187
|
+
catch (_a) {
|
|
188
|
+
return { method: result.method, path: dest, success: false };
|
|
189
|
+
}
|
|
185
190
|
}
|
|
191
|
+
return { method: result.method, path: result.path, success: true };
|
|
186
192
|
});
|
|
187
193
|
}
|
|
188
194
|
/**
|
|
@@ -316,7 +322,6 @@ class FrontendHelper {
|
|
|
316
322
|
*/
|
|
317
323
|
convertAppToNpmMode(options) {
|
|
318
324
|
return __awaiter(this, void 0, void 0, function* () {
|
|
319
|
-
var _a;
|
|
320
325
|
const { dest, targetVersion } = options;
|
|
321
326
|
const { filesystem } = this.toolbox;
|
|
322
327
|
const path = require('node:path');
|
|
@@ -375,13 +380,7 @@ class FrontendHelper {
|
|
|
375
380
|
// Remove vendor-specific scripts
|
|
376
381
|
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
377
382
|
const scripts = pkg.scripts;
|
|
378
|
-
|
|
379
|
-
// Unhook freshness from check/check:fix/check:naf
|
|
380
|
-
for (const scriptName of ['check', 'check:fix', 'check:naf']) {
|
|
381
|
-
if ((_a = scripts[scriptName]) === null || _a === void 0 ? void 0 : _a.includes('check:vendor-freshness')) {
|
|
382
|
-
scripts[scriptName] = scripts[scriptName].replace(/pnpm run check:vendor-freshness && /g, '');
|
|
383
|
-
}
|
|
384
|
-
}
|
|
383
|
+
(0, check_freshness_hooks_1.unhookCheckFreshness)(scripts);
|
|
385
384
|
}
|
|
386
385
|
filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
|
|
387
386
|
}
|
|
@@ -576,18 +575,9 @@ class FrontendHelper {
|
|
|
576
575
|
'setTimeout(function(){process.exit(0)},5000)',
|
|
577
576
|
'"',
|
|
578
577
|
].join('');
|
|
579
|
-
// Hook freshness into check scripts
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (!existing)
|
|
583
|
-
return;
|
|
584
|
-
if (existing.includes('check:vendor-freshness'))
|
|
585
|
-
return;
|
|
586
|
-
scripts[scriptName] = `pnpm run check:vendor-freshness && ${existing}`;
|
|
587
|
-
};
|
|
588
|
-
hookFreshness('check');
|
|
589
|
-
hookFreshness('check:fix');
|
|
590
|
-
hookFreshness('check:naf');
|
|
578
|
+
// Hook freshness into the check scripts (targets check:raw, never the
|
|
579
|
+
// check.mjs wrapper). See the shared lib helper.
|
|
580
|
+
(0, check_freshness_hooks_1.hookCheckFreshness)(scripts);
|
|
591
581
|
}
|
|
592
582
|
// Sort dependency maps alphabetically so merged-in entries
|
|
593
583
|
// (e.g. upstream `@nuxt/kit`) end up in the expected position
|
|
@@ -46,6 +46,7 @@ exports.Server = void 0;
|
|
|
46
46
|
const crypto = __importStar(require("crypto"));
|
|
47
47
|
const path_1 = require("path");
|
|
48
48
|
const ts = __importStar(require("typescript"));
|
|
49
|
+
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
49
50
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
50
51
|
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
51
52
|
/**
|
|
@@ -1005,6 +1006,20 @@ class Server {
|
|
|
1005
1006
|
// Best-effort — if we can't read upstream pkg, the starter's own
|
|
1006
1007
|
// deps should still cover most of the framework's needs.
|
|
1007
1008
|
}
|
|
1009
|
+
// Snapshot the upstream lockfile too. The shallow clone ships
|
|
1010
|
+
// pnpm-lock.yaml, which pins EXACT versions for every transitive
|
|
1011
|
+
// dependency the vendored core imports directly (cron, jose,
|
|
1012
|
+
// fs-capacitor, graphql-ws, ws, …) but that nest-server never declares
|
|
1013
|
+
// as a direct dep. Step 5b below resolves the core's import closure
|
|
1014
|
+
// against this so those packages become direct project deps.
|
|
1015
|
+
let upstreamLockRaw = '';
|
|
1016
|
+
try {
|
|
1017
|
+
upstreamLockRaw = filesystem.read(`${tmpClone}/pnpm-lock.yaml`) || '';
|
|
1018
|
+
}
|
|
1019
|
+
catch (_b) {
|
|
1020
|
+
// Best-effort — without the lockfile, the closure step falls back to
|
|
1021
|
+
// the upstream package.json ranges (still better than nothing).
|
|
1022
|
+
}
|
|
1008
1023
|
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
|
|
1009
1024
|
// The nest-server CLAUDE.md contains framework-specific instructions that
|
|
1010
1025
|
// Claude Code needs to work correctly with the vendored source (API conventions,
|
|
@@ -1017,7 +1032,7 @@ class Server {
|
|
|
1017
1032
|
upstreamClaudeMd = claudeMdContent;
|
|
1018
1033
|
}
|
|
1019
1034
|
}
|
|
1020
|
-
catch (
|
|
1035
|
+
catch (_c) {
|
|
1021
1036
|
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
|
|
1022
1037
|
}
|
|
1023
1038
|
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
|
|
@@ -1026,7 +1041,7 @@ class Server {
|
|
|
1026
1041
|
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
|
|
1027
1042
|
upstreamCommit = (sha || '').trim();
|
|
1028
1043
|
}
|
|
1029
|
-
catch (
|
|
1044
|
+
catch (_d) {
|
|
1030
1045
|
// Non-fatal — VENDOR.md will just show an empty SHA.
|
|
1031
1046
|
}
|
|
1032
1047
|
try {
|
|
@@ -1309,7 +1324,7 @@ class Server {
|
|
|
1309
1324
|
}
|
|
1310
1325
|
}
|
|
1311
1326
|
}
|
|
1312
|
-
catch (
|
|
1327
|
+
catch (_e) {
|
|
1313
1328
|
// skip unreadable file
|
|
1314
1329
|
}
|
|
1315
1330
|
}
|
|
@@ -1419,6 +1434,25 @@ class Server {
|
|
|
1419
1434
|
}
|
|
1420
1435
|
}
|
|
1421
1436
|
}
|
|
1437
|
+
// ── 5b. Backfill the vendored core's transitive import closure ──
|
|
1438
|
+
//
|
|
1439
|
+
// The merge above only covers nest-server's *declared* dependencies.
|
|
1440
|
+
// The vendored core ALSO imports packages that used to arrive
|
|
1441
|
+
// transitively through the @lenne.tech/nest-server npm dep
|
|
1442
|
+
// (cron←@nestjs/schedule, jose←better-auth, fs-capacitor←graphql-upload,
|
|
1443
|
+
// graphql-ws / ws). Once vendored, those imports are direct, so under
|
|
1444
|
+
// pnpm's isolated node_modules they are unresolvable unless declared
|
|
1445
|
+
// as direct deps → TS2307 at build time. Scan the core's bare imports
|
|
1446
|
+
// and add any missing package (+ matching @types) at the EXACT version
|
|
1447
|
+
// pinned by the upstream lockfile.
|
|
1448
|
+
this.gatherVendorCoreImportClosure({
|
|
1449
|
+
coreDir,
|
|
1450
|
+
deps,
|
|
1451
|
+
devDeps,
|
|
1452
|
+
lockRaw: upstreamLockRaw,
|
|
1453
|
+
upstreamDeps,
|
|
1454
|
+
upstreamDevDeps,
|
|
1455
|
+
});
|
|
1422
1456
|
// Add a script to run the local bin/migrate.js. The starter's
|
|
1423
1457
|
// existing migrate:* scripts are already correct for npm mode; we
|
|
1424
1458
|
// need them pointing at the local bin + local ts-compiler.
|
|
@@ -1458,26 +1492,9 @@ class Server {
|
|
|
1458
1492
|
'"',
|
|
1459
1493
|
].join('');
|
|
1460
1494
|
// Hook vendor-freshness as the first step of check / check:fix /
|
|
1461
|
-
// check:naf
|
|
1462
|
-
//
|
|
1463
|
-
|
|
1464
|
-
const existing = scripts[scriptName];
|
|
1465
|
-
if (!existing)
|
|
1466
|
-
return;
|
|
1467
|
-
if (existing.includes('check:vendor-freshness'))
|
|
1468
|
-
return;
|
|
1469
|
-
const installPrefix = 'pnpm install && ';
|
|
1470
|
-
if (existing.startsWith(installPrefix)) {
|
|
1471
|
-
scripts[scriptName] =
|
|
1472
|
-
`${installPrefix}pnpm run check:vendor-freshness && ${existing.slice(installPrefix.length)}`;
|
|
1473
|
-
}
|
|
1474
|
-
else {
|
|
1475
|
-
scripts[scriptName] = `pnpm run check:vendor-freshness && ${existing}`;
|
|
1476
|
-
}
|
|
1477
|
-
};
|
|
1478
|
-
hookFreshness('check');
|
|
1479
|
-
hookFreshness('check:fix');
|
|
1480
|
-
hookFreshness('check:naf');
|
|
1495
|
+
// check:naf (non-blocking; surfaces a warning at the top of the log).
|
|
1496
|
+
// Targets check:raw — never the check.mjs wrapper. See lib helper.
|
|
1497
|
+
(0, check_freshness_hooks_1.hookCheckFreshness)(scripts);
|
|
1481
1498
|
}
|
|
1482
1499
|
filesystem.write(pkgPath, pkg, { jsonIndent: 2 });
|
|
1483
1500
|
}
|
|
@@ -1708,7 +1725,13 @@ class Server {
|
|
|
1708
1725
|
// Scan all consumer files for stale bare-specifier imports that the
|
|
1709
1726
|
// codemod should have rewritten. A single miss causes a compile error,
|
|
1710
1727
|
// so catching it here with a clear message saves the user debugging time.
|
|
1711
|
-
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server'
|
|
1728
|
+
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server',
|
|
1729
|
+
// Match only real import/export/require specifiers in single/double
|
|
1730
|
+
// quotes — NOT comment references like
|
|
1731
|
+
// `node_modules/@lenne.tech/nest-server/...` (backticks, no keyword),
|
|
1732
|
+
// which the codemod legitimately leaves untouched. A naive substring
|
|
1733
|
+
// match flagged config.env.ts's JSDoc path as a false positive.
|
|
1734
|
+
/(?:from|import|export|require)\s*\(?\s*['"]@lenne\.tech\/nest-server(?:\/[^'"]*)?['"]/);
|
|
1712
1735
|
if (staleImports.length > 0) {
|
|
1713
1736
|
const { print } = this.toolbox;
|
|
1714
1737
|
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
@@ -2218,17 +2241,11 @@ class Server {
|
|
|
2218
2241
|
// (they'll come back via node_modules when nest-server is installed)
|
|
2219
2242
|
// We only remove deps that are ALSO in nest-server's package.json.
|
|
2220
2243
|
// For safety, we don't remove any dep the consumer might use directly.
|
|
2221
|
-
// Remove vendor-specific scripts
|
|
2244
|
+
// Remove vendor-specific scripts + unhook vendor-freshness (see lib helper).
|
|
2222
2245
|
const scripts = pkg.scripts || {};
|
|
2223
|
-
delete scripts['check:vendor-freshness'];
|
|
2224
2246
|
delete scripts['vendor:sync'];
|
|
2225
2247
|
delete scripts['vendor:propose-upstream'];
|
|
2226
|
-
|
|
2227
|
-
for (const key of ['check', 'check:fix', 'check:naf']) {
|
|
2228
|
-
if (scripts[key] && typeof scripts[key] === 'string') {
|
|
2229
|
-
scripts[key] = scripts[key].replace(/pnpm run check:vendor-freshness && /g, '');
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2248
|
+
(0, check_freshness_hooks_1.unhookCheckFreshness)(scripts);
|
|
2232
2249
|
// Restore migrate scripts to npm-mode paths
|
|
2233
2250
|
const migrateCompiler = 'ts:./node_modules/@lenne.tech/nest-server/dist/core/modules/migrate/helpers/ts-compiler.js';
|
|
2234
2251
|
const migrateStore = '--store ./migrations-utils/migrate.js --migrations-dir ./migrations';
|
|
@@ -2363,6 +2380,177 @@ class Server {
|
|
|
2363
2380
|
}
|
|
2364
2381
|
return stale;
|
|
2365
2382
|
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Backfills the vendored core's transitive import closure into the
|
|
2385
|
+
* project's dependencies. After vendoring, `src/core/**` imports packages
|
|
2386
|
+
* that previously arrived transitively via the `@lenne.tech/nest-server`
|
|
2387
|
+
* npm dependency (e.g. `cron`, `jose`, `fs-capacitor`, `graphql-ws`,
|
|
2388
|
+
* `ws`). nest-server never declares these as direct deps, so the
|
|
2389
|
+
* upstream-deps merge misses them — yet the vendored code imports them
|
|
2390
|
+
* directly, which pnpm's isolated node_modules cannot resolve → TS2307 at
|
|
2391
|
+
* build time.
|
|
2392
|
+
*
|
|
2393
|
+
* Scans every `.ts` file under the vendored core for bare import
|
|
2394
|
+
* specifiers, drops Node builtins and already-declared packages, and adds
|
|
2395
|
+
* the remainder (plus any matching `@types/*`) using EXACT versions read
|
|
2396
|
+
* from the upstream `pnpm-lock.yaml`. Mutates `deps`/`devDeps` in place.
|
|
2397
|
+
*/
|
|
2398
|
+
gatherVendorCoreImportClosure(options) {
|
|
2399
|
+
const { coreDir, deps, devDeps, lockRaw, upstreamDeps, upstreamDevDeps } = options;
|
|
2400
|
+
const { print } = this.toolbox;
|
|
2401
|
+
const builtins = new Set(require('module').builtinModules);
|
|
2402
|
+
// Compare two dotted versions numerically (major.minor.patch) — used to
|
|
2403
|
+
// pick the highest when the lockfile pins multiple copies (e.g. ws 7/8).
|
|
2404
|
+
const compareVersions = (a, b) => {
|
|
2405
|
+
const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
2406
|
+
const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
2407
|
+
for (let i = 0; i < 3; i++) {
|
|
2408
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) {
|
|
2409
|
+
return (pa[i] || 0) - (pb[i] || 0);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
return 0;
|
|
2413
|
+
};
|
|
2414
|
+
// Parse `<name>@<version>` keys from the lockfile's `packages:` section
|
|
2415
|
+
// into a name→version map (highest version wins). pnpm-lock v9 lists
|
|
2416
|
+
// every package (direct + transitive) there with a clean 2-space indent.
|
|
2417
|
+
const parseLockVersions = (raw) => {
|
|
2418
|
+
const map = {};
|
|
2419
|
+
let inPackages = false;
|
|
2420
|
+
for (const line of raw.split('\n')) {
|
|
2421
|
+
if (/^packages:\s*$/.test(line)) {
|
|
2422
|
+
inPackages = true;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (inPackages && /^\S/.test(line)) {
|
|
2426
|
+
break; // reached the next top-level section
|
|
2427
|
+
}
|
|
2428
|
+
if (!inPackages) {
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
const matched = line.match(/^ {2}(['"]?)(.+?)\1:\s*$/);
|
|
2432
|
+
if (!matched) {
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
let key = matched[2] || '';
|
|
2436
|
+
const paren = key.indexOf('('); // strip pnpm peer suffix
|
|
2437
|
+
if (paren >= 0) {
|
|
2438
|
+
key = key.slice(0, paren);
|
|
2439
|
+
}
|
|
2440
|
+
const at = key.lastIndexOf('@');
|
|
2441
|
+
if (at <= 0) {
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
const name = key.slice(0, at);
|
|
2445
|
+
const version = key.slice(at + 1);
|
|
2446
|
+
if (!/^\d/.test(version)) {
|
|
2447
|
+
continue; // ranges / non-versions
|
|
2448
|
+
}
|
|
2449
|
+
const current = map[name];
|
|
2450
|
+
if (!current || compareVersions(version, current) > 0) {
|
|
2451
|
+
map[name] = version;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return map;
|
|
2455
|
+
};
|
|
2456
|
+
// Reduce an import specifier to its package name (or null for relative
|
|
2457
|
+
// paths and Node builtins, which need no dependency entry).
|
|
2458
|
+
const packageNameOf = (spec) => {
|
|
2459
|
+
if (!spec || spec.startsWith('.') || spec.startsWith('/')) {
|
|
2460
|
+
return null;
|
|
2461
|
+
}
|
|
2462
|
+
const bare = spec.startsWith('node:') ? spec.slice('node:'.length) : spec;
|
|
2463
|
+
const parts = bare.split('/');
|
|
2464
|
+
const name = bare.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] || '';
|
|
2465
|
+
// Drop Node builtins, the framework package itself (never a dependency
|
|
2466
|
+
// of its own vendored code), and anything that is not a syntactically
|
|
2467
|
+
// valid npm package name (defensive guard).
|
|
2468
|
+
if (!name || builtins.has(name) || name === '@lenne.tech/nest-server') {
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
return /^(?:@[\w.~-]+\/)?[\w.~-]+$/.test(name) ? name : null;
|
|
2472
|
+
};
|
|
2473
|
+
// Collect every bare specifier imported by the vendored core. Parse the
|
|
2474
|
+
// TypeScript AST (ts-morph) rather than scanning text, so that prose in
|
|
2475
|
+
// comments/strings — a JSDoc `import … from '@lenne.tech/nest-server'`
|
|
2476
|
+
// example, or an English `… distinguishable from "never set"` — is never
|
|
2477
|
+
// mistaken for a real import (which a naive regex did).
|
|
2478
|
+
const imported = new Set();
|
|
2479
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
2480
|
+
const scanProject = new Project({
|
|
2481
|
+
skipAddingFilesFromTsConfig: true,
|
|
2482
|
+
skipFileDependencyResolution: true,
|
|
2483
|
+
});
|
|
2484
|
+
const sourceFiles = scanProject.addSourceFilesAtPaths(`${coreDir}/**/*.ts`);
|
|
2485
|
+
for (const sourceFile of sourceFiles) {
|
|
2486
|
+
// `import … from 'x'` and `export … from 'x'`
|
|
2487
|
+
for (const decl of [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()]) {
|
|
2488
|
+
const spec = decl.getModuleSpecifierValue();
|
|
2489
|
+
const name = spec ? packageNameOf(spec) : null;
|
|
2490
|
+
if (name) {
|
|
2491
|
+
imported.add(name);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
// `import x = require('x')`
|
|
2495
|
+
for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) {
|
|
2496
|
+
const ref = decl
|
|
2497
|
+
.getModuleReference()
|
|
2498
|
+
.getText()
|
|
2499
|
+
.match(/^require\(\s*['"]([^'"]+)['"]\s*\)$/);
|
|
2500
|
+
const name = ref ? packageNameOf(ref[1] || '') : null;
|
|
2501
|
+
if (name) {
|
|
2502
|
+
imported.add(name);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
// Dynamic `import('x')` / `require('x')` calls
|
|
2506
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2507
|
+
const callee = call.getExpression().getText();
|
|
2508
|
+
if (callee !== 'import' && callee !== 'require') {
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
const arg = call.getArguments()[0];
|
|
2512
|
+
if (arg && arg.getKind() === SyntaxKind.StringLiteral) {
|
|
2513
|
+
const name = packageNameOf(arg.getLiteralText());
|
|
2514
|
+
if (name) {
|
|
2515
|
+
imported.add(name);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
const lockVersions = parseLockVersions(lockRaw);
|
|
2521
|
+
const added = [];
|
|
2522
|
+
const unresolved = [];
|
|
2523
|
+
for (const name of [...imported].sort()) {
|
|
2524
|
+
if (name in deps || name in devDeps) {
|
|
2525
|
+
continue; // already declared by the starter or the upstream merge
|
|
2526
|
+
}
|
|
2527
|
+
const version = lockVersions[name] || upstreamDeps[name] || upstreamDevDeps[name];
|
|
2528
|
+
if (!version) {
|
|
2529
|
+
unresolved.push(name);
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
deps[name] = version;
|
|
2533
|
+
added.push(`${name}@${version}`);
|
|
2534
|
+
// Add a matching @types/* (for packages that ship no own types) so the
|
|
2535
|
+
// vendored core type-checks. Handles scoped names
|
|
2536
|
+
// (@scope/x → @types/scope__x) too.
|
|
2537
|
+
const typesName = name.startsWith('@') ? `@types/${name.slice(1).replace('/', '__')}` : `@types/${name}`;
|
|
2538
|
+
if (!(typesName in deps) && !(typesName in devDeps)) {
|
|
2539
|
+
const typesVersion = lockVersions[typesName] || upstreamDevDeps[typesName];
|
|
2540
|
+
if (typesVersion) {
|
|
2541
|
+
devDeps[typesName] = typesVersion;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
if (added.length > 0) {
|
|
2546
|
+
print.info(` vendored core closure: added ${added.length} transitive dep(s) → ${added.join(', ')}`);
|
|
2547
|
+
}
|
|
2548
|
+
if (unresolved.length > 0) {
|
|
2549
|
+
print.warning(`⚠ ${unresolved.length} vendored-core import(s) could not be resolved to a version ` +
|
|
2550
|
+
`(absent from the upstream lock and package.json): ${unresolved.join(', ')}. ` +
|
|
2551
|
+
'Add them to projects/api/package.json manually if the build fails.');
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2366
2554
|
/**
|
|
2367
2555
|
* Checks whether an import specifier resolves to the vendored core directory.
|
|
2368
2556
|
* Used during vendor→npm import rewriting.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helpers for (un)hooking the vendor-freshness step in a project's `check`
|
|
4
|
+
* scripts during vendor-mode conversion. Extracted so the logic is unit-tested
|
|
5
|
+
* (server.ts + frontend-helper.ts call these instead of inline closures).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.hookCheckFreshness = hookCheckFreshness;
|
|
9
|
+
exports.unhookCheckFreshness = unhookCheckFreshness;
|
|
10
|
+
const WRAPPER_HINT = 'check.mjs';
|
|
11
|
+
const FRESHNESS_STEP = 'pnpm run check:vendor-freshness';
|
|
12
|
+
const INSTALL_PREFIX = 'pnpm install && ';
|
|
13
|
+
/**
|
|
14
|
+
* Prepend the `check:vendor-freshness` step to the relevant check scripts.
|
|
15
|
+
*
|
|
16
|
+
* - Skips a script that already includes the step (idempotent) or that IS the
|
|
17
|
+
* `node scripts/check.mjs` wrapper (the wrapper only orchestrates the real
|
|
18
|
+
* chain — vendor-freshness belongs in that chain).
|
|
19
|
+
* - Targets `check:raw` instead of `check` when the project runs the wrapper,
|
|
20
|
+
* so freshness becomes a step the wrapper executes.
|
|
21
|
+
* - Preserves a leading `pnpm install && ` so install stays first.
|
|
22
|
+
*
|
|
23
|
+
* Assumes the caller has already defined `scripts['check:vendor-freshness']`.
|
|
24
|
+
* Mutates `scripts` in place.
|
|
25
|
+
*/
|
|
26
|
+
function hookCheckFreshness(scripts) {
|
|
27
|
+
const hook = (name) => {
|
|
28
|
+
const existing = scripts[name];
|
|
29
|
+
if (!existing || existing.includes('check:vendor-freshness') || existing.includes(WRAPPER_HINT)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
scripts[name] = existing.startsWith(INSTALL_PREFIX)
|
|
33
|
+
? `${INSTALL_PREFIX}${FRESHNESS_STEP} && ${existing.slice(INSTALL_PREFIX.length)}`
|
|
34
|
+
: `${FRESHNESS_STEP} && ${existing}`;
|
|
35
|
+
};
|
|
36
|
+
// Prefer the raw chain when the project runs the check.mjs wrapper.
|
|
37
|
+
hook(scripts['check:raw'] ? 'check:raw' : 'check');
|
|
38
|
+
hook('check:fix');
|
|
39
|
+
hook('check:naf');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Remove the vendor-freshness step (and its script) from a project's check
|
|
43
|
+
* scripts — the inverse of {@link hookCheckFreshness}, used when converting a
|
|
44
|
+
* project back to npm mode. Mutates `scripts` in place.
|
|
45
|
+
*/
|
|
46
|
+
function unhookCheckFreshness(scripts) {
|
|
47
|
+
delete scripts['check:vendor-freshness'];
|
|
48
|
+
for (const name of ['check', 'check:raw', 'check:fix', 'check:naf']) {
|
|
49
|
+
if (typeof scripts[name] === 'string') {
|
|
50
|
+
scripts[name] = scripts[name].replace(/pnpm run check:vendor-freshness && /g, '');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CODEX_MARKETPLACE_ROOT = void 0;
|
|
4
|
+
exports.findCodexCli = findCodexCli;
|
|
5
|
+
exports.runCodexCommand = runCodexCommand;
|
|
6
|
+
/**
|
|
7
|
+
* Codex CLI utilities.
|
|
8
|
+
*/
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const os_1 = require("os");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
exports.DEFAULT_CODEX_MARKETPLACE_ROOT = process.env.LT_CODEX_MARKETPLACE_ROOT || '/Users/kaihaase/code/lenneTech/codex';
|
|
14
|
+
function findCodexCli() {
|
|
15
|
+
const possiblePaths = [
|
|
16
|
+
(0, path_1.join)((0, os_1.homedir)(), '.local', 'bin', 'codex'),
|
|
17
|
+
(0, path_1.join)((0, os_1.homedir)(), '.codex', 'bin', 'codex'),
|
|
18
|
+
'/usr/local/bin/codex',
|
|
19
|
+
'/opt/homebrew/bin/codex',
|
|
20
|
+
'/usr/bin/codex',
|
|
21
|
+
];
|
|
22
|
+
for (const path of possiblePaths) {
|
|
23
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
24
|
+
return path;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const result = (0, child_process_1.spawnSync)('which', ['codex'], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
29
|
+
const path = (result.stdout || '').trim();
|
|
30
|
+
if (result.status === 0 && path && (0, fs_1.existsSync)(path)) {
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (_a) {
|
|
35
|
+
// Codex CLI not found in PATH.
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function runCodexCommand(cli, args) {
|
|
40
|
+
try {
|
|
41
|
+
const result = (0, child_process_1.spawnSync)(cli, args, {
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
output: result.stdout + result.stderr,
|
|
47
|
+
success: result.status === 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
output: err.message,
|
|
53
|
+
success: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|