@lenne.tech/cli 1.33.0 → 1.35.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 +6 -5
- 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/config/validate.js +0 -4
- package/build/commands/deployment/create.js +218 -213
- package/build/commands/dev/init.js +2 -1
- package/build/commands/dev/status.js +2 -0
- package/build/commands/dev/test.js +12 -6
- package/build/commands/dev/up.js +17 -7
- package/build/commands/frontend/nuxt.js +4 -7
- package/build/commands/fullstack/add-api.js +6 -9
- package/build/commands/fullstack/add-app.js +5 -7
- package/build/commands/fullstack/init.js +34 -20
- package/build/commands/status.js +2 -0
- package/build/commands/ticket/list.js +2 -0
- package/build/commands/ticket/start.js +6 -4
- package/build/extensions/frontend-helper.js +44 -39
- package/build/extensions/server.js +354 -22
- package/build/lib/angular-environments.js +108 -0
- package/build/lib/codex-cli.js +56 -0
- package/build/lib/codex-plugin-utils.js +102 -0
- package/build/lib/dev-identity.js +38 -18
- package/build/lib/dev-package-manager.js +89 -0
- package/build/lib/dev-patches.js +4 -1
- package/build/lib/dev-project.js +20 -7
- package/build/lib/dev-state.js +17 -1
- package/build/lib/dev-test-session.js +61 -19
- package/build/lib/dev-ticket.js +12 -6
- package/build/lib/ensure-root-dockerignore.js +92 -0
- package/build/lib/fs-utils.js +19 -0
- package/build/lib/hoist-workspace-pnpm-config.js +2 -11
- package/build/lib/remove-nested-lockfiles.js +53 -0
- package/build/lib/turboops-config.js +93 -0
- package/build/lib/vendor-claude-md.js +7 -2
- package/build/lib/workspace-integration.js +96 -3
- package/build/templates/deployment/turboops.json.ejs +3 -0
- package/build/templates/vendor-scripts/migrate-store.js +25 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/commands.md +30 -9
- package/docs/lt.config.md +9 -19
- package/package.json +1 -1
- package/build/templates/deployment/.github/workflows/pre-release.yml.ejs +0 -41
- package/build/templates/deployment/.github/workflows/release.yml.ejs +0 -41
- package/build/templates/deployment/.gitlab-ci.yml.ejs +0 -181
- package/build/templates/deployment/Dockerfile.app.ejs +0 -13
- package/build/templates/deployment/Dockerfile.ejs +0 -18
- package/build/templates/deployment/docker-compose.dev.yml.ejs +0 -99
- package/build/templates/deployment/docker-compose.prod.yml.ejs +0 -92
- package/build/templates/deployment/docker-compose.test.yml.ejs +0 -98
- package/build/templates/deployment/scripts/build-push.sh.ejs +0 -20
- package/build/templates/deployment/scripts/deploy.sh.ejs +0 -7
|
@@ -1006,6 +1006,20 @@ class Server {
|
|
|
1006
1006
|
// Best-effort — if we can't read upstream pkg, the starter's own
|
|
1007
1007
|
// deps should still cover most of the framework's needs.
|
|
1008
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
|
+
}
|
|
1009
1023
|
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
|
|
1010
1024
|
// The nest-server CLAUDE.md contains framework-specific instructions that
|
|
1011
1025
|
// Claude Code needs to work correctly with the vendored source (API conventions,
|
|
@@ -1018,7 +1032,7 @@ class Server {
|
|
|
1018
1032
|
upstreamClaudeMd = claudeMdContent;
|
|
1019
1033
|
}
|
|
1020
1034
|
}
|
|
1021
|
-
catch (
|
|
1035
|
+
catch (_c) {
|
|
1022
1036
|
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
|
|
1023
1037
|
}
|
|
1024
1038
|
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
|
|
@@ -1027,7 +1041,7 @@ class Server {
|
|
|
1027
1041
|
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
|
|
1028
1042
|
upstreamCommit = (sha || '').trim();
|
|
1029
1043
|
}
|
|
1030
|
-
catch (
|
|
1044
|
+
catch (_d) {
|
|
1031
1045
|
// Non-fatal — VENDOR.md will just show an empty SHA.
|
|
1032
1046
|
}
|
|
1033
1047
|
try {
|
|
@@ -1310,7 +1324,7 @@ class Server {
|
|
|
1310
1324
|
}
|
|
1311
1325
|
}
|
|
1312
1326
|
}
|
|
1313
|
-
catch (
|
|
1327
|
+
catch (_e) {
|
|
1314
1328
|
// skip unreadable file
|
|
1315
1329
|
}
|
|
1316
1330
|
}
|
|
@@ -1319,25 +1333,74 @@ class Server {
|
|
|
1319
1333
|
// above works indirectly (via `bin/migrate.js` loading ts-node first),
|
|
1320
1334
|
// but it's fragile: if somebody invokes migrate.js standalone, `require
|
|
1321
1335
|
// ('../src/core')` would crash because Node cannot load TypeScript.
|
|
1322
|
-
//
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1336
|
+
//
|
|
1337
|
+
// Two things this file must get right, both learned the hard way:
|
|
1338
|
+
// 1. ts-node is registered ONLY when the core next to it is still
|
|
1339
|
+
// TypeScript. In the production image everything is compiled and
|
|
1340
|
+
// ts-node is pruned, so an unconditional `require('./ts-compiler')`
|
|
1341
|
+
// would throw MODULE_NOT_FOUND and — under `set -e` in the
|
|
1342
|
+
// entrypoint — take the whole container down before the server starts.
|
|
1343
|
+
// 2. The Mongo URI comes from resolveMongoUri(), which falls back to
|
|
1344
|
+
// NSC__MONGOOSE__URI when config.env.ts is not loadable (Docker).
|
|
1345
|
+
// Reading `config.default.mongoose.uri` directly crashes there.
|
|
1325
1346
|
const migrateJsPath = `${dest}/migrations-utils/migrate.js`;
|
|
1326
|
-
filesystem.
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
'
|
|
1339
|
-
|
|
1340
|
-
|
|
1347
|
+
const migrateStoreTemplate = filesystem.read(path.join(__dirname, '..', 'templates', 'vendor-scripts', 'migrate-store.js'));
|
|
1348
|
+
if (!migrateStoreTemplate) {
|
|
1349
|
+
throw new Error('CLI build is missing templates/vendor-scripts/migrate-store.js — cannot generate migrations-utils/migrate.js.');
|
|
1350
|
+
}
|
|
1351
|
+
filesystem.write(migrateJsPath, migrateStoreTemplate);
|
|
1352
|
+
// The generated migrate.js does `require('./mongo-uri')` (resolveMongoUri).
|
|
1353
|
+
// Unlike ts-compiler.js (written below), that file is NOT generated here — it
|
|
1354
|
+
// ships with nest-server-starter (the vendor clone base) and is copied into
|
|
1355
|
+
// dist/ by `copy:migrations`. Assert it so a non-standard/older starter that
|
|
1356
|
+
// lacks it fails loudly HERE instead of silently skipping every migration at
|
|
1357
|
+
// container start (the entrypoint's `migrate up` is best-effort under `set -e`).
|
|
1358
|
+
if (!filesystem.exists(`${dest}/migrations-utils/mongo-uri.js`)) {
|
|
1359
|
+
this.toolbox.print.warning(' ⚠ migrations-utils/mongo-uri.js is missing — the generated migrate.js requires it ' +
|
|
1360
|
+
'(resolveMongoUri). Migrations will not run. It normally ships with nest-server-starter; ' +
|
|
1361
|
+
'add it (must export resolveMongoUri) so `migrate up` can resolve the Mongo URI.');
|
|
1362
|
+
}
|
|
1363
|
+
// Compile migrations to JavaScript. The production image prunes ts-node, so
|
|
1364
|
+
// `migrate up` can only load plain .js migrations there. Without this the
|
|
1365
|
+
// entrypoint's migration step never applies anything.
|
|
1366
|
+
const tsconfigBuildPath = `${dest}/tsconfig.build.json`;
|
|
1367
|
+
if (filesystem.exists(tsconfigBuildPath)) {
|
|
1368
|
+
const tsconfigBuildRaw = filesystem.read(tsconfigBuildPath) || '';
|
|
1369
|
+
if (!tsconfigBuildRaw.includes('migrations/**/*.ts')) {
|
|
1370
|
+
// 1. Extend `include` so tsc also emits the .ts migrations + helpers.
|
|
1371
|
+
const includePatched = tsconfigBuildRaw.replace(/"include":\s*\[\s*"src\/\*\*\/\*"\s*\]/, '"include": ["src/**/*", "migrations/**/*.ts", "migrations-utils/**/*.ts"]');
|
|
1372
|
+
if (includePatched === tsconfigBuildRaw) {
|
|
1373
|
+
this.toolbox.print.warning(' ⚠ tsconfig.build.json: could not extend "include" automatically — ' +
|
|
1374
|
+
'add "migrations/**/*.ts" and "migrations-utils/**/*.ts" manually, ' +
|
|
1375
|
+
'otherwise migrations are never compiled and the container skips them.');
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
// 2. Pin rootDir. Once `include` reaches beyond src/, tsc re-infers the
|
|
1379
|
+
// rootDir from the common parent of ALL included files, which can
|
|
1380
|
+
// shift the emit layout and relocate the entry point. An explicit
|
|
1381
|
+
// "rootDir": "./" keeps the output mirroring the project tree:
|
|
1382
|
+
// `dist/src/main.js` — which is exactly what the Docker entrypoint
|
|
1383
|
+
// runs (see nest-server-starter/docker-entrypoint.sh) — plus
|
|
1384
|
+
// `dist/migrations/` and `dist/migrations-utils/`. Without the pin the
|
|
1385
|
+
// entry can move and the container's `node dist/src/main.js` dies with
|
|
1386
|
+
// MODULE_NOT_FOUND. Newer starters ship the pin; older ones (pre-9.0) do not.
|
|
1387
|
+
let patched = includePatched;
|
|
1388
|
+
let rootDirPinned = /"rootDir"\s*:/.test(patched);
|
|
1389
|
+
if (!rootDirPinned) {
|
|
1390
|
+
const withRootDir = patched.replace(/("compilerOptions"\s*:\s*\{)/, '$1\n "rootDir": "./",');
|
|
1391
|
+
if (withRootDir !== patched) {
|
|
1392
|
+
patched = withRootDir;
|
|
1393
|
+
rootDirPinned = true;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
filesystem.write(tsconfigBuildPath, patched);
|
|
1397
|
+
if (!rootDirPinned) {
|
|
1398
|
+
this.toolbox.print.warning(' ⚠ tsconfig.build.json: extended "include" but could not confirm "rootDir": "./" — ' +
|
|
1399
|
+
'add it manually, otherwise tsc may relocate the emit and `node dist/src/main.js` fails.');
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1341
1404
|
// ── 4b. (removed) ─────────────────────────────────────────────────────
|
|
1342
1405
|
//
|
|
1343
1406
|
// Previous versions copied three maintenance scripts into
|
|
@@ -1420,6 +1483,25 @@ class Server {
|
|
|
1420
1483
|
}
|
|
1421
1484
|
}
|
|
1422
1485
|
}
|
|
1486
|
+
// ── 5b. Backfill the vendored core's transitive import closure ──
|
|
1487
|
+
//
|
|
1488
|
+
// The merge above only covers nest-server's *declared* dependencies.
|
|
1489
|
+
// The vendored core ALSO imports packages that used to arrive
|
|
1490
|
+
// transitively through the @lenne.tech/nest-server npm dep
|
|
1491
|
+
// (cron←@nestjs/schedule, jose←better-auth, fs-capacitor←graphql-upload,
|
|
1492
|
+
// graphql-ws / ws). Once vendored, those imports are direct, so under
|
|
1493
|
+
// pnpm's isolated node_modules they are unresolvable unless declared
|
|
1494
|
+
// as direct deps → TS2307 at build time. Scan the core's bare imports
|
|
1495
|
+
// and add any missing package (+ matching @types) at the EXACT version
|
|
1496
|
+
// pinned by the upstream lockfile.
|
|
1497
|
+
this.gatherVendorCoreImportClosure({
|
|
1498
|
+
coreDir,
|
|
1499
|
+
deps,
|
|
1500
|
+
devDeps,
|
|
1501
|
+
lockRaw: upstreamLockRaw,
|
|
1502
|
+
upstreamDeps,
|
|
1503
|
+
upstreamDevDeps,
|
|
1504
|
+
});
|
|
1423
1505
|
// Add a script to run the local bin/migrate.js. The starter's
|
|
1424
1506
|
// existing migrate:* scripts are already correct for npm mode; we
|
|
1425
1507
|
// need them pointing at the local bin + local ts-compiler.
|
|
@@ -1438,6 +1520,79 @@ class Server {
|
|
|
1438
1520
|
scripts['migrate:test:up'] = `NODE_ENV=test node ./bin/migrate.js up ${migrateArgs}`;
|
|
1439
1521
|
scripts['migrate:preview:up'] = `NODE_ENV=preview node ./bin/migrate.js up ${migrateArgs}`;
|
|
1440
1522
|
scripts['migrate:prod:up'] = `NODE_ENV=production node ./bin/migrate.js up ${migrateArgs}`;
|
|
1523
|
+
// Make the production build carry a runnable migration setup.
|
|
1524
|
+
//
|
|
1525
|
+
// The image has no ts-node, and the docker entrypoint looks for
|
|
1526
|
+
// `dist/bin/migrate.js`. Without these three scripts the entrypoint
|
|
1527
|
+
// silently skips migrations forever — the failure mode is invisible
|
|
1528
|
+
// until the first real data migration quietly does not run.
|
|
1529
|
+
//
|
|
1530
|
+
// copy:bin ships bin/migrate.js into dist/
|
|
1531
|
+
// copy:migrations ships only the .js helpers (the .ts migrations
|
|
1532
|
+
// are compiled by tsc, see tsconfig.build.json)
|
|
1533
|
+
// prune:migrations drops the *.d.ts / *.js.map that tsc emits next
|
|
1534
|
+
// to each compiled migration — the runner would
|
|
1535
|
+
// otherwise pick the declaration file up as a
|
|
1536
|
+
// second migration and throw on `export declare`
|
|
1537
|
+
//
|
|
1538
|
+
// Define each only if the project doesn't already have it, so a
|
|
1539
|
+
// deliberate customization survives a re-run (older starters simply
|
|
1540
|
+
// lack them → still repaired).
|
|
1541
|
+
if (typeof scripts['copy:bin'] !== 'string') {
|
|
1542
|
+
scripts['copy:bin'] = 'cpy ./bin ./dist/ || true';
|
|
1543
|
+
}
|
|
1544
|
+
if (typeof scripts['copy:migrations'] !== 'string') {
|
|
1545
|
+
scripts['copy:migrations'] =
|
|
1546
|
+
'cpy "./migrations-utils/*.js" ./dist/migrations-utils/ && cpy ./tsconfig.json ./dist/';
|
|
1547
|
+
}
|
|
1548
|
+
if (typeof scripts['prune:migrations'] !== 'string') {
|
|
1549
|
+
scripts['prune:migrations'] = 'rimraf --glob "./dist/migrations/*.d.ts" "./dist/migrations/*.js.map"';
|
|
1550
|
+
}
|
|
1551
|
+
// Wire the three steps into the build. Order among them is irrelevant:
|
|
1552
|
+
// all three only need to run AFTER tsc, which `build` does first
|
|
1553
|
+
// (copy:bin/copy:migrations copy source files; prune only cleans
|
|
1554
|
+
// tsc's dist/migrations output). A step is already covered when it is
|
|
1555
|
+
// named directly in `build` or reachable through a `copy` aggregator
|
|
1556
|
+
// that `build` runs (newer starters). Older starters have no such
|
|
1557
|
+
// aggregator, so append to `build` — the one script guaranteed to
|
|
1558
|
+
// exist and to run tsc first; without this the sub-scripts are defined
|
|
1559
|
+
// but never invoked and the image ships without dist/bin/migrate.js.
|
|
1560
|
+
//
|
|
1561
|
+
// Use the package-manager `run` the project's own scripts already use
|
|
1562
|
+
// (older starters: npm, newer: pnpm); a hard-coded `pnpm run` would
|
|
1563
|
+
// make `build` die with "pnpm: command not found" in an npm project.
|
|
1564
|
+
const scriptText = Object.values(scripts)
|
|
1565
|
+
.filter((v) => typeof v === 'string')
|
|
1566
|
+
.join('\n');
|
|
1567
|
+
const pmRun = /\bpnpm\s+run\b/.test(scriptText)
|
|
1568
|
+
? 'pnpm run'
|
|
1569
|
+
: /\byarn\s+run\b/.test(scriptText)
|
|
1570
|
+
? 'yarn run'
|
|
1571
|
+
: /\bnpm\s+run\b/.test(scriptText)
|
|
1572
|
+
? 'npm run'
|
|
1573
|
+
: 'pnpm run';
|
|
1574
|
+
if (typeof scripts.build === 'string') {
|
|
1575
|
+
const copyAggregator = typeof scripts.copy === 'string' ? scripts.copy : '';
|
|
1576
|
+
const covered = (step) => scripts.build.includes(step) || copyAggregator.includes(step);
|
|
1577
|
+
const missingSteps = ['copy:bin', 'copy:migrations', 'prune:migrations'].filter((s) => !covered(s));
|
|
1578
|
+
if (missingSteps.length > 0) {
|
|
1579
|
+
scripts.build = `${scripts.build}${missingSteps.map((s) => ` && ${pmRun} ${s}`).join('')}`;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
this.toolbox.print.warning(' ⚠ package.json has no "build" script — copy:bin/copy:migrations/prune:migrations were defined but ' +
|
|
1584
|
+
'not wired in; the production image may ship without dist/bin/migrate.js and skip migrations.');
|
|
1585
|
+
}
|
|
1586
|
+
// prune:migrations needs `rimraf`, copy:* need `cpy` (cpy-cli). Older
|
|
1587
|
+
// starters ship cpy-cli but not rimraf — add whatever is missing so
|
|
1588
|
+
// `build` doesn't die on an absent binary.
|
|
1589
|
+
const devDependencies = (pkg.devDependencies && typeof pkg.devDependencies === 'object'
|
|
1590
|
+
? pkg.devDependencies
|
|
1591
|
+
: (pkg.devDependencies = {}));
|
|
1592
|
+
if (typeof devDependencies.rimraf !== 'string')
|
|
1593
|
+
devDependencies.rimraf = '^6.0.1';
|
|
1594
|
+
if (typeof devDependencies['cpy-cli'] !== 'string')
|
|
1595
|
+
devDependencies['cpy-cli'] = '^6.0.0';
|
|
1441
1596
|
// Vendor freshness check: reads VENDOR.md baseline version and
|
|
1442
1597
|
// compares against npm registry. Non-blocking (always exits 0).
|
|
1443
1598
|
// Uses a short inline script — no external file needed.
|
|
@@ -1692,7 +1847,13 @@ class Server {
|
|
|
1692
1847
|
// Scan all consumer files for stale bare-specifier imports that the
|
|
1693
1848
|
// codemod should have rewritten. A single miss causes a compile error,
|
|
1694
1849
|
// so catching it here with a clear message saves the user debugging time.
|
|
1695
|
-
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server'
|
|
1850
|
+
const staleImports = this.findStaleImports(dest, '@lenne.tech/nest-server',
|
|
1851
|
+
// Match only real import/export/require specifiers in single/double
|
|
1852
|
+
// quotes — NOT comment references like
|
|
1853
|
+
// `node_modules/@lenne.tech/nest-server/...` (backticks, no keyword),
|
|
1854
|
+
// which the codemod legitimately leaves untouched. A naive substring
|
|
1855
|
+
// match flagged config.env.ts's JSDoc path as a false positive.
|
|
1856
|
+
/(?:from|import|export|require)\s*\(?\s*['"]@lenne\.tech\/nest-server(?:\/[^'"]*)?['"]/);
|
|
1696
1857
|
if (staleImports.length > 0) {
|
|
1697
1858
|
const { print } = this.toolbox;
|
|
1698
1859
|
print.warning(`⚠ ${staleImports.length} file(s) still contain '@lenne.tech/nest-server' imports after vendor conversion:`);
|
|
@@ -2341,6 +2502,177 @@ class Server {
|
|
|
2341
2502
|
}
|
|
2342
2503
|
return stale;
|
|
2343
2504
|
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Backfills the vendored core's transitive import closure into the
|
|
2507
|
+
* project's dependencies. After vendoring, `src/core/**` imports packages
|
|
2508
|
+
* that previously arrived transitively via the `@lenne.tech/nest-server`
|
|
2509
|
+
* npm dependency (e.g. `cron`, `jose`, `fs-capacitor`, `graphql-ws`,
|
|
2510
|
+
* `ws`). nest-server never declares these as direct deps, so the
|
|
2511
|
+
* upstream-deps merge misses them — yet the vendored code imports them
|
|
2512
|
+
* directly, which pnpm's isolated node_modules cannot resolve → TS2307 at
|
|
2513
|
+
* build time.
|
|
2514
|
+
*
|
|
2515
|
+
* Scans every `.ts` file under the vendored core for bare import
|
|
2516
|
+
* specifiers, drops Node builtins and already-declared packages, and adds
|
|
2517
|
+
* the remainder (plus any matching `@types/*`) using EXACT versions read
|
|
2518
|
+
* from the upstream `pnpm-lock.yaml`. Mutates `deps`/`devDeps` in place.
|
|
2519
|
+
*/
|
|
2520
|
+
gatherVendorCoreImportClosure(options) {
|
|
2521
|
+
const { coreDir, deps, devDeps, lockRaw, upstreamDeps, upstreamDevDeps } = options;
|
|
2522
|
+
const { print } = this.toolbox;
|
|
2523
|
+
const builtins = new Set(require('module').builtinModules);
|
|
2524
|
+
// Compare two dotted versions numerically (major.minor.patch) — used to
|
|
2525
|
+
// pick the highest when the lockfile pins multiple copies (e.g. ws 7/8).
|
|
2526
|
+
const compareVersions = (a, b) => {
|
|
2527
|
+
const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
2528
|
+
const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
2529
|
+
for (let i = 0; i < 3; i++) {
|
|
2530
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) {
|
|
2531
|
+
return (pa[i] || 0) - (pb[i] || 0);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return 0;
|
|
2535
|
+
};
|
|
2536
|
+
// Parse `<name>@<version>` keys from the lockfile's `packages:` section
|
|
2537
|
+
// into a name→version map (highest version wins). pnpm-lock v9 lists
|
|
2538
|
+
// every package (direct + transitive) there with a clean 2-space indent.
|
|
2539
|
+
const parseLockVersions = (raw) => {
|
|
2540
|
+
const map = {};
|
|
2541
|
+
let inPackages = false;
|
|
2542
|
+
for (const line of raw.split('\n')) {
|
|
2543
|
+
if (/^packages:\s*$/.test(line)) {
|
|
2544
|
+
inPackages = true;
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
if (inPackages && /^\S/.test(line)) {
|
|
2548
|
+
break; // reached the next top-level section
|
|
2549
|
+
}
|
|
2550
|
+
if (!inPackages) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
const matched = line.match(/^ {2}(['"]?)(.+?)\1:\s*$/);
|
|
2554
|
+
if (!matched) {
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2557
|
+
let key = matched[2] || '';
|
|
2558
|
+
const paren = key.indexOf('('); // strip pnpm peer suffix
|
|
2559
|
+
if (paren >= 0) {
|
|
2560
|
+
key = key.slice(0, paren);
|
|
2561
|
+
}
|
|
2562
|
+
const at = key.lastIndexOf('@');
|
|
2563
|
+
if (at <= 0) {
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
const name = key.slice(0, at);
|
|
2567
|
+
const version = key.slice(at + 1);
|
|
2568
|
+
if (!/^\d/.test(version)) {
|
|
2569
|
+
continue; // ranges / non-versions
|
|
2570
|
+
}
|
|
2571
|
+
const current = map[name];
|
|
2572
|
+
if (!current || compareVersions(version, current) > 0) {
|
|
2573
|
+
map[name] = version;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return map;
|
|
2577
|
+
};
|
|
2578
|
+
// Reduce an import specifier to its package name (or null for relative
|
|
2579
|
+
// paths and Node builtins, which need no dependency entry).
|
|
2580
|
+
const packageNameOf = (spec) => {
|
|
2581
|
+
if (!spec || spec.startsWith('.') || spec.startsWith('/')) {
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
const bare = spec.startsWith('node:') ? spec.slice('node:'.length) : spec;
|
|
2585
|
+
const parts = bare.split('/');
|
|
2586
|
+
const name = bare.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] || '';
|
|
2587
|
+
// Drop Node builtins, the framework package itself (never a dependency
|
|
2588
|
+
// of its own vendored code), and anything that is not a syntactically
|
|
2589
|
+
// valid npm package name (defensive guard).
|
|
2590
|
+
if (!name || builtins.has(name) || name === '@lenne.tech/nest-server') {
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
return /^(?:@[\w.~-]+\/)?[\w.~-]+$/.test(name) ? name : null;
|
|
2594
|
+
};
|
|
2595
|
+
// Collect every bare specifier imported by the vendored core. Parse the
|
|
2596
|
+
// TypeScript AST (ts-morph) rather than scanning text, so that prose in
|
|
2597
|
+
// comments/strings — a JSDoc `import … from '@lenne.tech/nest-server'`
|
|
2598
|
+
// example, or an English `… distinguishable from "never set"` — is never
|
|
2599
|
+
// mistaken for a real import (which a naive regex did).
|
|
2600
|
+
const imported = new Set();
|
|
2601
|
+
const { Project, SyntaxKind } = require('ts-morph');
|
|
2602
|
+
const scanProject = new Project({
|
|
2603
|
+
skipAddingFilesFromTsConfig: true,
|
|
2604
|
+
skipFileDependencyResolution: true,
|
|
2605
|
+
});
|
|
2606
|
+
const sourceFiles = scanProject.addSourceFilesAtPaths(`${coreDir}/**/*.ts`);
|
|
2607
|
+
for (const sourceFile of sourceFiles) {
|
|
2608
|
+
// `import … from 'x'` and `export … from 'x'`
|
|
2609
|
+
for (const decl of [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()]) {
|
|
2610
|
+
const spec = decl.getModuleSpecifierValue();
|
|
2611
|
+
const name = spec ? packageNameOf(spec) : null;
|
|
2612
|
+
if (name) {
|
|
2613
|
+
imported.add(name);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
// `import x = require('x')`
|
|
2617
|
+
for (const decl of sourceFile.getDescendantsOfKind(SyntaxKind.ImportEqualsDeclaration)) {
|
|
2618
|
+
const ref = decl
|
|
2619
|
+
.getModuleReference()
|
|
2620
|
+
.getText()
|
|
2621
|
+
.match(/^require\(\s*['"]([^'"]+)['"]\s*\)$/);
|
|
2622
|
+
const name = ref ? packageNameOf(ref[1] || '') : null;
|
|
2623
|
+
if (name) {
|
|
2624
|
+
imported.add(name);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
// Dynamic `import('x')` / `require('x')` calls
|
|
2628
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
2629
|
+
const callee = call.getExpression().getText();
|
|
2630
|
+
if (callee !== 'import' && callee !== 'require') {
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
const arg = call.getArguments()[0];
|
|
2634
|
+
if (arg && arg.getKind() === SyntaxKind.StringLiteral) {
|
|
2635
|
+
const name = packageNameOf(arg.getLiteralText());
|
|
2636
|
+
if (name) {
|
|
2637
|
+
imported.add(name);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
const lockVersions = parseLockVersions(lockRaw);
|
|
2643
|
+
const added = [];
|
|
2644
|
+
const unresolved = [];
|
|
2645
|
+
for (const name of [...imported].sort()) {
|
|
2646
|
+
if (name in deps || name in devDeps) {
|
|
2647
|
+
continue; // already declared by the starter or the upstream merge
|
|
2648
|
+
}
|
|
2649
|
+
const version = lockVersions[name] || upstreamDeps[name] || upstreamDevDeps[name];
|
|
2650
|
+
if (!version) {
|
|
2651
|
+
unresolved.push(name);
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
deps[name] = version;
|
|
2655
|
+
added.push(`${name}@${version}`);
|
|
2656
|
+
// Add a matching @types/* (for packages that ship no own types) so the
|
|
2657
|
+
// vendored core type-checks. Handles scoped names
|
|
2658
|
+
// (@scope/x → @types/scope__x) too.
|
|
2659
|
+
const typesName = name.startsWith('@') ? `@types/${name.slice(1).replace('/', '__')}` : `@types/${name}`;
|
|
2660
|
+
if (!(typesName in deps) && !(typesName in devDeps)) {
|
|
2661
|
+
const typesVersion = lockVersions[typesName] || upstreamDevDeps[typesName];
|
|
2662
|
+
if (typesVersion) {
|
|
2663
|
+
devDeps[typesName] = typesVersion;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
if (added.length > 0) {
|
|
2668
|
+
print.info(` vendored core closure: added ${added.length} transitive dep(s) → ${added.join(', ')}`);
|
|
2669
|
+
}
|
|
2670
|
+
if (unresolved.length > 0) {
|
|
2671
|
+
print.warning(`⚠ ${unresolved.length} vendored-core import(s) could not be resolved to a version ` +
|
|
2672
|
+
`(absent from the upstream lock and package.json): ${unresolved.join(', ')}. ` +
|
|
2673
|
+
'Add them to projects/api/package.json manually if the build fails.');
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2344
2676
|
/**
|
|
2345
2677
|
* Checks whether an import specifier resolves to the vendored core directory.
|
|
2346
2678
|
* Used during vendor→npm import rewriting.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findAngularEnvironmentsDir = findAngularEnvironmentsDir;
|
|
4
|
+
exports.patchAngularEnvironments = patchAngularEnvironments;
|
|
5
|
+
exports.stageHosts = stageHosts;
|
|
6
|
+
/**
|
|
7
|
+
* Rewrite the hardcoded localhost URLs in an Angular app's environment files
|
|
8
|
+
* to the deployed TurboOps stage URLs.
|
|
9
|
+
*
|
|
10
|
+
* WHY: a Nuxt app reads its API URL from the stage env at runtime
|
|
11
|
+
* (`NUXT_PUBLIC_API_URL`, injected by TurboOps). Angular has no such runtime
|
|
12
|
+
* config — `ng build --configuration production` BAKES `environment.prod.ts`
|
|
13
|
+
* into the bundle. Without this patch a deployed Angular app ships
|
|
14
|
+
* `apiUrl: 'http://127.0.0.1:3000/graphql'` and calls the end user's own
|
|
15
|
+
* machine.
|
|
16
|
+
*
|
|
17
|
+
* Stage mapping (TurboOps knows exactly two stages, `dev` and `production`):
|
|
18
|
+
*
|
|
19
|
+
* environment.prod.ts → production → <domain> + api.<domain>
|
|
20
|
+
* environment.develop.ts → dev → dev.<domain> + api.dev.<domain>
|
|
21
|
+
* environment.test.ts → dev → dev.<domain> + api.dev.<domain>
|
|
22
|
+
*
|
|
23
|
+
* `environment.ts` is the local-dev config and is deliberately left alone.
|
|
24
|
+
* `environment.test.ts` and `environment.develop.ts` are identical in
|
|
25
|
+
* ng-base-starter (both `prefix: 'app-test'`); with no TurboOps "test" stage,
|
|
26
|
+
* the non-production stage is `dev` for both.
|
|
27
|
+
*
|
|
28
|
+
* Only the URL ORIGIN (scheme + host + optional port) is replaced — the path
|
|
29
|
+
* (`/graphql`, `/api`) is preserved, so a project that customised it keeps its
|
|
30
|
+
* routes. That also makes the patch idempotent AND re-runnable with a new
|
|
31
|
+
* domain: an already-deployed URL is rewritten to the new one instead of being
|
|
32
|
+
* skipped (the old `http://127.0.0.1:3000`-only replace silently did nothing
|
|
33
|
+
* on the second run).
|
|
34
|
+
*/
|
|
35
|
+
const fs_1 = require("fs");
|
|
36
|
+
const path_1 = require("path");
|
|
37
|
+
/** Which environment file belongs to which TurboOps stage. */
|
|
38
|
+
const ENVIRONMENT_STAGES = {
|
|
39
|
+
'environment.develop.ts': 'dev',
|
|
40
|
+
'environment.prod.ts': 'production',
|
|
41
|
+
'environment.test.ts': 'dev',
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Locate the Angular `environments/` directory for a project root.
|
|
45
|
+
* Returns null for non-Angular apps (e.g. Nuxt), which makes the whole
|
|
46
|
+
* patch a no-op there.
|
|
47
|
+
*/
|
|
48
|
+
function findAngularEnvironmentsDir(projectRoot) {
|
|
49
|
+
const candidates = [(0, path_1.join)('projects', 'app', 'src', 'environments'), (0, path_1.join)('src', 'environments')];
|
|
50
|
+
for (const rel of candidates) {
|
|
51
|
+
const dir = (0, path_1.join)(projectRoot, rel);
|
|
52
|
+
if ((0, fs_1.existsSync)(dir))
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Point every stage-specific Angular environment file at the deployed URLs.
|
|
59
|
+
* Returns one {@link PatchResult} per file that exists (empty array when the
|
|
60
|
+
* project is not an Angular app).
|
|
61
|
+
*/
|
|
62
|
+
function patchAngularEnvironments(options) {
|
|
63
|
+
const dir = findAngularEnvironmentsDir(options.projectRoot);
|
|
64
|
+
if (!dir)
|
|
65
|
+
return [];
|
|
66
|
+
const results = [];
|
|
67
|
+
for (const [fileName, stage] of Object.entries(ENVIRONMENT_STAGES)) {
|
|
68
|
+
const file = (0, path_1.join)(dir, fileName);
|
|
69
|
+
if (!(0, fs_1.existsSync)(file))
|
|
70
|
+
continue;
|
|
71
|
+
results.push(patchEnvironmentFile(file, options.domain, stage));
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Public hostnames of a stage. The API always lives one label in front of the
|
|
77
|
+
* app host, so `dev` nests as `api.dev.<domain>`.
|
|
78
|
+
*/
|
|
79
|
+
function stageHosts(domain, stage) {
|
|
80
|
+
const app = stage === 'production' ? domain : `dev.${domain}`;
|
|
81
|
+
return { api: `api.${app}`, app };
|
|
82
|
+
}
|
|
83
|
+
/** Rewrite the four URL properties of a single environment file. */
|
|
84
|
+
function patchEnvironmentFile(file, domain, stage) {
|
|
85
|
+
const hosts = stageHosts(domain, stage);
|
|
86
|
+
const original = (0, fs_1.readFileSync)(file, 'utf8');
|
|
87
|
+
let content = original;
|
|
88
|
+
let replacements = 0;
|
|
89
|
+
let recognized = 0;
|
|
90
|
+
const rewriteOrigin = (property, origin) => {
|
|
91
|
+
// (property: )(quote)scheme://host[:port](path)(same quote)
|
|
92
|
+
const pattern = new RegExp(`(${property}:\\s*)(['"])[a-z]+://[^'"/]+([^'"]*)\\2`, 'g');
|
|
93
|
+
content = content.replace(pattern, (match, prefix, quote, path) => {
|
|
94
|
+
recognized++;
|
|
95
|
+
const next = `${prefix}${quote}${origin}${path}${quote}`;
|
|
96
|
+
if (next !== match)
|
|
97
|
+
replacements++;
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
rewriteOrigin('apiUrl', `https://${hosts.api}`);
|
|
102
|
+
rewriteOrigin('restUrl', `https://${hosts.api}`);
|
|
103
|
+
rewriteOrigin('wsUrl', `wss://${hosts.api}`);
|
|
104
|
+
rewriteOrigin('appUrl', `https://${hosts.app}`);
|
|
105
|
+
if (replacements > 0)
|
|
106
|
+
(0, fs_1.writeFileSync)(file, content, 'utf8');
|
|
107
|
+
return { file, patched: replacements > 0, recognized: recognized > 0, replacements };
|
|
108
|
+
}
|
|
@@ -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
|
+
}
|