@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
|
@@ -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
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 (
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
package/build/lib/dev-patches.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/build/lib/dev-project.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,
|
package/build/lib/dev-state.js
CHANGED
|
@@ -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
|
|
195
|
-
// `skipBuild` (sibling shards) reuses the dist the
|
|
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)(
|
|
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
|
-
|
|
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(
|
|
216
|
-
apiSpawn = (0, dev_process_1.spawnDetached)(
|
|
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)(
|
|
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(
|
|
252
|
-
appSpawn = (0, dev_process_1.spawnDetached)(
|
|
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
|
-
|
|
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 `
|
|
344
|
-
// forwarding option flags through
|
|
345
|
-
// passed the separator on to Playwright, which
|
|
346
|
-
// `--reporter` as file FILTERS (not options) →
|
|
347
|
-
// suite.
|
|
348
|
-
|
|
349
|
-
|
|
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.
|
package/build/lib/dev-ticket.js
CHANGED
|
@@ -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);
|