Files
Claude-Code-Monitor/server/lib/lane-detect.js
T

87 lines
2.9 KiB
JavaScript

/**
* @file Node.js project detection and `.ccam/profile/` scaffolding (A3).
* `detectNode` reads a repository's filesystem (package.json layout,
* docker-compose.yml) without executing anything; `scaffoldProfile` turns
* those facts into a working profile; `checkProfile` validates one. Only a
* single preset (Node.js, single-service or a flat backend/+frontend/
* monorepo) is detected — anything else is refused rather than guessed, per
* `docs/superpowers/specs/2026-08-03-lane-profile-scaffolding-design.md`.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
/**
* A name safe to embed as literal text inside a generated shell script (an
* npm script name, a docker-compose service name). Deliberately NOT applied
* to profile.env `KEY=VALUE` values — those are parsed, never sourced, and
* already safe by construction (see `lane-profile.js:parseEnvFile`). This
* guard is only for names that get spliced into `.sh` file TEXT.
*/
const IDENTIFIER_RE = /^[\w.:-]+$/;
/** Read and parse a package.json, or null if it doesn't exist or is invalid JSON. */
function readPackageJson(dir) {
try {
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
} catch {
return null;
}
}
/**
* First candidate script name present in `pkg.scripts` AND safe to embed in
* shell text. Returns null (never a guess) when no candidate qualifies.
*
* @param {object|null} pkg - Parsed package.json.
* @param {string[]} candidates - Script names in preference order.
* @returns {string|null}
*/
function pickScript(pkg, candidates) {
if (!pkg || typeof pkg.scripts !== "object" || !pkg.scripts) return null;
for (const name of candidates) {
if (typeof pkg.scripts[name] === "string" && IDENTIFIER_RE.test(name)) return name;
}
return null;
}
/**
* Detect a Node.js project's layout and, for each service, which npm script
* to run. Returns null when neither supported layout matches — the caller
* must refuse rather than guess.
*
* @param {string} repoPath - Absolute path to the repository root.
* @returns {object|null}
*/
function detectNode(repoPath) {
const backendPkg = readPackageJson(path.join(repoPath, "backend"));
const frontendPkg = readPackageJson(path.join(repoPath, "frontend"));
if (backendPkg && frontendPkg) {
return {
layout: "monorepo",
services: {
backend: { dir: "backend", script: pickScript(backendPkg, ["start", "dev"]) },
frontend: { dir: "frontend", script: pickScript(frontendPkg, ["preview", "dev"]) },
},
database: null,
env: null,
};
}
const rootPkg = readPackageJson(repoPath);
if (rootPkg) {
return {
layout: "single-service",
services: { app: { dir: ".", script: pickScript(rootPkg, ["start", "dev"]) } },
database: null,
env: null,
};
}
return null;
}
module.exports = { IDENTIFIER_RE, readPackageJson, pickScript, detectNode };