@lenne.tech/cli 1.21.0 → 1.23.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 +16 -10
- package/build/commands/{local/local.js → dev/dev.js} +14 -10
- package/build/commands/dev/doctor.js +144 -0
- package/build/commands/dev/down.js +76 -0
- package/build/commands/dev/install.js +172 -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/tunnel.js +142 -0
- package/build/commands/dev/uninstall.js +92 -0
- package/build/commands/dev/up.js +228 -0
- package/build/commands/fullstack/init.js +36 -2
- package/build/commands/status.js +16 -16
- package/build/lib/caddy.js +183 -0
- package/build/lib/cloudflared.js +129 -0
- package/build/lib/dev-env-bridge.js +112 -0
- package/build/lib/dev-env.js +65 -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/{local-project.js → dev-project.js} +23 -38
- package/build/lib/dev-service.js +414 -0
- package/build/lib/dev-state.js +152 -0
- package/docs/commands.md +167 -94
- package/package.json +1 -1
- package/build/commands/local/down.js +0 -71
- package/build/commands/local/init.js +0 -162
- package/build/commands/local/status.js +0 -69
- package/build/commands/local/up.js +0 -148
- package/build/commands/ports/ports.js +0 -118
- package/build/commands/ports/scan.js +0 -131
- package/build/lib/local-patches.js +0 -175
- package/build/lib/port-registry.js +0 -304
|
@@ -1,162 +0,0 @@
|
|
|
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 local_patches_1 = require("../../lib/local-patches");
|
|
15
|
-
const local_project_1 = require("../../lib/local-project");
|
|
16
|
-
const port_registry_1 = require("../../lib/port-registry");
|
|
17
|
-
/**
|
|
18
|
-
* Register a port slot for the current project + optionally patch
|
|
19
|
-
* legacy hardcoded ports.
|
|
20
|
-
*/
|
|
21
|
-
const InitCommand = {
|
|
22
|
-
alias: ['i'],
|
|
23
|
-
description: 'Register port slot',
|
|
24
|
-
hidden: false,
|
|
25
|
-
name: 'init',
|
|
26
|
-
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
27
|
-
const { filesystem, parameters, print: { colors, error, info, success, warning }, prompt, } = toolbox;
|
|
28
|
-
const layout = (0, local_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
29
|
-
if (!layout.apiDir && !layout.appDir) {
|
|
30
|
-
error('No API (src/config.env.ts) or App (nuxt.config.ts) project detected at this path.');
|
|
31
|
-
if (!parameters.options.fromGluegunMenu)
|
|
32
|
-
process.exit(1);
|
|
33
|
-
return 'local init: not a project';
|
|
34
|
-
}
|
|
35
|
-
const slug = (0, port_registry_1.projectSlug)(layout.root);
|
|
36
|
-
const registry = (0, port_registry_1.loadRegistry)();
|
|
37
|
-
// Determine slot: existing entry > CLI flag > deterministic from slug.
|
|
38
|
-
let slot;
|
|
39
|
-
const cliSlot = parameters.options.slot !== undefined ? Number(parameters.options.slot) : null;
|
|
40
|
-
if (registry.projects[slug]) {
|
|
41
|
-
slot = registry.projects[slug].slot;
|
|
42
|
-
info(`Project "${slug}" already registered with slot ${slot}.`);
|
|
43
|
-
}
|
|
44
|
-
else if (cliSlot !== null && Number.isFinite(cliSlot)) {
|
|
45
|
-
slot = cliSlot;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
slot = (0, port_registry_1.allocateSlot)(slug, registry);
|
|
49
|
-
}
|
|
50
|
-
const ports = (0, port_registry_1.portsForSlot)(slot);
|
|
51
|
-
info('');
|
|
52
|
-
info(colors.bold('Project layout'));
|
|
53
|
-
info(colors.dim('─'.repeat(50)));
|
|
54
|
-
info(` name: ${slug}`);
|
|
55
|
-
info(` root: ${layout.root}`);
|
|
56
|
-
if (layout.apiDir)
|
|
57
|
-
info(` api: ${layout.apiDir}`);
|
|
58
|
-
if (layout.appDir)
|
|
59
|
-
info(` app: ${layout.appDir}`);
|
|
60
|
-
info(` slot: ${slot}`);
|
|
61
|
-
info(` ports: api=${ports.api} app=${ports.app}`);
|
|
62
|
-
// Detect legacy hardcoded ports
|
|
63
|
-
const apiPatchFile = layout.apiDir ? (0, local_project_1.apiNeedsPortPatch)(layout.apiDir) : null;
|
|
64
|
-
const appPatchFiles = layout.appDir ? (0, local_project_1.appNeedsPortPatch)(layout.appDir) : [];
|
|
65
|
-
const filesToPatch = [apiPatchFile, ...appPatchFiles].filter((f) => Boolean(f));
|
|
66
|
-
const noConfirm = Boolean(parameters.options.noConfirm);
|
|
67
|
-
const noPatch = Boolean(parameters.options.noPatch);
|
|
68
|
-
const forcePatch = Boolean(parameters.options.patch);
|
|
69
|
-
if (filesToPatch.length > 0 && !noPatch) {
|
|
70
|
-
info('');
|
|
71
|
-
warning('Files with legacy hardcoded ports detected:');
|
|
72
|
-
filesToPatch.forEach((f) => info(` - ${f}`));
|
|
73
|
-
info(colors.dim('Patch makes them env-overridable: `process.env.PORT || 3000` etc. — defaults preserved.'));
|
|
74
|
-
let doPatch = forcePatch;
|
|
75
|
-
if (!doPatch && !noConfirm) {
|
|
76
|
-
const ans = yield prompt.confirm('Apply env-aware patches now?', true);
|
|
77
|
-
doPatch = Boolean(ans);
|
|
78
|
-
}
|
|
79
|
-
else if (!doPatch && noConfirm) {
|
|
80
|
-
info(colors.dim('Skipping patches (--noConfirm without --patch). Pass --patch to auto-apply.'));
|
|
81
|
-
}
|
|
82
|
-
if (doPatch) {
|
|
83
|
-
for (const file of filesToPatch) {
|
|
84
|
-
const result = (0, local_patches_1.autoPatch)(file);
|
|
85
|
-
if (result.patched) {
|
|
86
|
-
success(`patched ${result.replacements}× in ${file}`);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
info(colors.dim(`skipped (already patched): ${file}`));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else if (filesToPatch.length === 0) {
|
|
95
|
-
info(colors.dim(' patches: not needed (already env-aware)'));
|
|
96
|
-
}
|
|
97
|
-
// Persist to registry only when something actually changed — avoids
|
|
98
|
-
// mtime churn on ~/.lenneTech/ports.json for cloud-sync tools (Dropbox,
|
|
99
|
-
// iCloud Drive, Syncthing) and editor "file changed externally" prompts.
|
|
100
|
-
const dbName = deriveDbName(layout, slug);
|
|
101
|
-
const existing = registry.projects[slug];
|
|
102
|
-
const next = { dbName, path: layout.root, ports, slot };
|
|
103
|
-
const changed = !existing ||
|
|
104
|
-
existing.path !== next.path ||
|
|
105
|
-
existing.slot !== next.slot ||
|
|
106
|
-
existing.dbName !== next.dbName ||
|
|
107
|
-
existing.ports.api !== next.ports.api ||
|
|
108
|
-
existing.ports.app !== next.ports.app;
|
|
109
|
-
if (changed) {
|
|
110
|
-
registry.projects[slug] = next;
|
|
111
|
-
(0, port_registry_1.saveRegistry)(registry);
|
|
112
|
-
}
|
|
113
|
-
// Add .lt-local/ to .gitignore (idempotent)
|
|
114
|
-
addToGitignore(layout.root, '.lt-local/');
|
|
115
|
-
// Patch CLAUDE.md files (workspace + each subproject) with the active
|
|
116
|
-
// port block so future Claude Code sessions read the correct ports
|
|
117
|
-
// even when the lt-dev plugin's hook is not active.
|
|
118
|
-
const claudeMdCandidates = [
|
|
119
|
-
(0, path_1.join)(layout.root, 'CLAUDE.md'),
|
|
120
|
-
...(layout.apiDir ? [(0, path_1.join)(layout.apiDir, 'CLAUDE.md')] : []),
|
|
121
|
-
...(layout.appDir ? [(0, path_1.join)(layout.appDir, 'CLAUDE.md')] : []),
|
|
122
|
-
];
|
|
123
|
-
const claudePatches = claudeMdCandidates
|
|
124
|
-
.map((file) => (0, local_patches_1.patchClaudeMd)(file, { apiPort: ports.api, appPort: ports.app, dbName, slug }))
|
|
125
|
-
.filter((r) => r.patched);
|
|
126
|
-
if (claudePatches.length > 0) {
|
|
127
|
-
claudePatches.forEach((r) => success(`updated CLAUDE.md port block: ${r.file}`));
|
|
128
|
-
}
|
|
129
|
-
info('');
|
|
130
|
-
success(`Registered. Run \`lt local up\` to start.`);
|
|
131
|
-
if (!parameters.options.fromGluegunMenu)
|
|
132
|
-
process.exit();
|
|
133
|
-
return `local init ${slug} slot=${slot}`;
|
|
134
|
-
}),
|
|
135
|
-
};
|
|
136
|
-
/** Append entry to .gitignore if not already present. */
|
|
137
|
-
function addToGitignore(root, entry) {
|
|
138
|
-
const path = (0, path_1.join)(root, '.gitignore');
|
|
139
|
-
let content = '';
|
|
140
|
-
if ((0, fs_1.existsSync)(path))
|
|
141
|
-
content = (0, fs_1.readFileSync)(path, 'utf8');
|
|
142
|
-
const lines = content.split(/\r?\n/);
|
|
143
|
-
if (lines.some((l) => l.trim() === entry || l.trim() === entry.replace(/\/$/, '')))
|
|
144
|
-
return;
|
|
145
|
-
const ensured = `${(content.endsWith('\n') || content.length === 0 ? content : `${content}\n`) + entry}\n`;
|
|
146
|
-
(0, fs_1.writeFileSync)(path, ensured, 'utf8');
|
|
147
|
-
}
|
|
148
|
-
/** Derive a sensible default DB name from project + workspace shape. */
|
|
149
|
-
function deriveDbName(layout, slug) {
|
|
150
|
-
// Reuse existing dbName from the API config if it is the default `${slug}-local`
|
|
151
|
-
if (layout.apiDir) {
|
|
152
|
-
const cfg = (0, path_1.join)(layout.apiDir, 'src', 'config.env.ts');
|
|
153
|
-
if ((0, fs_1.existsSync)(cfg)) {
|
|
154
|
-
const content = (0, fs_1.readFileSync)(cfg, 'utf8');
|
|
155
|
-
const match = content.match(/dbName:\s*['"`]([^'"`]+)['"`]/);
|
|
156
|
-
if (match)
|
|
157
|
-
return match[1];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return `${slug}-local`;
|
|
161
|
-
}
|
|
162
|
-
module.exports = InitCommand;
|
|
@@ -1,69 +0,0 @@
|
|
|
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 local_project_1 = require("../../lib/local-project");
|
|
13
|
-
const port_registry_1 = require("../../lib/port-registry");
|
|
14
|
-
/**
|
|
15
|
-
* Show what is running for the current project.
|
|
16
|
-
*/
|
|
17
|
-
const StatusCommand = {
|
|
18
|
-
alias: ['s'],
|
|
19
|
-
description: 'Show local status',
|
|
20
|
-
hidden: false,
|
|
21
|
-
name: 'status',
|
|
22
|
-
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
23
|
-
var _a, _b;
|
|
24
|
-
const { filesystem, parameters, print: { colors, info, warning }, } = toolbox;
|
|
25
|
-
const layout = (0, local_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
26
|
-
const slug = (0, port_registry_1.projectSlug)(layout.root);
|
|
27
|
-
const registry = (0, port_registry_1.loadRegistry)();
|
|
28
|
-
const entry = registry.projects[slug];
|
|
29
|
-
info('');
|
|
30
|
-
info(colors.bold(`Local status: ${slug}`));
|
|
31
|
-
info(colors.dim('─'.repeat(50)));
|
|
32
|
-
if (!entry) {
|
|
33
|
-
warning('Not registered. Run `lt local init` first.');
|
|
34
|
-
if (!parameters.options.fromGluegunMenu)
|
|
35
|
-
process.exit();
|
|
36
|
-
return 'local status: not registered';
|
|
37
|
-
}
|
|
38
|
-
const ports = (0, port_registry_1.portsForSlot)(entry.slot);
|
|
39
|
-
info(` slot: ${entry.slot}`);
|
|
40
|
-
info(` api: http://localhost:${ports.api}`);
|
|
41
|
-
info(` app: http://localhost:${ports.app}`);
|
|
42
|
-
if (entry.dbName)
|
|
43
|
-
info(` db: mongodb://127.0.0.1/${entry.dbName}`);
|
|
44
|
-
const state = (0, port_registry_1.loadLocalState)(layout.root);
|
|
45
|
-
info('');
|
|
46
|
-
if (!state || (!state.pids.api && !state.pids.app)) {
|
|
47
|
-
info(colors.dim(' no `lt local up` session active'));
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const apiAlive = state.pids.api ? (0, port_registry_1.isPidAlive)(state.pids.api) : false;
|
|
51
|
-
const appAlive = state.pids.app ? (0, port_registry_1.isPidAlive)(state.pids.app) : false;
|
|
52
|
-
info(` api: ${apiAlive ? colors.green('running') : colors.red('dead')} (pid ${(_a = state.pids.api) !== null && _a !== void 0 ? _a : '-'})`);
|
|
53
|
-
info(` app: ${appAlive ? colors.green('running') : colors.red('dead')} (pid ${(_b = state.pids.app) !== null && _b !== void 0 ? _b : '-'})`);
|
|
54
|
-
info(colors.dim(` started: ${state.startedAt}`));
|
|
55
|
-
}
|
|
56
|
-
info('');
|
|
57
|
-
info(colors.bold('Live port state'));
|
|
58
|
-
const liveSnapshot = yield (0, port_registry_1.listenSnapshot)([ports.api, ports.app]);
|
|
59
|
-
for (const port of [ports.api, ports.app]) {
|
|
60
|
-
const r = liveSnapshot.get(port);
|
|
61
|
-
info(` ${port}: ${r ? colors.green(`bound to ${r.command} (pid ${r.pid})`) : colors.dim('free')}`);
|
|
62
|
-
}
|
|
63
|
-
info('');
|
|
64
|
-
if (!parameters.options.fromGluegunMenu)
|
|
65
|
-
process.exit();
|
|
66
|
-
return `local status ${slug}`;
|
|
67
|
-
}),
|
|
68
|
-
};
|
|
69
|
-
module.exports = StatusCommand;
|
|
@@ -1,148 +0,0 @@
|
|
|
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 path_1 = require("path");
|
|
15
|
-
const local_project_1 = require("../../lib/local-project");
|
|
16
|
-
const port_registry_1 = require("../../lib/port-registry");
|
|
17
|
-
/**
|
|
18
|
-
* Start API + App with project-specific ports.
|
|
19
|
-
*
|
|
20
|
-
* Reads the slot from the central registry, exports env vars,
|
|
21
|
-
* spawns `pnpm start` (api) and `pnpm dev` (app) detached, and
|
|
22
|
-
* persists the PIDs to <root>/.lt-local/state.json.
|
|
23
|
-
*
|
|
24
|
-
* Environment-variable contract injected into spawned children
|
|
25
|
-
* (single source of truth — keep in sync with the CLAUDE.md port
|
|
26
|
-
* block in `lib/local-patches.ts#patchClaudeMd`):
|
|
27
|
-
*
|
|
28
|
-
* PORT — Nest (api) / Nuxt dev server (app); slot-derived
|
|
29
|
-
* BASE_URL — nest-server config.env.ts (canonical API base)
|
|
30
|
-
* APP_URL — nest-server config.env.ts (frontend origin for redirects/CORS)
|
|
31
|
-
* NUXT_API_URL — Nuxt vite-proxy target for /api, /iam, …
|
|
32
|
-
* NUXT_PUBLIC_API_URL — public, exposed via useRuntimeConfig().public.apiUrl
|
|
33
|
-
* NUXT_PUBLIC_SITE_URL — public, used by useRuntimeConfig().public.siteUrl + Playwright
|
|
34
|
-
* NUXT_PUBLIC_STORAGE_PREFIX — namespaces sessionStorage/localStorage so parallel
|
|
35
|
-
* projects don't share auth tokens (e.g. "crm-local")
|
|
36
|
-
* NSC__MONGOOSE__URI — nest-server-config Mongoose URI (only when dbName known)
|
|
37
|
-
*/
|
|
38
|
-
const UpCommand = {
|
|
39
|
-
alias: ['u'],
|
|
40
|
-
description: 'Start API + App',
|
|
41
|
-
hidden: false,
|
|
42
|
-
name: 'up',
|
|
43
|
-
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
-
var _a, _b, _c, _d;
|
|
45
|
-
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
46
|
-
const layout = (0, local_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
47
|
-
const slug = (0, port_registry_1.projectSlug)(layout.root);
|
|
48
|
-
const registry = (0, port_registry_1.loadRegistry)();
|
|
49
|
-
const entry = registry.projects[slug];
|
|
50
|
-
if (!entry) {
|
|
51
|
-
error(`Project "${slug}" not registered. Run \`lt local init\` first.`);
|
|
52
|
-
if (!parameters.options.fromGluegunMenu)
|
|
53
|
-
process.exit(1);
|
|
54
|
-
return 'local up: not registered';
|
|
55
|
-
}
|
|
56
|
-
const ports = (0, port_registry_1.portsForSlot)(entry.slot);
|
|
57
|
-
// Already running?
|
|
58
|
-
const existing = (0, port_registry_1.loadLocalState)(layout.root);
|
|
59
|
-
if (existing &&
|
|
60
|
-
((existing.pids.api && (0, port_registry_1.isPidAlive)(existing.pids.api)) || (existing.pids.app && (0, port_registry_1.isPidAlive)(existing.pids.app)))) {
|
|
61
|
-
warning(`Already running (api pid ${(_a = existing.pids.api) !== null && _a !== void 0 ? _a : '-'}, app pid ${(_b = existing.pids.app) !== null && _b !== void 0 ? _b : '-'}). Run \`lt local down\` first.`);
|
|
62
|
-
if (!parameters.options.fromGluegunMenu)
|
|
63
|
-
process.exit(1);
|
|
64
|
-
return 'local up: already running';
|
|
65
|
-
}
|
|
66
|
-
// Pre-flight port check (single lsof call covers both ports).
|
|
67
|
-
const snapshot = yield (0, port_registry_1.listenSnapshot)([ports.api, ports.app]);
|
|
68
|
-
for (const p of [ports.api, ports.app]) {
|
|
69
|
-
const r = snapshot.get(p);
|
|
70
|
-
if (r) {
|
|
71
|
-
error(`Port ${p} already in use by ${r.command} (pid ${r.pid}).`);
|
|
72
|
-
if (!parameters.options.fromGluegunMenu)
|
|
73
|
-
process.exit(1);
|
|
74
|
-
return 'local up: port busy';
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const env = Object.assign(Object.assign(Object.assign({}, process.env), { APP_URL: `http://localhost:${ports.app}`, BASE_URL: `http://localhost:${ports.api}`, NUXT_API_URL: `http://localhost:${ports.api}`, NUXT_PUBLIC_API_URL: `http://localhost:${ports.api}`, NUXT_PUBLIC_SITE_URL: `http://localhost:${ports.app}`, NUXT_PUBLIC_STORAGE_PREFIX: `${slug}-local` }), (entry.dbName ? { NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${entry.dbName}` } : {}));
|
|
78
|
-
info('');
|
|
79
|
-
info(colors.bold(`Starting "${slug}" on slot ${entry.slot}`));
|
|
80
|
-
info(` api: http://localhost:${ports.api}`);
|
|
81
|
-
info(` app: http://localhost:${ports.app}`);
|
|
82
|
-
if (entry.dbName)
|
|
83
|
-
info(` db: mongodb://127.0.0.1/${entry.dbName}`);
|
|
84
|
-
info('');
|
|
85
|
-
const pids = {};
|
|
86
|
-
// Allow corporate / pinned setups to override the binary used for `lt local up`.
|
|
87
|
-
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
88
|
-
if (layout.apiDir && (0, fs_1.existsSync)((0, path_1.join)(layout.apiDir, 'package.json'))) {
|
|
89
|
-
const apiPid = spawnDetached(pnpmBin, ['start'], {
|
|
90
|
-
cwd: layout.apiDir,
|
|
91
|
-
env: Object.assign(Object.assign({}, env), { PORT: String(ports.api) }),
|
|
92
|
-
logFile: (0, path_1.join)(layout.root, '.lt-local', 'api.log'),
|
|
93
|
-
});
|
|
94
|
-
if (apiPid)
|
|
95
|
-
pids.api = apiPid;
|
|
96
|
-
}
|
|
97
|
-
if (layout.appDir && (0, fs_1.existsSync)((0, path_1.join)(layout.appDir, 'package.json'))) {
|
|
98
|
-
const appPid = spawnDetached(pnpmBin, ['dev'], {
|
|
99
|
-
cwd: layout.appDir,
|
|
100
|
-
env: Object.assign(Object.assign({}, env), { PORT: String(ports.app) }),
|
|
101
|
-
logFile: (0, path_1.join)(layout.root, '.lt-local', 'app.log'),
|
|
102
|
-
});
|
|
103
|
-
if (appPid)
|
|
104
|
-
pids.app = appPid;
|
|
105
|
-
}
|
|
106
|
-
(0, port_registry_1.saveLocalState)(layout.root, { pids, ports, startedAt: new Date().toISOString() });
|
|
107
|
-
success(`Started: api pid=${(_c = pids.api) !== null && _c !== void 0 ? _c : '-'}, app pid=${(_d = pids.app) !== null && _d !== void 0 ? _d : '-'}`);
|
|
108
|
-
info(colors.dim('Logs: <root>/.lt-local/api.log, <root>/.lt-local/app.log'));
|
|
109
|
-
info(colors.dim('Stop with: lt local down'));
|
|
110
|
-
if (!parameters.options.fromGluegunMenu)
|
|
111
|
-
process.exit();
|
|
112
|
-
return `local up: api=${pids.api}, app=${pids.app}`;
|
|
113
|
-
}),
|
|
114
|
-
};
|
|
115
|
-
/**
|
|
116
|
-
* Spawn a detached child whose stdio is redirected to a log file.
|
|
117
|
-
*
|
|
118
|
-
* The parent's copy of the log file descriptor is closed in the `finally`
|
|
119
|
-
* block — the child has already inherited its own fd before `spawn` returns,
|
|
120
|
-
* so closing here prevents fd leaks and avoids racing-write artifacts on
|
|
121
|
-
* filesystems where O_APPEND is not atomic.
|
|
122
|
-
*/
|
|
123
|
-
function spawnDetached(cmd, args, opts) {
|
|
124
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(opts.logFile), { recursive: true });
|
|
125
|
-
const out = (0, fs_1.openSync)(opts.logFile, 'a');
|
|
126
|
-
try {
|
|
127
|
-
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
128
|
-
cwd: opts.cwd,
|
|
129
|
-
detached: true,
|
|
130
|
-
env: opts.env,
|
|
131
|
-
stdio: ['ignore', out, out],
|
|
132
|
-
});
|
|
133
|
-
child.unref();
|
|
134
|
-
return child.pid;
|
|
135
|
-
}
|
|
136
|
-
catch (_a) {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
try {
|
|
141
|
-
(0, fs_1.closeSync)(out);
|
|
142
|
-
}
|
|
143
|
-
catch (_b) {
|
|
144
|
-
/* fd already inherited by child; ignore */
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
module.exports = UpCommand;
|
|
@@ -1,118 +0,0 @@
|
|
|
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 port_registry_1 = require("../../lib/port-registry");
|
|
13
|
-
/**
|
|
14
|
-
* Show port registry overview + live `lsof` state.
|
|
15
|
-
*
|
|
16
|
-
* `lt ports` → list all reserved + currently bound dev ports
|
|
17
|
-
* `lt ports check 3030` → exit-coded check (0 = free, 1 = in use)
|
|
18
|
-
* `lt ports scan <dir>` → rebuild registry from filesystem (subcommand)
|
|
19
|
-
*
|
|
20
|
-
* The default action issues a single `lsof` call (via {@link listenSnapshot})
|
|
21
|
-
* and filters in memory rather than spawning lsof per port — see
|
|
22
|
-
* `port-registry.ts` for the rationale.
|
|
23
|
-
*/
|
|
24
|
-
const PortsCommand = {
|
|
25
|
-
alias: ['p'],
|
|
26
|
-
description: 'Inspect port allocation',
|
|
27
|
-
hidden: false,
|
|
28
|
-
name: 'ports',
|
|
29
|
-
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
|
-
var _a, _b;
|
|
31
|
-
const { parameters, print: { colors, info, success, warning }, } = toolbox;
|
|
32
|
-
// Sub-command "check": fast exit-coded port probe.
|
|
33
|
-
if (parameters.first === 'check') {
|
|
34
|
-
const port = Number(parameters.second);
|
|
35
|
-
const fromMenu = Boolean(parameters.options.fromGluegunMenu);
|
|
36
|
-
if (!Number.isFinite(port)) {
|
|
37
|
-
warning('Usage: lt ports check <port>');
|
|
38
|
-
if (!fromMenu)
|
|
39
|
-
process.exit(2);
|
|
40
|
-
return 'ports check missing-port';
|
|
41
|
-
}
|
|
42
|
-
const result = yield (0, port_registry_1.checkPortInUse)(port);
|
|
43
|
-
if (result === null) {
|
|
44
|
-
warning('lsof not available on this system.');
|
|
45
|
-
if (!fromMenu)
|
|
46
|
-
process.exit(2);
|
|
47
|
-
return 'ports check no-lsof';
|
|
48
|
-
}
|
|
49
|
-
if (result.inUse) {
|
|
50
|
-
info(`port ${port}: in use by ${result.command} (pid ${result.pid})`);
|
|
51
|
-
if (!fromMenu)
|
|
52
|
-
process.exit(1);
|
|
53
|
-
return 'ports check in-use';
|
|
54
|
-
}
|
|
55
|
-
success(`port ${port}: free`);
|
|
56
|
-
if (!fromMenu)
|
|
57
|
-
process.exit(0);
|
|
58
|
-
return 'ports check free';
|
|
59
|
-
}
|
|
60
|
-
// Default action: combined list. Single lsof call covers the entire slot
|
|
61
|
-
// range plus all registry-allocated ports.
|
|
62
|
-
const registry = (0, port_registry_1.loadRegistry)();
|
|
63
|
-
const projects = Object.entries(registry.projects);
|
|
64
|
-
const wanted = new Set();
|
|
65
|
-
for (let p = port_registry_1.SLOT_BASE_API; p < port_registry_1.SLOT_PORT_RANGE_END; p++)
|
|
66
|
-
wanted.add(p);
|
|
67
|
-
for (const [, entry] of projects) {
|
|
68
|
-
const ports = (0, port_registry_1.portsForSlot)(entry.slot);
|
|
69
|
-
wanted.add(ports.api);
|
|
70
|
-
wanted.add(ports.app);
|
|
71
|
-
}
|
|
72
|
-
const snapshot = yield (0, port_registry_1.listenSnapshot)(wanted);
|
|
73
|
-
info('');
|
|
74
|
-
info(colors.bold('Reserved ports (registry)'));
|
|
75
|
-
info(colors.dim('─'.repeat(60)));
|
|
76
|
-
if (projects.length === 0) {
|
|
77
|
-
info(colors.dim(' (none yet — run `lt local init` in a project)'));
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
for (const [name, entry] of projects.sort((a, b) => a[1].slot - b[1].slot)) {
|
|
81
|
-
const ports = (0, port_registry_1.portsForSlot)(entry.slot);
|
|
82
|
-
const apiTag = snapshot.has(ports.api) ? colors.green('●') : colors.dim('○');
|
|
83
|
-
const appTag = snapshot.has(ports.app) ? colors.green('●') : colors.dim('○');
|
|
84
|
-
info(` ${name.padEnd(28)} api ${apiTag} ${ports.api} app ${appTag} ${ports.app} ${colors.dim(entry.path)}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
info('');
|
|
88
|
-
info(colors.bold(`Currently bound dev ports (${port_registry_1.SLOT_BASE_API}-${port_registry_1.SLOT_PORT_RANGE_END - 1})`));
|
|
89
|
-
info(colors.dim('─'.repeat(60)));
|
|
90
|
-
let anyBound = false;
|
|
91
|
-
for (let p = port_registry_1.SLOT_BASE_API; p < port_registry_1.SLOT_PORT_RANGE_END; p++) {
|
|
92
|
-
const r = snapshot.get(p);
|
|
93
|
-
if (!r)
|
|
94
|
-
continue;
|
|
95
|
-
anyBound = true;
|
|
96
|
-
const owner = registryOwner(registry, p);
|
|
97
|
-
info(` ${String(p).padEnd(6)} ${(_b = (_a = r.command) === null || _a === void 0 ? void 0 : _a.padEnd(12)) !== null && _b !== void 0 ? _b : ''} pid ${r.pid}${owner ? ` ${colors.dim(owner)}` : ''}`);
|
|
98
|
-
}
|
|
99
|
-
if (!anyBound)
|
|
100
|
-
info(colors.dim(` (no ports in ${port_registry_1.SLOT_BASE_API}-${port_registry_1.SLOT_PORT_RANGE_END - 1} range bound)`));
|
|
101
|
-
info('');
|
|
102
|
-
if (!parameters.options.fromGluegunMenu) {
|
|
103
|
-
process.exit();
|
|
104
|
-
}
|
|
105
|
-
return 'ports list';
|
|
106
|
-
}),
|
|
107
|
-
};
|
|
108
|
-
function registryOwner(registry, port) {
|
|
109
|
-
for (const [name, entry] of Object.entries(registry.projects)) {
|
|
110
|
-
const ports = (0, port_registry_1.portsForSlot)(entry.slot);
|
|
111
|
-
if (ports.api === port)
|
|
112
|
-
return `→ ${name} (api)`;
|
|
113
|
-
if (ports.app === port)
|
|
114
|
-
return `→ ${name} (app)`;
|
|
115
|
-
}
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
module.exports = PortsCommand;
|
|
@@ -1,131 +0,0 @@
|
|
|
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 port_registry_1 = require("../../lib/port-registry");
|
|
15
|
-
/**
|
|
16
|
-
* Rebuild the port registry from the filesystem.
|
|
17
|
-
*
|
|
18
|
-
* Walks the given directory (default: cwd) up to depth 3, looking for
|
|
19
|
-
* `lt.config.json` + `package.json` pairs or workspace markers. Re-allocates
|
|
20
|
-
* a slot for each new project found; preserves slots for projects whose
|
|
21
|
-
* names already exist in the registry.
|
|
22
|
-
*/
|
|
23
|
-
const ScanCommand = {
|
|
24
|
-
alias: [],
|
|
25
|
-
description: 'Rebuild port registry',
|
|
26
|
-
hidden: false,
|
|
27
|
-
name: 'scan',
|
|
28
|
-
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
-
const { filesystem, parameters, print: { colors, info, success }, } = toolbox;
|
|
30
|
-
const startDir = parameters.first ? filesystem.path(parameters.first) : filesystem.cwd();
|
|
31
|
-
info('');
|
|
32
|
-
info(colors.bold(`Scanning for projects under ${startDir} ...`));
|
|
33
|
-
const found = walkForProjects(startDir, 0, 3);
|
|
34
|
-
info(colors.dim(`Found ${found.length} candidate project(s)`));
|
|
35
|
-
const registry = (0, port_registry_1.loadRegistry)();
|
|
36
|
-
let added = 0;
|
|
37
|
-
let kept = 0;
|
|
38
|
-
let dirty = false;
|
|
39
|
-
for (const project of found) {
|
|
40
|
-
const slug = (0, port_registry_1.projectSlug)(project.path);
|
|
41
|
-
const existing = registry.projects[slug];
|
|
42
|
-
if (existing) {
|
|
43
|
-
if (existing.path !== project.path) {
|
|
44
|
-
existing.path = project.path;
|
|
45
|
-
dirty = true;
|
|
46
|
-
}
|
|
47
|
-
kept++;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const slot = (0, port_registry_1.allocateSlot)(slug, registry);
|
|
51
|
-
registry.projects[slug] = { path: project.path, ports: (0, port_registry_1.portsForSlot)(slot), slot };
|
|
52
|
-
added++;
|
|
53
|
-
dirty = true;
|
|
54
|
-
}
|
|
55
|
-
if (dirty)
|
|
56
|
-
(0, port_registry_1.saveRegistry)(registry);
|
|
57
|
-
success(`Registry updated: ${added} new, ${kept} kept (total: ${Object.keys(registry.projects).length})`);
|
|
58
|
-
if (!parameters.options.fromGluegunMenu) {
|
|
59
|
-
process.exit();
|
|
60
|
-
}
|
|
61
|
-
return `ports scan: ${added} new`;
|
|
62
|
-
}),
|
|
63
|
-
};
|
|
64
|
-
/** A directory looks like a project if it has `package.json` with a non-empty `name` field. */
|
|
65
|
-
function looksLikeProject(dir) {
|
|
66
|
-
if (!(0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json')))
|
|
67
|
-
return false;
|
|
68
|
-
try {
|
|
69
|
-
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, 'package.json'), 'utf8'));
|
|
70
|
-
return Boolean(pkg.name);
|
|
71
|
-
}
|
|
72
|
-
catch (_a) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Recursive project discovery.
|
|
78
|
-
*
|
|
79
|
-
* Stops descending into a directory as soon as a project marker is detected
|
|
80
|
-
* (lt.config.json with package.json, pnpm-workspace.yaml, or `projects/`).
|
|
81
|
-
* Skips dotdirs and `node_modules`. Skips symlinks (lstatSync) to avoid
|
|
82
|
-
* traversal loops on pathological filesystems.
|
|
83
|
-
*/
|
|
84
|
-
function walkForProjects(dir, depth, maxDepth) {
|
|
85
|
-
if (depth > maxDepth)
|
|
86
|
-
return [];
|
|
87
|
-
if (!(0, fs_1.existsSync)(dir))
|
|
88
|
-
return [];
|
|
89
|
-
const out = [];
|
|
90
|
-
// A project is detected if EITHER an lt.config.json exists OR a workspace marker is found.
|
|
91
|
-
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'lt.config.json'))) {
|
|
92
|
-
if (looksLikeProject(dir)) {
|
|
93
|
-
out.push({ path: dir });
|
|
94
|
-
return out; // don't recurse into a detected project
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'pnpm-workspace.yaml')) || (0, fs_1.existsSync)((0, path_1.join)(dir, 'projects'))) {
|
|
98
|
-
out.push({ path: dir });
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
101
|
-
let entries;
|
|
102
|
-
try {
|
|
103
|
-
entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
104
|
-
}
|
|
105
|
-
catch (_a) {
|
|
106
|
-
return [];
|
|
107
|
-
}
|
|
108
|
-
for (const entry of entries) {
|
|
109
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
110
|
-
continue;
|
|
111
|
-
// Use the cheap Dirent check first — it doesn't need an extra syscall.
|
|
112
|
-
if (!entry.isDirectory()) {
|
|
113
|
-
// Could still be a symlink that points to a directory. Skip those to
|
|
114
|
-
// avoid traversal loops.
|
|
115
|
-
if (!entry.isSymbolicLink())
|
|
116
|
-
continue;
|
|
117
|
-
let s;
|
|
118
|
-
try {
|
|
119
|
-
s = (0, fs_1.lstatSync)((0, path_1.join)(dir, entry.name));
|
|
120
|
-
}
|
|
121
|
-
catch (_b) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (s.isSymbolicLink())
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
out.push(...walkForProjects((0, path_1.join)(dir, entry.name), depth + 1, maxDepth));
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
module.exports = ScanCommand;
|