@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
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EMPTY_CODEX_PLUGIN_CONTENTS = void 0;
4
+ exports.installCodexAgents = installCodexAgents;
5
+ exports.installCodexPrompts = installCodexPrompts;
6
+ exports.readCodexMarketplaceName = readCodexMarketplaceName;
7
+ exports.readLocalCodexPluginContents = readLocalCodexPluginContents;
8
+ /**
9
+ * Codex plugin setup helpers.
10
+ */
11
+ const fs_1 = require("fs");
12
+ const os_1 = require("os");
13
+ const path_1 = require("path");
14
+ const json_utils_1 = require("./json-utils");
15
+ exports.EMPTY_CODEX_PLUGIN_CONTENTS = {
16
+ agents: [],
17
+ hooks: 0,
18
+ mcpServers: [],
19
+ prompts: [],
20
+ skills: [],
21
+ };
22
+ function installCodexAgents(pluginRoot) {
23
+ const sourceDir = (0, path_1.join)(pluginRoot, 'codex-agents');
24
+ if (!(0, fs_1.existsSync)(sourceDir)) {
25
+ return 0;
26
+ }
27
+ const targetDir = (0, path_1.join)((0, os_1.homedir)(), '.codex', 'agents');
28
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
29
+ let count = 0;
30
+ for (const entry of (0, fs_1.readdirSync)(sourceDir, { withFileTypes: true })) {
31
+ if (!entry.isFile() || !entry.name.endsWith('.toml'))
32
+ continue;
33
+ (0, fs_1.copyFileSync)((0, path_1.join)(sourceDir, entry.name), (0, path_1.join)(targetDir, entry.name));
34
+ count += 1;
35
+ }
36
+ return count;
37
+ }
38
+ function installCodexPrompts(pluginRoot) {
39
+ const sourceDir = (0, path_1.join)(pluginRoot, 'prompts');
40
+ if (!(0, fs_1.existsSync)(sourceDir)) {
41
+ return 0;
42
+ }
43
+ const targetDir = (0, path_1.join)((0, os_1.homedir)(), '.codex', 'prompts');
44
+ (0, fs_1.mkdirSync)(targetDir, { recursive: true });
45
+ let count = 0;
46
+ for (const entry of (0, fs_1.readdirSync)(sourceDir, { withFileTypes: true })) {
47
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
48
+ continue;
49
+ (0, fs_1.copyFileSync)((0, path_1.join)(sourceDir, entry.name), (0, path_1.join)(targetDir, entry.name));
50
+ count += 1;
51
+ }
52
+ return count;
53
+ }
54
+ function readCodexMarketplaceName(root) {
55
+ const path = (0, path_1.join)(root, '.agents', 'plugins', 'marketplace.json');
56
+ if (!(0, fs_1.existsSync)(path)) {
57
+ return null;
58
+ }
59
+ const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(path, 'utf-8'));
60
+ return (parsed === null || parsed === void 0 ? void 0 : parsed.name) || null;
61
+ }
62
+ function readLocalCodexPluginContents(pluginRoot) {
63
+ const result = Object.assign({}, exports.EMPTY_CODEX_PLUGIN_CONTENTS);
64
+ const skillsDir = (0, path_1.join)(pluginRoot, 'skills');
65
+ if ((0, fs_1.existsSync)(skillsDir)) {
66
+ result.skills = (0, fs_1.readdirSync)(skillsDir, { withFileTypes: true })
67
+ .filter((entry) => entry.isDirectory())
68
+ .map((entry) => entry.name)
69
+ .sort();
70
+ }
71
+ const agentsDir = (0, path_1.join)(pluginRoot, 'codex-agents');
72
+ if ((0, fs_1.existsSync)(agentsDir)) {
73
+ result.agents = (0, fs_1.readdirSync)(agentsDir, { withFileTypes: true })
74
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.toml'))
75
+ .map((entry) => entry.name.replace(/\.toml$/, ''))
76
+ .sort();
77
+ }
78
+ const promptsDir = (0, path_1.join)(pluginRoot, 'prompts');
79
+ if ((0, fs_1.existsSync)(promptsDir)) {
80
+ result.prompts = (0, fs_1.readdirSync)(promptsDir, { withFileTypes: true })
81
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
82
+ .map((entry) => entry.name.replace(/\.md$/, ''))
83
+ .sort();
84
+ }
85
+ const hooksPath = (0, path_1.join)(pluginRoot, 'hooks', 'hooks.json');
86
+ if ((0, fs_1.existsSync)(hooksPath)) {
87
+ const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(hooksPath, 'utf-8'));
88
+ if (parsed === null || parsed === void 0 ? void 0 : parsed.hooks) {
89
+ for (const groups of Object.values(parsed.hooks)) {
90
+ for (const group of groups) {
91
+ result.hooks += Array.isArray(group.hooks) ? group.hooks.length : 0;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ const mcpPath = (0, path_1.join)(pluginRoot, '.mcp.json');
97
+ if ((0, fs_1.existsSync)(mcpPath)) {
98
+ const parsed = (0, json_utils_1.safeJsonParse)((0, fs_1.readFileSync)(mcpPath, 'utf-8'));
99
+ result.mcpServers = Object.keys((parsed === null || parsed === void 0 ? void 0 : parsed.mcpServers) || {}).sort();
100
+ }
101
+ return result;
102
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildIdentity = buildIdentity;
4
4
  exports.buildTestIdentity = buildTestIdentity;
5
5
  exports.buildTicketIdentity = buildTicketIdentity;
6
+ exports.detectStandaloneKind = detectStandaloneKind;
6
7
  exports.isUnmodifiedTemplateName = isUnmodifiedTemplateName;
7
8
  exports.projectSlug = projectSlug;
8
9
  exports.slugify = slugify;
@@ -31,43 +32,46 @@ const path_1 = require("path");
31
32
  * - `projects/app` → `<slug>.localhost` (primary)
32
33
  * - `projects/<other>` → `<other>.<slug>.localhost`
33
34
  *
34
- * For standalone projects (single repo, no `projects/` directory):
35
+ * For standalone projects (single repo, no `projects/api` or `projects/app`):
35
36
  * - API project (config.env.ts present) → `api.<slug>.localhost`
36
37
  * - App project (nuxt.config.ts present) → `<slug>.localhost`
38
+ *
39
+ * Every combination is valid: api+app, api-only, and app-only.
37
40
  */
38
41
  function buildIdentity(root) {
39
42
  const slug = projectSlug(root);
40
43
  const subdomains = {};
41
- const projectsDir = (0, path_1.join)(root, 'projects');
42
- if ((0, fs_1.existsSync)(projectsDir)) {
43
- // Monorepo: enumerate projects/* subdirectories.
44
- const apiDir = (0, path_1.join)(projectsDir, 'api');
45
- const appDir = (0, path_1.join)(projectsDir, 'app');
46
- if ((0, fs_1.existsSync)(apiDir)) {
44
+ const apiDir = (0, path_1.join)(root, 'projects', 'api');
45
+ const appDir = (0, path_1.join)(root, 'projects', 'app');
46
+ const hasApi = (0, fs_1.existsSync)(apiDir);
47
+ const hasApp = (0, fs_1.existsSync)(appDir);
48
+ // Monorepo only when a known subproject is actually present. A bare (or
49
+ // unrelated) `projects/` directory must not shadow the standalone probe —
50
+ // it would yield an identity with zero subdomains and no routable URLs.
51
+ if (hasApi || hasApp) {
52
+ if (hasApi) {
47
53
  subdomains.api = {
48
54
  hostname: `api.${slug}.localhost`,
49
55
  isPrimaryApp: false,
50
56
  subdir: 'projects/api',
51
57
  };
52
58
  }
53
- if ((0, fs_1.existsSync)(appDir)) {
59
+ if (hasApp) {
54
60
  subdomains.app = {
55
61
  hostname: `${slug}.localhost`,
56
62
  isPrimaryApp: true,
57
63
  subdir: 'projects/app',
58
64
  };
59
65
  }
66
+ return { root, slug, subdomains };
60
67
  }
61
- else {
62
- // Standalone derive from project shape.
63
- const isApi = (0, fs_1.existsSync)((0, path_1.join)(root, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(root, 'nest-cli.json'));
64
- const isApp = (0, fs_1.existsSync)((0, path_1.join)(root, 'nuxt.config.ts'));
65
- if (isApi) {
66
- subdomains.api = { hostname: `api.${slug}.localhost`, isPrimaryApp: false, subdir: null };
67
- }
68
- if (isApp) {
69
- subdomains.app = { hostname: `${slug}.localhost`, isPrimaryApp: true, subdir: null };
70
- }
68
+ // Standalone — derive from project shape.
69
+ const { isApi, isApp } = detectStandaloneKind(root);
70
+ if (isApi) {
71
+ subdomains.api = { hostname: `api.${slug}.localhost`, isPrimaryApp: false, subdir: null };
72
+ }
73
+ if (isApp) {
74
+ subdomains.app = { hostname: `${slug}.localhost`, isPrimaryApp: true, subdir: null };
71
75
  }
72
76
  return { root, slug, subdomains };
73
77
  }
@@ -105,6 +109,22 @@ function buildTestIdentity(base, suffix = '-test') {
105
109
  function buildTicketIdentity(base, id) {
106
110
  return buildTestIdentity(base, `-${id}`);
107
111
  }
112
+ /**
113
+ * Probe a single directory for the shape of a STANDALONE lt project.
114
+ *
115
+ * Single source of truth for "is this an API / an App?", shared by
116
+ * {@link buildIdentity} and `dev-project.ts#resolveLayout` so the identity
117
+ * (URLs) and the layout (which processes to spawn) can never disagree.
118
+ *
119
+ * Both flags can be true (a single repo holding both), and both can be false
120
+ * (not an lt-dev project).
121
+ */
122
+ function detectStandaloneKind(dir) {
123
+ return {
124
+ isApi: (0, fs_1.existsSync)((0, path_1.join)(dir, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(dir, 'nest-cli.json')),
125
+ isApp: (0, fs_1.existsSync)((0, path_1.join)(dir, 'nuxt.config.ts')),
126
+ };
127
+ }
108
128
  /**
109
129
  * package.json `name` values that are unchanged starter-template defaults.
110
130
  *
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickPackageManager = pickPackageManager;
4
+ /**
5
+ * Pick the package manager `lt dev` should drive for a given project dir.
6
+ *
7
+ * `lt dev up` and the related test/ticket flows used to hard-code `pnpm`
8
+ * for every monorepo. That breaks npm-only and yarn-only projects: a
9
+ * `pnpm install` invoked from inside `pnpm start` regenerates a
10
+ * pnpm-lock.yaml (which the project's .gitignore then refuses to track),
11
+ * fails on un-approved build scripts (bcrypt, sharp, esbuild) and exits
12
+ * non-zero — the supervised api/app processes die immediately and
13
+ * `lt dev status` reports them as `dead`.
14
+ *
15
+ * Detection order — highest precedence first:
16
+ * 1. `LT_PM_BIN` env var (generic override).
17
+ * 2. `LT_PNPM_BIN` env var (legacy — kept for backwards compatibility).
18
+ * 3. `pnpm-lock.yaml` in `cwd` → pnpm
19
+ * 4. `yarn.lock` in `cwd` → yarn
20
+ * 5. `package-lock.json` in `cwd` → npm
21
+ * 6. Fallback: `pnpm` (preserves the historical default so nothing
22
+ * breaks for the projects this CLI was originally written for).
23
+ *
24
+ * The detection is per-cwd so a monorepo with a pnpm api + npm app gets
25
+ * the correct command per component.
26
+ */
27
+ const node_fs_1 = require("node:fs");
28
+ const node_path_1 = require("node:path");
29
+ /**
30
+ * Resolve which package manager to drive for `cwd`. Pure — only filesystem
31
+ * existence checks, no exec. The override env vars take precedence so a
32
+ * CI pipeline can pin the manager without touching the lockfile.
33
+ *
34
+ * `cwd` is expected to be a project root (i.e. where the lockfile lives).
35
+ * For a monorepo with separate api/app dirs, call this once per dir.
36
+ */
37
+ function pickPackageManager(cwd, env = process.env) {
38
+ const override = env.LT_PM_BIN || env.LT_PNPM_BIN;
39
+ if (override) {
40
+ return buildCommand(override, inferNameFromBin(override));
41
+ }
42
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, 'pnpm-lock.yaml'))) {
43
+ return buildCommand('pnpm', 'pnpm');
44
+ }
45
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, 'yarn.lock'))) {
46
+ return buildCommand('yarn', 'yarn');
47
+ }
48
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(cwd, 'package-lock.json'))) {
49
+ return buildCommand('npm', 'npm');
50
+ }
51
+ // Historical default — keep so projects without a lockfile (fresh
52
+ // scaffolds, vendored monorepos) behave exactly as before.
53
+ return buildCommand('pnpm', 'pnpm');
54
+ }
55
+ function buildCommand(bin, name) {
56
+ const isNpm = name === 'npm';
57
+ return {
58
+ bin,
59
+ exec(binary, args = []) {
60
+ // `npm exec` consumes the args before the binary name and treats
61
+ // anything after as its own flags unless separated by `--`. pnpm
62
+ // and yarn route them through unchanged. Without the separator
63
+ // `npm exec playwright test --shard=1/2` would parse `--shard`
64
+ // as an npm option, NOT a Playwright one.
65
+ return isNpm ? ['exec', '--', binary, ...args] : ['exec', binary, ...args];
66
+ },
67
+ installArgs: ['install'],
68
+ name,
69
+ runScript(script, extra = []) {
70
+ // pnpm + yarn accept the bare-script shortcut (`pnpm dev`), npm
71
+ // does not. Using `run <script>` is universally accepted so we
72
+ // route every manager through the same call.
73
+ return ['run', script, ...extra];
74
+ },
75
+ };
76
+ }
77
+ function inferNameFromBin(bin) {
78
+ const lower = bin.toLowerCase();
79
+ if (lower.endsWith('pnpm') || lower.includes('/pnpm')) {
80
+ return 'pnpm';
81
+ }
82
+ if (lower.endsWith('yarn') || lower.includes('/yarn')) {
83
+ return 'yarn';
84
+ }
85
+ if (lower.endsWith('npm') || lower.includes('/npm')) {
86
+ return 'npm';
87
+ }
88
+ return 'unknown';
89
+ }
@@ -127,7 +127,10 @@ function patchClaudeMd(file, options) {
127
127
  }
128
128
  else {
129
129
  const sep = content.endsWith('\n\n') ? '' : content.endsWith('\n') ? '\n' : '\n\n';
130
- next = `${content}${sep}${block}\n`;
130
+ // No trailing newline: oxfmt strips it from .md files, so emitting one
131
+ // makes a freshly patched CLAUDE.md fail `format:check` (read-only) until
132
+ // the next `check` auto-fix. Keep the block flush with EOF.
133
+ next = `${content}${sep}${block}`;
131
134
  }
132
135
  if (next === content)
133
136
  return { file, patched: false, replacements: 0 };
@@ -8,6 +8,7 @@ exports.deriveTicketDbName = deriveTicketDbName;
8
8
  exports.resolveLayout = resolveLayout;
9
9
  const fs_1 = require("fs");
10
10
  const path_1 = require("path");
11
+ const dev_identity_1 = require("./dev-identity");
11
12
  const workspace_integration_1 = require("./workspace-integration");
12
13
  /**
13
14
  * Detect whether the API project still has the legacy hardcoded `port: 3000`.
@@ -85,20 +86,32 @@ function deriveTicketDbName(devDbName, ticketId) {
85
86
  /**
86
87
  * Resolve layout starting from `cwd`. Walks up to find a workspace if
87
88
  * cwd is inside `projects/api/` or `projects/app/`.
89
+ *
90
+ * A workspace marker alone never wins: it only selects the monorepo layout
91
+ * when `projects/api` or `projects/app` actually exists. Otherwise we fall
92
+ * through to the standalone probe — a single-package repo may legitimately
93
+ * carry a settings-only `pnpm-workspace.yaml` (pnpm 10/11 keeps `overrides`
94
+ * and build allowlists there), and an npm workspace may use `packages/*`
95
+ * rather than the lt `projects/*` convention.
88
96
  */
89
97
  function resolveLayout(cwd, filesystem) {
90
98
  const subContext = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
91
99
  if (subContext)
92
100
  return monorepoLayout(subContext.workspaceRoot);
93
101
  const layout = (0, workspace_integration_1.detectWorkspaceLayout)(cwd, filesystem);
94
- if (layout.hasWorkspace)
95
- return monorepoLayout(layout.workspaceDir);
102
+ if (layout.hasWorkspace) {
103
+ const mono = monorepoLayout(layout.workspaceDir);
104
+ if (mono.apiDir || mono.appDir)
105
+ return mono;
106
+ }
96
107
  const workspaceRoot = (0, workspace_integration_1.findWorkspaceRoot)(cwd, filesystem);
97
- if (workspaceRoot)
98
- return monorepoLayout(workspaceRoot);
99
- // Standalone project — figure out if it's API or App.
100
- const isApi = (0, fs_1.existsSync)((0, path_1.join)(cwd, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(cwd, 'nest-cli.json'));
101
- const isApp = (0, fs_1.existsSync)((0, path_1.join)(cwd, 'nuxt.config.ts'));
108
+ if (workspaceRoot) {
109
+ const mono = monorepoLayout(workspaceRoot);
110
+ if (mono.apiDir || mono.appDir)
111
+ return mono;
112
+ }
113
+ // Standalone project — API-only, App-only, or a single repo holding both.
114
+ const { isApi, isApp } = (0, dev_identity_1.detectStandaloneKind)(cwd);
102
115
  return {
103
116
  apiDir: isApi ? cwd : null,
104
117
  appDir: isApp ? cwd : null,
@@ -118,7 +118,7 @@ function loadRegistry() {
118
118
  try {
119
119
  const parsed = JSON.parse((0, fs_1.readFileSync)(REGISTRY_PATH, 'utf8'));
120
120
  if (parsed && typeof parsed === 'object' && parsed.version === 1 && typeof parsed.projects === 'object') {
121
- return parsed;
121
+ return normalizeRegistry(parsed);
122
122
  }
123
123
  }
124
124
  catch (_a) {
@@ -184,6 +184,22 @@ function takenInternalPorts(reg, excludeSlug) {
184
184
  }
185
185
  return ports;
186
186
  }
187
+ /**
188
+ * Drop a `dbName` from any entry that has no `api` subdomain. An App-only
189
+ * project has no database, yet older registries (written before `lt dev up`
190
+ * stopped persisting one for App-only stacks) still carry a derived name.
191
+ * Normalizing once on load lets every reader test `entry.dbName` alone,
192
+ * instead of repeating `entry.dbName && entry.subdomains.api` at each display.
193
+ */
194
+ function normalizeRegistry(reg) {
195
+ var _a;
196
+ for (const entry of Object.values(reg.projects)) {
197
+ if ((entry === null || entry === void 0 ? void 0 : entry.dbName) && !((_a = entry.subdomains) === null || _a === void 0 ? void 0 : _a.api)) {
198
+ delete entry.dbName;
199
+ }
200
+ }
201
+ return reg;
202
+ }
187
203
  /** True if two paths resolve to the same location (normalising symlinks, e.g. /var → /private/var). */
188
204
  function sameRealPath(a, b) {
189
205
  try {
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.TEST_INITIAL_ADMIN_ENV = void 0;
12
13
  exports.autoShardCount = autoShardCount;
13
14
  exports.bringUpTestSession = bringUpTestSession;
14
15
  exports.hasTestSession = hasTestSession;
@@ -47,6 +48,7 @@ const caddy_1 = require("./caddy");
47
48
  const dev_env_1 = require("./dev-env");
48
49
  const dev_env_bridge_1 = require("./dev-env-bridge");
49
50
  const dev_identity_1 = require("./dev-identity");
51
+ const dev_package_manager_1 = require("./dev-package-manager");
50
52
  const dev_patches_1 = require("./dev-patches");
51
53
  const dev_process_1 = require("./dev-process");
52
54
  const dev_project_1 = require("./dev-project");
@@ -56,6 +58,25 @@ const TEST_APP_LOG = 'app.test.log';
56
58
  const TEST_BRIDGE_FILE = '.env.test';
57
59
  /** Internal port band for the test stack — distinct from the dev band (4000+). */
58
60
  const TEST_PORT_BASE = 4500;
61
+ /**
62
+ * Throwaway initial-admin credentials for the isolated `lt dev test` database.
63
+ *
64
+ * The nest-server core auto-parses these `NSC__…` vars into
65
+ * `systemSetup.initialAdmin.*` and seeds the admin ONCE on an empty DB (it never
66
+ * overwrites an existing admin), so a set-up system exists before the suite runs.
67
+ * A fresh template project's standard auth E2E specs assume such a system — they
68
+ * do no self-setup — so without this they fail LOCALLY on the empty per-run DB.
69
+ * These are the exact values the lt-monorepo template CI uses
70
+ * (`.gitlab-ci.yml` / `.github/workflows/test.yml`), keeping CI ↔ local parity.
71
+ *
72
+ * Injected ONLY into the `lt dev test` API process (its DB is fresh + discarded
73
+ * per run), NEVER into `lt dev up` — no surprise admin in the persistent dev DB.
74
+ */
75
+ exports.TEST_INITIAL_ADMIN_ENV = {
76
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL: 'ci-admin@test.com',
77
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME: 'CI Admin',
78
+ NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD: 'CiThrowawayAdmin123!',
79
+ };
59
80
  /**
60
81
  * Heuristic for the default local shard count (`--shard auto` / bare `--shard`).
61
82
  *
@@ -189,20 +210,27 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
189
210
  dbName,
190
211
  identity: testIdentity,
191
212
  });
192
- const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
193
213
  const pids = {};
194
- // --- API: compiled (`node dist`) for stability; fall back to `pnpm start`.
195
- // `skipBuild` (sibling shards) reuses the dist the first shard produced. ---
214
+ // --- API: compiled (`node dist`) for stability; fall back to the project's
215
+ // own dev start script. `skipBuild` (sibling shards) reuses the dist the
216
+ // first shard produced. Per-component PM detection mirrors `lt dev up`:
217
+ // a monorepo with an npm api and a pnpm app must drive each correctly. ---
196
218
  if (layout.apiDir && apiPort) {
219
+ const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
197
220
  let build = 0;
198
221
  if (!skipBuild) {
199
222
  log.info(log.dim('Building API (compiled, for stable long runs) …'));
200
- build = yield (0, dev_process_1.runChildInherit)(pnpmBin, ['run', 'build'], { cwd: layout.apiDir, env: process.env });
223
+ build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: layout.apiDir, env: process.env });
201
224
  }
202
225
  const entry = ['dist/src/main.js', 'dist/main.js']
203
226
  .map((rel) => (0, path_1.join)(layout.apiDir, rel))
204
227
  .find((p) => (0, fs_1.existsSync)(p));
205
- const apiEnv = Object.assign(Object.assign({}, devEnv.api.env), { NODE_ENV: 'local' });
228
+ // Seed a throwaway initial admin into the fresh, isolated test DB so the
229
+ // standard auth E2E specs run against a set-up system — locally exactly like
230
+ // the lt-monorepo template CI. Defaults first so an explicitly inherited
231
+ // `NSC__…INITIAL_ADMIN__…` still wins (deliberate override respected). Only
232
+ // reached from `lt dev test` — `lt dev up` never calls bringUpTestSession.
233
+ const apiEnv = Object.assign(Object.assign(Object.assign({}, exports.TEST_INITIAL_ADMIN_ENV), devEnv.api.env), { NODE_ENV: 'local' });
206
234
  let apiSpawn;
207
235
  if (build === 0 && entry) {
208
236
  apiSpawn = (0, dev_process_1.spawnDetached)('node', [entry], {
@@ -212,8 +240,8 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
212
240
  });
213
241
  }
214
242
  else {
215
- log.warn('compiled API not available — falling back to `pnpm start` (ts-node).');
216
- apiSpawn = (0, dev_process_1.spawnDetached)(pnpmBin, ['start'], {
243
+ log.warn(`compiled API not available — falling back to \`${apiPm.bin} start\` (ts-node).`);
244
+ apiSpawn = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
217
245
  cwd: layout.apiDir,
218
246
  env: apiEnv,
219
247
  logFile: (0, path_1.join)(layout.root, '.lt-dev', names.apiLog),
@@ -231,10 +259,14 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
231
259
  // must be a cross-subdomain DOMAIN cookie — see the project's parseCookieHeader).
232
260
  // Rebuilt every run so the suite never hits stale code (no build-skip / reuse). ---
233
261
  if (layout.appDir && appPort) {
262
+ const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
234
263
  let appBuild = 0;
235
264
  if (!skipBuild) {
236
265
  log.info(log.dim('Building App (nuxt build, for speed + prod-fidelity) …'));
237
- appBuild = yield (0, dev_process_1.runChildInherit)(pnpmBin, ['run', 'build'], { cwd: layout.appDir, env: devEnv.app.env });
266
+ appBuild = yield (0, dev_process_1.runChildInherit)(appPm.bin, appPm.runScript('build'), {
267
+ cwd: layout.appDir,
268
+ env: devEnv.app.env,
269
+ });
238
270
  }
239
271
  const appEntry = ['.output/server/index.mjs']
240
272
  .map((rel) => (0, path_1.join)(layout.appDir, rel))
@@ -248,8 +280,8 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
248
280
  });
249
281
  }
250
282
  else {
251
- log.warn('built app not available — falling back to `pnpm dev` (slower: cold-compiles routes).');
252
- appSpawn = (0, dev_process_1.spawnDetached)(pnpmBin, ['dev'], {
283
+ log.warn(`built app not available — falling back to \`${appPm.bin} dev\` (slower: cold-compiles routes).`);
284
+ appSpawn = (0, dev_process_1.spawnDetached)(appPm.bin, appPm.runScript('dev'), {
253
285
  cwd: layout.appDir,
254
286
  env: devEnv.app.env,
255
287
  logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
@@ -281,7 +313,10 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
281
313
  if (!apiReady)
282
314
  log.warn(`Test API did not answer 2xx on ${apiUrl}/meta within 120s — the first specs may skip.`);
283
315
  }
284
- return { apiUrl, appEnv: devEnv.app.env, appUrl, dbName, pids, testIdentity };
316
+ // Expose THIS stack's API log path so the caller can point the auth E2E specs
317
+ // (via NEST_SERVER_LOG) at the exact isolated log — correct per shard.
318
+ const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
319
+ return { apiLogPath, apiUrl, appEnv: devEnv.app.env, appUrl, dbName, pids, testIdentity };
285
320
  });
286
321
  }
287
322
  /** True when a test session file exists (used by status/down). */
@@ -338,15 +373,22 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
338
373
  // suite runs under concurrent sharded load, so it can relax navigation /
339
374
  // test timeouts (N built SSR servers + N Chromium saturate the CPU and slow
340
375
  // every navigation) without loosening them for serial runs.
341
- const env = Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` });
376
+ const env = Object.assign(Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` }), (ctx.apiLogPath ? { NEST_SERVER_LOG: ctx.apiLogPath } : {}));
342
377
  const logFile = (0, path_1.join)(layout.root, '.lt-dev', `shard.${index}.test.log`);
343
- // Invoke Playwright DIRECTLY via `pnpm exec` (NOT `pnpm run test:e2e -- …`):
344
- // forwarding option flags through `pnpm run`'s `--` is unreliable — pnpm
345
- // passed the separator on to Playwright, which then read `--shard`/
346
- // `--reporter` as file FILTERS (not options) → every shard ran the whole
347
- // suite. `pnpm exec` hands args straight to the binary (mirrors CI).
348
- const args = ['exec', 'playwright', 'test', `--shard=${index}/${total}`, '--reporter=line', ...opts.forwarded];
349
- const code = yield (0, dev_process_1.runChildToFile)(opts.pnpmBin, args, { cwd: appDir, env, logFile });
378
+ // Invoke Playwright DIRECTLY via the manager's `exec` (NOT `<pm> run
379
+ // test:e2e -- …`): forwarding option flags through `<pm> run`'s `--`
380
+ // is unreliable — pnpm passed the separator on to Playwright, which
381
+ // then read `--shard` / `--reporter` as file FILTERS (not options) →
382
+ // every shard ran the whole suite. `<pm> exec` hands args straight
383
+ // to the binary (mirrors CI); the helper inserts `--` for npm so
384
+ // those flags don't get re-parsed as npm's own.
385
+ const args = opts.pm.exec('playwright', [
386
+ 'test',
387
+ `--shard=${index}/${total}`,
388
+ '--reporter=line',
389
+ ...opts.forwarded,
390
+ ]);
391
+ const code = yield (0, dev_process_1.runChildToFile)(opts.pm.bin, args, { cwd: appDir, env, logFile });
350
392
  return { code, index, logFile };
351
393
  })));
352
394
  // Aggregate per-shard exit codes into a single result.
@@ -8,8 +8,8 @@ exports.dropDatabase = dropDatabase;
8
8
  exports.gitBranchExists = gitBranchExists;
9
9
  exports.gitFetch = gitFetch;
10
10
  exports.gitMainRepoRoot = gitMainRepoRoot;
11
+ exports.installWorktreeDeps = installWorktreeDeps;
11
12
  exports.listWorktrees = listWorktrees;
12
- exports.pnpmInstall = pnpmInstall;
13
13
  exports.readTicketMarker = readTicketMarker;
14
14
  exports.resolveDevIdentity = resolveDevIdentity;
15
15
  exports.worktreeAdd = worktreeAdd;
@@ -43,6 +43,7 @@ const fs_1 = require("fs");
43
43
  const os_1 = require("os");
44
44
  const path_1 = require("path");
45
45
  const dev_identity_1 = require("./dev-identity");
46
+ const dev_package_manager_1 = require("./dev-package-manager");
46
47
  const dev_patches_1 = require("./dev-patches");
47
48
  const dev_project_1 = require("./dev-project");
48
49
  const dev_state_1 = require("./dev-state");
@@ -183,6 +184,16 @@ function gitMainRepoRoot(cwd) {
183
184
  const commonDir = git(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
184
185
  return (0, path_1.dirname)(commonDir);
185
186
  }
187
+ /**
188
+ * Install dependencies in a freshly-created worktree. Auto-detects the
189
+ * project's package manager from its lockfile (pnpm hard-links from the
190
+ * shared store → fast; npm + yarn install normally). Falls back to pnpm
191
+ * for fresh scaffolds without a lockfile yet.
192
+ */
193
+ function installWorktreeDeps(dir) {
194
+ const pm = (0, dev_package_manager_1.pickPackageManager)(dir);
195
+ (0, child_process_1.execFileSync)(pm.bin, pm.installArgs, { cwd: dir, stdio: 'inherit' });
196
+ }
186
197
  /** List all worktrees of the repo (parsed from `git worktree list --porcelain`). */
187
198
  function listWorktrees(repoDir) {
188
199
  let out = '';
@@ -208,11 +219,6 @@ function listWorktrees(repoDir) {
208
219
  result.push(finalizeWorktree(current));
209
220
  return result;
210
221
  }
211
- /** Install dependencies in a freshly-created worktree (pnpm hard-links from the shared store → fast). */
212
- function pnpmInstall(dir) {
213
- const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
214
- (0, child_process_1.execFileSync)(pnpmBin, ['install'], { cwd: dir, stdio: 'inherit' });
215
- }
216
222
  /** Read the ticket id this worktree is tagged with, or null. */
217
223
  function readTicketMarker(root) {
218
224
  const file = (0, path_1.join)(root, dev_state_1.paths.sessionDir, TICKET_MARKER);