@lenne.tech/cli 1.20.0 → 1.22.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 +25 -0
- package/build/commands/dev/dev.js +32 -0
- package/build/commands/dev/doctor.js +110 -0
- package/build/commands/dev/down.js +76 -0
- package/build/commands/dev/install.js +113 -0
- package/build/commands/dev/migrate.js +96 -0
- package/build/commands/dev/status.js +153 -0
- package/build/commands/dev/test.js +191 -0
- package/build/commands/dev/up.js +228 -0
- package/build/commands/fullstack/init.js +36 -2
- package/build/commands/status.js +37 -6
- package/build/extensions/server.js +14 -4
- package/build/lib/caddy.js +169 -0
- package/build/lib/dev-env-bridge.js +109 -0
- package/build/lib/dev-env.js +35 -0
- package/build/lib/dev-identity.js +100 -0
- package/build/lib/dev-migrate-helper.js +81 -0
- package/build/lib/dev-patches.js +190 -0
- package/build/lib/dev-process.js +152 -0
- package/build/lib/dev-project.js +86 -0
- package/build/lib/dev-state.js +152 -0
- package/docs/commands.md +202 -0
- package/package.json +7 -10
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const caddy_1 = require("../../lib/caddy");
|
|
15
|
+
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
16
|
+
const dev_identity_1 = require("../../lib/dev-identity");
|
|
17
|
+
const dev_project_1 = require("../../lib/dev-project");
|
|
18
|
+
const dev_state_1 = require("../../lib/dev-state");
|
|
19
|
+
/**
|
|
20
|
+
* One-shot E2E convenience wrapper.
|
|
21
|
+
*
|
|
22
|
+
* Ensures the project is up under `lt dev`, then runs the test command
|
|
23
|
+
* with the ENV bridge loaded, and optionally tears the session down at
|
|
24
|
+
* the end. Useful for TDD loops, CI reproduction, and "just test it"
|
|
25
|
+
* workflows where the developer doesn't want to remember three commands.
|
|
26
|
+
*
|
|
27
|
+
* Behaviour:
|
|
28
|
+
* 1. If no `lt dev up` session is alive: run `lt dev up` first.
|
|
29
|
+
* 2. Wait for the App URL to respond (best-effort, ~30s timeout).
|
|
30
|
+
* 3. Inherit the env from `<root>/.lt-dev/.env` (so VS Code IDE-style
|
|
31
|
+
* runners get the same setup) and spawn the test command in the
|
|
32
|
+
* App project.
|
|
33
|
+
* 4. With `--teardown` (default false): run `lt dev down` after.
|
|
34
|
+
*
|
|
35
|
+
* Usage:
|
|
36
|
+
* lt dev test # ensure up + run pnpm test:e2e in app
|
|
37
|
+
* lt dev test --api # run API tests instead (pnpm test in api)
|
|
38
|
+
* lt dev test --teardown # plus stop session after
|
|
39
|
+
* lt dev test --debug # PWDEBUG=1 + headed mode
|
|
40
|
+
* lt dev test -- <args> # forward args to the test runner
|
|
41
|
+
* lt dev test -- --ui crm-login.spec.ts
|
|
42
|
+
*/
|
|
43
|
+
const TestCommand = {
|
|
44
|
+
alias: ['t'],
|
|
45
|
+
description: 'Ensure up + run E2E tests with lt dev env',
|
|
46
|
+
hidden: false,
|
|
47
|
+
name: 'test',
|
|
48
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
var _a;
|
|
50
|
+
const { filesystem, parameters, print: { colors, error, info, success }, } = toolbox;
|
|
51
|
+
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
52
|
+
if (!layout.apiDir && !layout.appDir) {
|
|
53
|
+
error('No API or App project detected at this path.');
|
|
54
|
+
if (!parameters.options.fromGluegunMenu)
|
|
55
|
+
process.exit(1);
|
|
56
|
+
return 'dev test: not a project';
|
|
57
|
+
}
|
|
58
|
+
const apiMode = Boolean(parameters.options.api);
|
|
59
|
+
const teardown = Boolean(parameters.options.teardown);
|
|
60
|
+
const debug = Boolean(parameters.options.debug);
|
|
61
|
+
// Forwarded args after `--` (gluegun puts them in parameters.array).
|
|
62
|
+
const forwarded = parameters.array || [];
|
|
63
|
+
const targetDir = apiMode ? layout.apiDir : layout.appDir;
|
|
64
|
+
if (!targetDir) {
|
|
65
|
+
error(`Cannot run ${apiMode ? 'API' : 'App'} tests — no ${apiMode ? 'apiDir' : 'appDir'} in layout.`);
|
|
66
|
+
if (!parameters.options.fromGluegunMenu)
|
|
67
|
+
process.exit(1);
|
|
68
|
+
return 'dev test: target missing';
|
|
69
|
+
}
|
|
70
|
+
const identity = (0, dev_identity_1.buildIdentity)(layout.root);
|
|
71
|
+
// Pre-flight: Caddy + ensure up.
|
|
72
|
+
if (!apiMode) {
|
|
73
|
+
if (!(yield (0, caddy_1.caddyAvailable)())) {
|
|
74
|
+
error('caddy is not installed. Run `lt dev install` first.');
|
|
75
|
+
if (!parameters.options.fromGluegunMenu)
|
|
76
|
+
process.exit(1);
|
|
77
|
+
return 'dev test: caddy missing';
|
|
78
|
+
}
|
|
79
|
+
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
|
|
80
|
+
error('caddy daemon not running. `brew services start caddy` first.');
|
|
81
|
+
if (!parameters.options.fromGluegunMenu)
|
|
82
|
+
process.exit(1);
|
|
83
|
+
return 'dev test: caddy daemon down';
|
|
84
|
+
}
|
|
85
|
+
const reg = (0, dev_state_1.loadRegistry)();
|
|
86
|
+
const entry = reg.projects[identity.slug];
|
|
87
|
+
const session = (0, dev_state_1.loadSession)(layout.root);
|
|
88
|
+
const sessionAlive = session !== null &&
|
|
89
|
+
((session.pids.api && (0, dev_state_1.isPidAlive)(session.pids.api)) || (session.pids.app && (0, dev_state_1.isPidAlive)(session.pids.app)));
|
|
90
|
+
if (!entry || !sessionAlive) {
|
|
91
|
+
info(colors.dim('Project not running — invoking `lt dev up` first ...'));
|
|
92
|
+
const upResult = yield runChild('lt', ['dev', 'up'], { cwd: layout.root, env: process.env, inherit: true });
|
|
93
|
+
if (upResult !== 0) {
|
|
94
|
+
error(`lt dev up failed (exit ${upResult}).`);
|
|
95
|
+
if (!parameters.options.fromGluegunMenu)
|
|
96
|
+
process.exit(upResult !== null && upResult !== void 0 ? upResult : 1);
|
|
97
|
+
return 'dev test: up failed';
|
|
98
|
+
}
|
|
99
|
+
// Wait for App URL to respond (best-effort).
|
|
100
|
+
const appHost = (_a = identity.subdomains.app) === null || _a === void 0 ? void 0 : _a.hostname;
|
|
101
|
+
if (appHost) {
|
|
102
|
+
info(colors.dim(`Waiting for https://${appHost} to respond ...`));
|
|
103
|
+
yield waitForUrl(`https://${appHost}`, 30000);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Build env: process.env + bridge file (bridge wins for keys it defines).
|
|
108
|
+
const env = Object.assign(Object.assign({}, process.env), readBridgeEnv(layout.root));
|
|
109
|
+
if (debug) {
|
|
110
|
+
env.PWDEBUG = '1';
|
|
111
|
+
env.HEADED = '1';
|
|
112
|
+
}
|
|
113
|
+
// Pick the runner.
|
|
114
|
+
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
115
|
+
const args = apiMode ? ['run', 'test:e2e', ...forwarded] : ['run', 'test:e2e', ...forwarded];
|
|
116
|
+
info('');
|
|
117
|
+
info(colors.bold(`Running ${apiMode ? 'API' : 'App'} E2E tests for "${identity.slug}"`));
|
|
118
|
+
info(colors.dim(` ${pnpmBin} ${args.join(' ')}`));
|
|
119
|
+
info(colors.dim(` cwd: ${targetDir}`));
|
|
120
|
+
info('');
|
|
121
|
+
const exitCode = yield runChild(pnpmBin, args, { cwd: targetDir, env, inherit: true });
|
|
122
|
+
if (teardown) {
|
|
123
|
+
info('');
|
|
124
|
+
info(colors.dim('Tearing down lt dev session ...'));
|
|
125
|
+
yield runChild('lt', ['dev', 'down'], { cwd: layout.root, env: process.env, inherit: true });
|
|
126
|
+
}
|
|
127
|
+
if (exitCode === 0)
|
|
128
|
+
success('Tests passed.');
|
|
129
|
+
else
|
|
130
|
+
error(`Tests failed (exit ${exitCode}).`);
|
|
131
|
+
if (!parameters.options.fromGluegunMenu)
|
|
132
|
+
process.exit(exitCode !== null && exitCode !== void 0 ? exitCode : 1);
|
|
133
|
+
return `dev test: exit=${exitCode}`;
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Read the .lt-dev/.env bridge file as a key/value map.
|
|
138
|
+
* Returns an empty object if the file is missing.
|
|
139
|
+
*/
|
|
140
|
+
function readBridgeEnv(root) {
|
|
141
|
+
const file = (0, dev_env_bridge_1.envBridgePath)(root);
|
|
142
|
+
if (!(0, fs_1.existsSync)(file))
|
|
143
|
+
return {};
|
|
144
|
+
const out = {};
|
|
145
|
+
for (const line of (0, fs_1.readFileSync)(file, 'utf8').split(/\r?\n/)) {
|
|
146
|
+
const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
|
|
147
|
+
if (m)
|
|
148
|
+
out[m[1]] = m[2];
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** Spawn a child synchronously (waits for exit), inheriting stdio when requested. */
|
|
153
|
+
function runChild(cmd, args, opts) {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
156
|
+
cwd: opts.cwd,
|
|
157
|
+
env: opts.env,
|
|
158
|
+
stdio: opts.inherit ? 'inherit' : 'pipe',
|
|
159
|
+
});
|
|
160
|
+
child.on('error', () => resolve(1));
|
|
161
|
+
child.on('close', (code) => resolve(code));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Poll a URL until it responds or timeout elapses. */
|
|
165
|
+
function waitForUrl(url, timeoutMs) {
|
|
166
|
+
const start = Date.now();
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const tick = () => {
|
|
169
|
+
var _a;
|
|
170
|
+
const child = (0, child_process_1.spawn)('curl', ['-sk', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '2', url], {
|
|
171
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
|
+
});
|
|
173
|
+
let status = '';
|
|
174
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (status += String(b)));
|
|
175
|
+
child.on('close', () => {
|
|
176
|
+
if (/^[1-5]\d\d$/.test(status.trim()))
|
|
177
|
+
return resolve(true);
|
|
178
|
+
if (Date.now() - start > timeoutMs)
|
|
179
|
+
return resolve(false);
|
|
180
|
+
setTimeout(tick, 500);
|
|
181
|
+
});
|
|
182
|
+
child.on('error', () => {
|
|
183
|
+
if (Date.now() - start > timeoutMs)
|
|
184
|
+
return resolve(false);
|
|
185
|
+
setTimeout(tick, 500);
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
tick();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
module.exports = TestCommand;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const caddy_1 = require("../../lib/caddy");
|
|
15
|
+
const dev_env_1 = require("../../lib/dev-env");
|
|
16
|
+
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
17
|
+
const dev_identity_1 = require("../../lib/dev-identity");
|
|
18
|
+
const dev_patches_1 = require("../../lib/dev-patches");
|
|
19
|
+
const dev_process_1 = require("../../lib/dev-process");
|
|
20
|
+
const dev_project_1 = require("../../lib/dev-project");
|
|
21
|
+
const dev_state_1 = require("../../lib/dev-state");
|
|
22
|
+
/**
|
|
23
|
+
* Start API + App behind Caddy with project-specific URLs.
|
|
24
|
+
*
|
|
25
|
+
* Pre-flight:
|
|
26
|
+
* - Caddy must be installed and running (otherwise points to install)
|
|
27
|
+
* - No existing `lt dev up` session for THIS project
|
|
28
|
+
* - Internal upstream ports must be free
|
|
29
|
+
*
|
|
30
|
+
* Process:
|
|
31
|
+
* 1. Resolve layout + identity
|
|
32
|
+
* 2. Allocate (or reuse) internal upstream ports
|
|
33
|
+
* 3. Upsert Caddy block + reload
|
|
34
|
+
* 4. Spawn API + App detached, log into `<root>/.lt-dev/{api,app}.log`
|
|
35
|
+
* 5. Persist registry entry + session state
|
|
36
|
+
*
|
|
37
|
+
* Env-vars injected (see lib/dev-env.ts):
|
|
38
|
+
* PORT, BASE_URL, APP_URL, NUXT_API_URL, NUXT_PUBLIC_API_URL,
|
|
39
|
+
* NUXT_PUBLIC_SITE_URL, NUXT_PUBLIC_STORAGE_PREFIX,
|
|
40
|
+
* NSC__MONGOOSE__URI, NSC__BASE_URL, NSC__APP_URL, DATABASE_URL,
|
|
41
|
+
* NUXT_PUBLIC_API_PROXY=false (Caddy makes vite-proxy obsolete).
|
|
42
|
+
*/
|
|
43
|
+
const UpCommand = {
|
|
44
|
+
alias: ['u'],
|
|
45
|
+
description: 'Start API + App behind Caddy',
|
|
46
|
+
hidden: false,
|
|
47
|
+
name: 'up',
|
|
48
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
var _a, _b, _c, _d, _e, _f;
|
|
50
|
+
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
51
|
+
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
52
|
+
if (!layout.apiDir && !layout.appDir) {
|
|
53
|
+
error('No API or App project detected at this path. Run `lt dev migrate` first.');
|
|
54
|
+
if (!parameters.options.fromGluegunMenu)
|
|
55
|
+
process.exit(1);
|
|
56
|
+
return 'dev up: not a project';
|
|
57
|
+
}
|
|
58
|
+
// Pre-flight: Caddy
|
|
59
|
+
if (!(yield (0, caddy_1.caddyAvailable)())) {
|
|
60
|
+
error('caddy is not installed. Run `lt dev install` first.');
|
|
61
|
+
if (!parameters.options.fromGluegunMenu)
|
|
62
|
+
process.exit(1);
|
|
63
|
+
return 'dev up: caddy missing';
|
|
64
|
+
}
|
|
65
|
+
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
|
|
66
|
+
error('caddy daemon is not running. Start it: `brew services start caddy`.');
|
|
67
|
+
if (!parameters.options.fromGluegunMenu)
|
|
68
|
+
process.exit(1);
|
|
69
|
+
return 'dev up: caddy daemon down';
|
|
70
|
+
}
|
|
71
|
+
const identity = (0, dev_identity_1.buildIdentity)(layout.root);
|
|
72
|
+
const dbName = (0, dev_project_1.deriveDbName)(layout.apiDir, identity.slug);
|
|
73
|
+
// Sanft auto-migrate sichere Operationen (ohne Code-Modifikation):
|
|
74
|
+
// CLAUDE.md-URL-Block einfügen + .gitignore ergänzen.
|
|
75
|
+
// Code-Patches (config.env.ts, nuxt.config.ts) bleiben explizit `lt dev migrate`.
|
|
76
|
+
{
|
|
77
|
+
const claudeCandidates = [
|
|
78
|
+
(0, path_1.join)(layout.root, 'CLAUDE.md'),
|
|
79
|
+
...(layout.apiDir ? [(0, path_1.join)(layout.apiDir, 'CLAUDE.md')] : []),
|
|
80
|
+
...(layout.appDir ? [(0, path_1.join)(layout.appDir, 'CLAUDE.md')] : []),
|
|
81
|
+
];
|
|
82
|
+
const patched = claudeCandidates.map((f) => (0, dev_patches_1.patchClaudeMd)(f, { dbName, identity })).filter((r) => r.patched);
|
|
83
|
+
if (patched.length > 0) {
|
|
84
|
+
info(colors.dim(`updated CLAUDE.md URL block in ${patched.length} file(s)`));
|
|
85
|
+
}
|
|
86
|
+
if ((0, dev_patches_1.addToGitignore)(layout.root, '.lt-dev/')) {
|
|
87
|
+
info(colors.dim('added `.lt-dev/` to .gitignore'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Warnung bei Legacy-Code (hardcoded ports) — kein Auto-Patch.
|
|
91
|
+
{
|
|
92
|
+
const legacyFiles = [];
|
|
93
|
+
if (layout.apiDir) {
|
|
94
|
+
const f = (0, dev_project_1.apiNeedsPortPatch)(layout.apiDir);
|
|
95
|
+
if (f)
|
|
96
|
+
legacyFiles.push(f);
|
|
97
|
+
}
|
|
98
|
+
if (layout.appDir)
|
|
99
|
+
legacyFiles.push(...(0, dev_project_1.appNeedsPortPatch)(layout.appDir));
|
|
100
|
+
if (legacyFiles.length > 0) {
|
|
101
|
+
warning('Legacy hardcoded ports detected — Caddy will proxy correctly only after running `lt dev migrate`:');
|
|
102
|
+
legacyFiles.forEach((f) => info(colors.dim(` - ${f}`)));
|
|
103
|
+
info(colors.dim(' (Continuing — env-aware files will work; legacy files may bind on 3000/3001 and miss Caddy.)'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Already running?
|
|
107
|
+
const existingSession = (0, dev_state_1.loadSession)(layout.root);
|
|
108
|
+
if (existingSession) {
|
|
109
|
+
const apiUp = existingSession.pids.api ? (0, dev_state_1.isPidAlive)(existingSession.pids.api) : false;
|
|
110
|
+
const appUp = existingSession.pids.app ? (0, dev_state_1.isPidAlive)(existingSession.pids.app) : false;
|
|
111
|
+
if (apiUp || appUp) {
|
|
112
|
+
warning(`Already running (api pid ${(_a = existingSession.pids.api) !== null && _a !== void 0 ? _a : '-'}, app pid ${(_b = existingSession.pids.app) !== null && _b !== void 0 ? _b : '-'}).`);
|
|
113
|
+
info('Run `lt dev down` first.');
|
|
114
|
+
if (!parameters.options.fromGluegunMenu)
|
|
115
|
+
process.exit(1);
|
|
116
|
+
return 'dev up: already running';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Allocate internal ports (reuse existing if registered).
|
|
120
|
+
const reg = (0, dev_state_1.loadRegistry)();
|
|
121
|
+
const entry = reg.projects[identity.slug];
|
|
122
|
+
const taken = (0, dev_state_1.takenInternalPorts)(reg, identity.slug);
|
|
123
|
+
const apiPort = (_c = entry === null || entry === void 0 ? void 0 : entry.internalPorts.api) !== null && _c !== void 0 ? _c : (layout.apiDir ? (0, dev_state_1.allocateInternalPort)(4000, taken) : undefined);
|
|
124
|
+
if (apiPort)
|
|
125
|
+
taken.add(apiPort);
|
|
126
|
+
const appPort = (_d = entry === null || entry === void 0 ? void 0 : entry.internalPorts.app) !== null && _d !== void 0 ? _d : (layout.appDir ? (0, dev_state_1.allocateInternalPort)(4000, taken) : undefined);
|
|
127
|
+
// Pre-flight: internal ports free?
|
|
128
|
+
const portsToCheck = [apiPort, appPort].filter((p) => typeof p === 'number');
|
|
129
|
+
const snap = yield (0, dev_process_1.listenSnapshot)(portsToCheck);
|
|
130
|
+
for (const p of portsToCheck) {
|
|
131
|
+
const r = snap.get(p);
|
|
132
|
+
if (r) {
|
|
133
|
+
error(`Internal port ${p} already in use by ${r.command} (pid ${r.pid}).`);
|
|
134
|
+
if (!parameters.options.fromGluegunMenu)
|
|
135
|
+
process.exit(1);
|
|
136
|
+
return 'dev up: port in use';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Caddy block + reload.
|
|
140
|
+
const routes = [];
|
|
141
|
+
if (identity.subdomains.api && apiPort)
|
|
142
|
+
routes.push({ hostname: identity.subdomains.api.hostname, upstreamPort: apiPort });
|
|
143
|
+
if (identity.subdomains.app && appPort)
|
|
144
|
+
routes.push({ hostname: identity.subdomains.app.hostname, upstreamPort: appPort });
|
|
145
|
+
if (routes.length === 0) {
|
|
146
|
+
error('No subdomains to expose (project has neither api nor app).');
|
|
147
|
+
if (!parameters.options.fromGluegunMenu)
|
|
148
|
+
process.exit(1);
|
|
149
|
+
return 'dev up: nothing to expose';
|
|
150
|
+
}
|
|
151
|
+
(0, caddy_1.upsertProjectBlock)(identity.slug, routes);
|
|
152
|
+
const reload = yield (0, caddy_1.reloadCaddy)();
|
|
153
|
+
if (!reload.ok) {
|
|
154
|
+
error(`caddy reload failed:\n${reload.stderr}`);
|
|
155
|
+
if (!parameters.options.fromGluegunMenu)
|
|
156
|
+
process.exit(1);
|
|
157
|
+
return 'dev up: caddy reload failed';
|
|
158
|
+
}
|
|
159
|
+
info('');
|
|
160
|
+
info(colors.bold(`Starting "${identity.slug}"`));
|
|
161
|
+
if (identity.subdomains.app)
|
|
162
|
+
info(` app: https://${identity.subdomains.app.hostname} → 127.0.0.1:${appPort}`);
|
|
163
|
+
if (identity.subdomains.api)
|
|
164
|
+
info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}`);
|
|
165
|
+
info(` db: mongodb://127.0.0.1/${dbName}`);
|
|
166
|
+
info('');
|
|
167
|
+
// Build env per process.
|
|
168
|
+
const devEnv = (0, dev_env_1.buildDevEnv)({
|
|
169
|
+
apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
|
|
170
|
+
appInternalPort: appPort !== null && appPort !== void 0 ? appPort : 0,
|
|
171
|
+
baseEnv: process.env,
|
|
172
|
+
dbName,
|
|
173
|
+
identity,
|
|
174
|
+
});
|
|
175
|
+
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
176
|
+
const pids = {};
|
|
177
|
+
if (layout.apiDir && (0, fs_1.existsSync)((0, path_1.join)(layout.apiDir, 'package.json')) && apiPort) {
|
|
178
|
+
const apiPid = (0, dev_process_1.spawnDetached)(pnpmBin, ['start'], {
|
|
179
|
+
cwd: layout.apiDir,
|
|
180
|
+
env: devEnv.api.env,
|
|
181
|
+
logFile: (0, path_1.join)(layout.root, '.lt-dev', 'api.log'),
|
|
182
|
+
});
|
|
183
|
+
if (apiPid)
|
|
184
|
+
pids.api = apiPid;
|
|
185
|
+
}
|
|
186
|
+
if (layout.appDir && (0, fs_1.existsSync)((0, path_1.join)(layout.appDir, 'package.json')) && appPort) {
|
|
187
|
+
const appPid = (0, dev_process_1.spawnDetached)(pnpmBin, ['dev'], {
|
|
188
|
+
cwd: layout.appDir,
|
|
189
|
+
env: devEnv.app.env,
|
|
190
|
+
logFile: (0, path_1.join)(layout.root, '.lt-dev', 'app.log'),
|
|
191
|
+
});
|
|
192
|
+
if (appPid)
|
|
193
|
+
pids.app = appPid;
|
|
194
|
+
}
|
|
195
|
+
// Persist.
|
|
196
|
+
const subdomainMap = {};
|
|
197
|
+
for (const [k, v] of Object.entries(identity.subdomains))
|
|
198
|
+
subdomainMap[k] = v.hostname;
|
|
199
|
+
reg.projects[identity.slug] = {
|
|
200
|
+
dbName,
|
|
201
|
+
internalPorts: { api: apiPort, app: appPort },
|
|
202
|
+
lastUsedAt: new Date().toISOString(),
|
|
203
|
+
path: layout.root,
|
|
204
|
+
subdomains: subdomainMap,
|
|
205
|
+
};
|
|
206
|
+
(0, dev_state_1.saveRegistry)(reg);
|
|
207
|
+
(0, dev_state_1.saveSession)(layout.root, { pids, startedAt: new Date().toISOString() });
|
|
208
|
+
// Write the ENV bridge so external tools (Playwright, IDE test runners,
|
|
209
|
+
// custom shell scripts) can pick up the URLs without inheriting our shell.
|
|
210
|
+
const bridgePath = (0, dev_env_bridge_1.writeEnvBridge)(layout.root, devEnv, dbName);
|
|
211
|
+
info(colors.dim(`ENV bridge: ${bridgePath}`));
|
|
212
|
+
success(`Started: api pid=${(_e = pids.api) !== null && _e !== void 0 ? _e : '-'}, app pid=${(_f = pids.app) !== null && _f !== void 0 ? _f : '-'}`);
|
|
213
|
+
info(colors.dim('Logs: <root>/.lt-dev/api.log, <root>/.lt-dev/app.log'));
|
|
214
|
+
info(colors.dim('Stop with: lt dev down'));
|
|
215
|
+
// Best-effort: kill orphaned children if neither spawned (unlikely, but tidy).
|
|
216
|
+
if (Object.keys(pids).length === 0) {
|
|
217
|
+
warning('Nothing was spawned. Check package.json scripts (`start` for api, `dev` for app).');
|
|
218
|
+
if (pids.api)
|
|
219
|
+
(0, dev_process_1.killProcessGroup)(pids.api);
|
|
220
|
+
if (pids.app)
|
|
221
|
+
(0, dev_process_1.killProcessGroup)(pids.app);
|
|
222
|
+
}
|
|
223
|
+
if (!parameters.options.fromGluegunMenu)
|
|
224
|
+
process.exit();
|
|
225
|
+
return `dev up: api=${pids.api}, app=${pids.app}`;
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
module.exports = UpCommand;
|
|
@@ -12,6 +12,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const gluegun_1 = require("gluegun");
|
|
16
|
+
const caddy_1 = require("../../lib/caddy");
|
|
17
|
+
const dev_migrate_helper_1 = require("../../lib/dev-migrate-helper");
|
|
18
|
+
const dev_project_1 = require("../../lib/dev-project");
|
|
15
19
|
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
16
20
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
17
21
|
const add_api_1 = __importDefault(require("./add-api"));
|
|
@@ -27,7 +31,7 @@ const NewCommand = {
|
|
|
27
31
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
28
32
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
29
33
|
// Retrieve the tools we need
|
|
30
|
-
const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
|
|
34
|
+
const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { colors, error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
|
|
31
35
|
// Start timer
|
|
32
36
|
const timer = system.startTimer();
|
|
33
37
|
// Info
|
|
@@ -637,6 +641,22 @@ const NewCommand = {
|
|
|
637
641
|
return;
|
|
638
642
|
}
|
|
639
643
|
}
|
|
644
|
+
// Best-effort `lt dev migrate` so the workspace is ready for `lt dev up`
|
|
645
|
+
// out-of-the-box: registers the slug in `~/.lenneTech/projects.json`,
|
|
646
|
+
// injects the URL block into CLAUDE.md, adds `.lt-dev/` to .gitignore.
|
|
647
|
+
// Failures here are non-fatal — `init` itself remains successful.
|
|
648
|
+
let devMigrateOk = false;
|
|
649
|
+
try {
|
|
650
|
+
const layout = (0, dev_project_1.resolveLayout)(projectDir, gluegun_1.filesystem);
|
|
651
|
+
const migrate = (0, dev_migrate_helper_1.runMigrate)({ layout });
|
|
652
|
+
devMigrateOk = true;
|
|
653
|
+
if (!migrate.alreadyMigrated) {
|
|
654
|
+
info(colors.dim(` registered "${migrate.identity.slug}" with \`lt dev\``));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (_1) {
|
|
658
|
+
/* best-effort — never block init */
|
|
659
|
+
}
|
|
640
660
|
// We're done, so show what to do next
|
|
641
661
|
info('');
|
|
642
662
|
success(`Generated fullstack workspace with ${frontend} in ${projectDir} with ${name} app in ${helper.msToMinutesAndSeconds(timer())}m.`);
|
|
@@ -652,7 +672,21 @@ const NewCommand = {
|
|
|
652
672
|
else {
|
|
653
673
|
info(` Run ${name}`);
|
|
654
674
|
info(` $ cd ${projectDir}`);
|
|
655
|
-
|
|
675
|
+
if (devMigrateOk) {
|
|
676
|
+
// Prefer lt dev up — sets up Caddy + HTTPS URLs + cross-wiring guards.
|
|
677
|
+
const caddyOk = yield (0, caddy_1.caddyAvailable)();
|
|
678
|
+
if (caddyOk) {
|
|
679
|
+
info(' $ lt dev up # start API + App behind Caddy with project-specific URLs');
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
info(' $ lt dev install # one-time per machine: verify Caddy + CA');
|
|
683
|
+
info(' $ lt dev up # then: start API + App behind Caddy');
|
|
684
|
+
}
|
|
685
|
+
info(colors.dim(` (fallback: ${toolbox.pm.run('start')} runs the classic localhost:3000/3001 mode)`));
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
info(` $ ${toolbox.pm.run('start')}`);
|
|
689
|
+
}
|
|
656
690
|
}
|
|
657
691
|
info('');
|
|
658
692
|
if (!toolbox.parameters.options.fromGluegunMenu) {
|
package/build/commands/status.js
CHANGED
|
@@ -10,6 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const path_1 = require("path");
|
|
13
|
+
const dev_identity_1 = require("../lib/dev-identity");
|
|
14
|
+
const dev_state_1 = require("../lib/dev-state");
|
|
13
15
|
const framework_detection_1 = require("../lib/framework-detection");
|
|
14
16
|
const frontend_framework_detection_1 = require("../lib/frontend-framework-detection");
|
|
15
17
|
const workspace_integration_1 = require("../lib/workspace-integration");
|
|
@@ -22,7 +24,7 @@ const StatusCommand = {
|
|
|
22
24
|
hidden: false,
|
|
23
25
|
name: 'status',
|
|
24
26
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
25
|
-
var _a, _b;
|
|
27
|
+
var _a, _b, _c;
|
|
26
28
|
const { filesystem, print: { colors, info, success, warning }, system, } = toolbox;
|
|
27
29
|
const cwd = filesystem.cwd();
|
|
28
30
|
info('');
|
|
@@ -107,7 +109,7 @@ const StatusCommand = {
|
|
|
107
109
|
projectInfo.projectType = 'node';
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
|
-
catch (
|
|
112
|
+
catch (_d) {
|
|
111
113
|
// Ignore parse errors
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -136,7 +138,7 @@ const StatusCommand = {
|
|
|
136
138
|
});
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
|
-
catch (
|
|
141
|
+
catch (_e) {
|
|
140
142
|
// ignore
|
|
141
143
|
}
|
|
142
144
|
}
|
|
@@ -152,7 +154,7 @@ const StatusCommand = {
|
|
|
152
154
|
});
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
|
-
catch (
|
|
157
|
+
catch (_f) {
|
|
156
158
|
// ignore
|
|
157
159
|
}
|
|
158
160
|
}
|
|
@@ -167,7 +169,7 @@ const StatusCommand = {
|
|
|
167
169
|
projectInfo.gitBranch = (branch === null || branch === void 0 ? void 0 : branch.trim()) || null;
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
|
-
catch (
|
|
172
|
+
catch (_g) {
|
|
171
173
|
// Not a git repository
|
|
172
174
|
}
|
|
173
175
|
// Get Node/npm versions
|
|
@@ -175,7 +177,7 @@ const StatusCommand = {
|
|
|
175
177
|
projectInfo.nodeVersion = ((_a = (yield system.run('node --version 2>/dev/null'))) === null || _a === void 0 ? void 0 : _a.trim()) || null;
|
|
176
178
|
projectInfo.npmVersion = ((_b = (yield system.run('npm --version 2>/dev/null'))) === null || _b === void 0 ? void 0 : _b.trim()) || null;
|
|
177
179
|
}
|
|
178
|
-
catch (
|
|
180
|
+
catch (_h) {
|
|
179
181
|
// Ignore errors
|
|
180
182
|
}
|
|
181
183
|
// Display project info
|
|
@@ -234,6 +236,35 @@ const StatusCommand = {
|
|
|
234
236
|
info(colors.dim(' Hint: `lt fullstack add-app` to integrate a Nuxt or Angular app.'));
|
|
235
237
|
}
|
|
236
238
|
}
|
|
239
|
+
// Local dev orchestration registry — surface URLs if registered.
|
|
240
|
+
// Helps users discover that `lt dev` is set up for this project, and
|
|
241
|
+
// gives a quick inline answer to "what URLs does this project use?".
|
|
242
|
+
{
|
|
243
|
+
const registryRoot = ((_c = projectInfo.workspaceSubProject) === null || _c === void 0 ? void 0 : _c.root) ||
|
|
244
|
+
(projectInfo.workspaceLayout.hasWorkspace ? projectInfo.workspaceLayout.workspaceDir : cwd);
|
|
245
|
+
if (registryRoot) {
|
|
246
|
+
const slug = (0, dev_identity_1.projectSlug)(registryRoot);
|
|
247
|
+
const entry = (0, dev_state_1.loadRegistry)().projects[slug];
|
|
248
|
+
if (entry) {
|
|
249
|
+
info('');
|
|
250
|
+
info(colors.bold('Local dev orchestration (lt dev):'));
|
|
251
|
+
for (const [sub, host] of Object.entries(entry.subdomains)) {
|
|
252
|
+
info(` ${sub.padEnd(6)} https://${host}`);
|
|
253
|
+
}
|
|
254
|
+
if (entry.dbName)
|
|
255
|
+
info(` db mongodb://127.0.0.1/${entry.dbName}`);
|
|
256
|
+
const session = (0, dev_state_1.loadSession)(registryRoot);
|
|
257
|
+
const apiAlive = (session === null || session === void 0 ? void 0 : session.pids.api) ? (0, dev_state_1.isPidAlive)(session.pids.api) : false;
|
|
258
|
+
const appAlive = (session === null || session === void 0 ? void 0 : session.pids.app) ? (0, dev_state_1.isPidAlive)(session.pids.app) : false;
|
|
259
|
+
if (apiAlive || appAlive) {
|
|
260
|
+
info(` Running: api ${apiAlive ? colors.green('●') : colors.dim('○')} app ${appAlive ? colors.green('●') : colors.dim('○')}`);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
info(colors.dim(' Hint: `lt dev up` to start API + App behind Caddy.'));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
237
268
|
// Show monorepo subprojects if we detected any (typically at monorepo root)
|
|
238
269
|
if (projectInfo.monorepoSubprojects.length > 0) {
|
|
239
270
|
info('');
|
|
@@ -1975,21 +1975,31 @@ class Server {
|
|
|
1975
1975
|
literal.setLiteralValue(secretMap.get(text));
|
|
1976
1976
|
continue;
|
|
1977
1977
|
}
|
|
1978
|
-
// Replace database names
|
|
1978
|
+
// Replace database names so the project gets project-specific DB
|
|
1979
|
+
// names. Matches BOTH the legacy `nest-server-{env}` form AND the
|
|
1980
|
+
// current `nest-server-starter-{env}` form used by the public
|
|
1981
|
+
// nest-server-starter repo. Examples:
|
|
1982
|
+
// nest-server-starter-local → ${projectDir}-local
|
|
1983
|
+
// nest-server-starter-production → ${projectDir}-production
|
|
1984
|
+
// nest-server-ci → ${projectDir}-ci (legacy)
|
|
1985
|
+
// The optional `(?:starter-)?` non-capturing group is what fixes
|
|
1986
|
+
// the previously-broken case where dbName: 'nest-server-starter-local'
|
|
1987
|
+
// turned into 'svl-sports-system-starter-local' (-starter- stayed).
|
|
1979
1988
|
if (text.includes('nest-server-')) {
|
|
1980
|
-
literal.setLiteralValue(text.replace(/nest-server
|
|
1989
|
+
literal.setLiteralValue(text.replace(/nest-server-(?:starter-)?/g, `${projectDir}-`));
|
|
1981
1990
|
}
|
|
1982
1991
|
}
|
|
1983
1992
|
sourceFile.saveSync();
|
|
1984
1993
|
}
|
|
1985
1994
|
catch (_a) {
|
|
1986
|
-
// Fallback to regex-based approach if ts-morph fails
|
|
1995
|
+
// Fallback to regex-based approach if ts-morph fails. Same matcher
|
|
1996
|
+
// as the AST branch — keep them in sync.
|
|
1987
1997
|
let content = this.filesystem.read(configPath);
|
|
1988
1998
|
if (!content) {
|
|
1989
1999
|
return;
|
|
1990
2000
|
}
|
|
1991
2001
|
content = this.replaceSecretOrPrivateKeys(content);
|
|
1992
|
-
content = content.replace(/nest-server-(\w+)/g, `${projectDir}-$1`);
|
|
2002
|
+
content = content.replace(/nest-server-(?:starter-)?(\w+)/g, `${projectDir}-$1`);
|
|
1993
2003
|
this.filesystem.write(configPath, content);
|
|
1994
2004
|
}
|
|
1995
2005
|
}
|