@lenne.tech/cli
Version:
lenne.Tech CLI: lt
137 lines (136 loc) • 5.28 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildIdentity = buildIdentity;
exports.buildTestIdentity = buildTestIdentity;
exports.buildTicketIdentity = buildTicketIdentity;
exports.projectSlug = projectSlug;
exports.slugify = slugify;
/**
* Project identity for `lt dev`.
*
* URL-first: every project has a slug derived from its package.json
* "name", and a deterministic set of subdomains under `*.localhost`.
*
* Convention:
* - `<slug>.localhost` → primary App
* - `api.<slug>.localhost` → API
* - `<other>.<slug>.localhost` → optional additional services
*
* The internal port behind each subdomain is opaque — Caddy proxies
* arbitrary local ports. Developers and Claude only ever see the URL.
*/
const fs_1 = require("fs");
const path_1 = require("path");
/**
* Build a complete identity from a project root.
*
* Detects monorepo subprojects under `projects/` automatically:
* - `projects/api` → `api.<slug>.localhost`
* - `projects/app` → `<slug>.localhost` (primary)
* - `projects/<other>` → `<other>.<slug>.localhost`
*
* For standalone projects (single repo, no `projects/` directory):
* - API project (config.env.ts present) → `api.<slug>.localhost`
* - App project (nuxt.config.ts present) → `<slug>.localhost`
*/
function buildIdentity(root) {
const slug = projectSlug(root);
const subdomains = {};
const projectsDir = (0, path_1.join)(root, 'projects');
if ((0, fs_1.existsSync)(projectsDir)) {
// Monorepo: enumerate projects/* subdirectories.
const apiDir = (0, path_1.join)(projectsDir, 'api');
const appDir = (0, path_1.join)(projectsDir, 'app');
if ((0, fs_1.existsSync)(apiDir)) {
subdomains.api = {
hostname: `api.${slug}.localhost`,
isPrimaryApp: false,
subdir: 'projects/api',
};
}
if ((0, fs_1.existsSync)(appDir)) {
subdomains.app = {
hostname: `${slug}.localhost`,
isPrimaryApp: true,
subdir: 'projects/app',
};
}
}
else {
// Standalone — derive from project shape.
const isApi = (0, fs_1.existsSync)((0, path_1.join)(root, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(root, 'nest-cli.json'));
const isApp = (0, fs_1.existsSync)((0, path_1.join)(root, 'nuxt.config.ts'));
if (isApi) {
subdomains.api = { hostname: `api.${slug}.localhost`, isPrimaryApp: false, subdir: null };
}
if (isApp) {
subdomains.app = { hostname: `${slug}.localhost`, isPrimaryApp: true, subdir: null };
}
}
return { root, slug, subdomains };
}
/**
* Derive an ephemeral "test" identity from a base identity (used by
* `lt dev test`). Suffixes the slug and every subdomain hostname with
* `-test`, so the test stack runs on its own URLs / ports / Caddy block,
* fully parallel to (and isolated from) the dev session.
*
* svl.localhost → svl-test.localhost
* api.svl.localhost → api.svl-test.localhost
*/
function buildTestIdentity(base, suffix = '-test') {
const slug = `${base.slug}${suffix}`;
const subdomains = {};
for (const [sub, value] of Object.entries(base.subdomains)) {
subdomains[sub] = Object.assign(Object.assign({}, value), { hostname: value.isPrimaryApp ? `${slug}.localhost` : `${sub}.${slug}.localhost` });
}
return { root: base.root, slug, subdomains };
}
/**
* Derive a per-TICKET identity from a base identity (used by `lt ticket` /
* `lt dev up --ticket`). Suffixes the slug + every subdomain hostname with the
* ticket id, so each ticket worktree runs on its OWN URLs / ports / Caddy block
* / DB — fully parallel to and isolated from every other ticket and the base
* dev session.
*
* svl.localhost → svl-2200.localhost
* api.svl.localhost → api.svl-2200.localhost
*
* Mechanically identical to {@link buildTestIdentity} (a named wrapper for
* readability + intent at the call sites). `id` is already a clean slug (see
* `deriveTicketId` in dev-ticket.ts).
*/
function buildTicketIdentity(base, id) {
return buildTestIdentity(base, `-${id}`);
}
/**
* Read the bare project name from package.json (scope stripped).
* Falls back to directory basename if no package.json or no `name`.
*/
function projectSlug(root) {
const fromPkg = readPackageName(root);
const raw = fromPkg || (0, path_1.basename)(root);
return slugify(raw);
}
/** Lowercase, alphanumerics + dashes only, trimmed dashes. */
function slugify(input) {
return input
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
/** Read `name` from package.json, scope-stripped (e.g. `@lenne.tech/foo` → `foo`). */
function readPackageName(dir) {
const pkgPath = (0, path_1.join)(dir, 'package.json');
if (!(0, fs_1.existsSync)(pkgPath))
return null;
try {
const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf8'));
if (!pkg.name)
return null;
return pkg.name.includes('/') ? pkg.name.split('/').pop() : pkg.name;
}
catch (_a) {
return null;
}
}