Runner: Projekterkennung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 21:53:41 +00:00
1 parent bc5fddf13f
commit e2c517a015
3 files changed
+240

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
// Reine Projekterkennung (Spec 1.2 „Erkennung“): static | build | dev, Paketmanager, Befehle.
import { statSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const DEFAULT_OUTPUTS = ['dist', 'build', 'out', 'web-build'];
const DEFAULT_PORT = 5173;
const safeRel = (p) => typeof p === 'string' && p !== '' && p.length <= 200 && !p.startsWith('/')
&& !p.includes('\0') && !p.split(/[\\/]/).includes('..');
function scriptsOf(files) {
try {
const txt = files.read('package.json');
if (txt == null) return {};
const s = JSON.parse(txt)?.scripts;
return s && typeof s === 'object' && !Array.isArray(s) ? s : {};
} catch {
return {};
}
}
const hasScript = (scripts, name) => typeof scripts[name] === 'string' && scripts[name].trim() !== '';
function detectPm(files) {
if (files.has('pnpm-lock.yaml')) return 'pnpm';
if (files.has('yarn.lock')) return 'yarn';
if (files.has('bun.lockb') || files.has('bun.lock')) return 'bun';
return 'npm';
}
function installCmd(pm, files) {
if (pm === 'npm') {
return files.has('package-lock.json') || files.has('npm-shrinkwrap.json')
? ['npm', 'ci', '--include=dev']
: ['npm', 'install', '--include=dev', '--no-audit', '--no-fund'];
}
return [pm, 'install', '--frozen-lockfile'];
}
// npm/pnpm brauchen `--` vor den Skript-Argumenten, yarn/bun reichen sie direkt durch.
function devCmdFor(pm, port) {
const args = ['--host', '0.0.0.0', '--port', String(port)];
return pm === 'npm' || pm === 'pnpm' ? [pm, 'run', 'dev', '--', ...args] : [pm, 'run', 'dev', ...args];
}
export function detectProject(files, { mode, index, previewJson } = {}) {
const pj = previewJson && typeof previewJson === 'object' && !Array.isArray(previewJson) ? previewJson : {};
const scripts = scriptsOf(files);
const pm = detectPm(files);
const devPort = Number.isInteger(pj.devPort) && pj.devPort >= 1024 && pj.devPort <= 65535 ? pj.devPort : DEFAULT_PORT;
const outputDirs = safeRel(pj.output) ? [pj.output] : [...DEFAULT_OUTPUTS];
const indexFile = [index, pj.index].find(safeRel) || 'index.html';
const hasIndex = files.has(indexFile);
const hasBuild = hasScript(scripts, 'build');
const hasDev = hasScript(scripts, 'dev');
const m = mode === 'build' || mode === 'dev' ? mode : 'auto';
const base = { pm, install: installCmd(pm, files), buildCmd: hasBuild ? [pm, 'run', 'build'] : null, devCmd: hasDev ? devCmdFor(pm, devPort) : null, outputDirs, devPort };
const asBuild = () => ({ kind: 'build', ...base });
const asDev = () => ({ kind: 'dev', ...base });
const asStatic = (hint) => ({ kind: 'static', ...base, install: [], buildCmd: null, devCmd: null, outputDirs: [], index: indexFile, ...(hint ? { hint } : {}) });
if (m === 'build') return hasBuild ? asBuild() : { ...asBuild(), error: 'Kein build-Skript in package.json' };
if (m === 'dev') return hasDev ? asDev() : { ...asDev(), error: 'Kein dev-Skript in package.json' };
if (hasIndex && !hasBuild) return asStatic();
if (hasBuild) return asBuild();
if (hasDev) return asDev();
return asStatic(`Hinweis: Weder ${indexFile} noch ein build-/dev-Skript gefunden – Inhalt wird unverändert als statische Seite bereitgestellt.`);
}
// Dateisystem-Adapter für detectProject; previewJson wird aus .webklar-preview.json gelesen, wenn nicht übergeben.
export function detectFromDir(dir, opts = {}) {
const abs = (p) => join(dir, p);
const files = {
has: (p) => { try { return statSync(abs(p)).isFile(); } catch { return false; } },
read: (p) => { try { return readFileSync(abs(p), 'utf8'); } catch { return null; } },
};
let previewJson = opts.previewJson;
if (previewJson === undefined) {
try { previewJson = JSON.parse(files.read('.webklar-preview.json')); } catch { previewJson = {}; }
}
return detectProject(files, { ...opts, previewJson });
}