@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.
Files changed (52) hide show
  1. package/README.md +6 -5
  2. package/build/commands/codex/codex.js +23 -0
  3. package/build/commands/codex/plugins.js +103 -0
  4. package/build/commands/codex/shortcuts.js +110 -0
  5. package/build/commands/config/validate.js +0 -4
  6. package/build/commands/deployment/create.js +218 -213
  7. package/build/commands/dev/init.js +2 -1
  8. package/build/commands/dev/status.js +2 -0
  9. package/build/commands/dev/test.js +12 -6
  10. package/build/commands/dev/up.js +17 -7
  11. package/build/commands/frontend/nuxt.js +4 -7
  12. package/build/commands/fullstack/add-api.js +6 -9
  13. package/build/commands/fullstack/add-app.js +5 -7
  14. package/build/commands/fullstack/init.js +34 -20
  15. package/build/commands/status.js +2 -0
  16. package/build/commands/ticket/list.js +2 -0
  17. package/build/commands/ticket/start.js +6 -4
  18. package/build/extensions/frontend-helper.js +44 -39
  19. package/build/extensions/server.js +354 -22
  20. package/build/lib/angular-environments.js +108 -0
  21. package/build/lib/codex-cli.js +56 -0
  22. package/build/lib/codex-plugin-utils.js +102 -0
  23. package/build/lib/dev-identity.js +38 -18
  24. package/build/lib/dev-package-manager.js +89 -0
  25. package/build/lib/dev-patches.js +4 -1
  26. package/build/lib/dev-project.js +20 -7
  27. package/build/lib/dev-state.js +17 -1
  28. package/build/lib/dev-test-session.js +61 -19
  29. package/build/lib/dev-ticket.js +12 -6
  30. package/build/lib/ensure-root-dockerignore.js +92 -0
  31. package/build/lib/fs-utils.js +19 -0
  32. package/build/lib/hoist-workspace-pnpm-config.js +2 -11
  33. package/build/lib/remove-nested-lockfiles.js +53 -0
  34. package/build/lib/turboops-config.js +93 -0
  35. package/build/lib/vendor-claude-md.js +7 -2
  36. package/build/lib/workspace-integration.js +96 -3
  37. package/build/templates/deployment/turboops.json.ejs +3 -0
  38. package/build/templates/vendor-scripts/migrate-store.js +25 -0
  39. package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
  40. package/docs/commands.md +30 -9
  41. package/docs/lt.config.md +9 -19
  42. package/package.json +1 -1
  43. package/build/templates/deployment/.github/workflows/pre-release.yml.ejs +0 -41
  44. package/build/templates/deployment/.github/workflows/release.yml.ejs +0 -41
  45. package/build/templates/deployment/.gitlab-ci.yml.ejs +0 -181
  46. package/build/templates/deployment/Dockerfile.app.ejs +0 -13
  47. package/build/templates/deployment/Dockerfile.ejs +0 -18
  48. package/build/templates/deployment/docker-compose.dev.yml.ejs +0 -99
  49. package/build/templates/deployment/docker-compose.prod.yml.ejs +0 -92
  50. package/build/templates/deployment/docker-compose.test.yml.ejs +0 -98
  51. package/build/templates/deployment/scripts/build-push.sh.ejs +0 -20
  52. 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 (_b) {
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 (_c) {
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 (_d) {
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
- // Replace the file with the explicit, imo-pilot-proven variant that
1323
- // registers ts-node itself BEFORE requiring the vendored core and
1324
- // points at the specific migration.helper path (not the index hub).
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.write(migrateJsPath, [
1327
- '// The vendored core is TypeScript-only (no prebuilt dist/). Register ts-node (via our',
1328
- '// custom bootstrap) before requiring any vendor module, so that .ts source files are',
1329
- '// transparently compiled on demand. Uses the same compiler config as the migrate CLI.',
1330
- "require('./ts-compiler');",
1331
- '',
1332
- "const { createMigrationStore } = require('../src/core/modules/migrate/helpers/migration.helper');",
1333
- "const config = require('../src/config.env');",
1334
- '',
1335
- 'module.exports = createMigrationStore(',
1336
- ' config.default.mongoose.uri,',
1337
- " 'migrations' // optional, default is 'migrations'",
1338
- ');',
1339
- '',
1340
- ].join('\n'));
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
+ }