mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 10:36:27 +03:00
Add monorepo context support (#213)
Context files (PRODUCT.md / DESIGN.md) resolve child-first then fall back to the repo root, and /impeccable live lets the user pick a child app in a monorepo. Single-app behavior is unchanged. Closes #202. Co-Authored-By: abdulwahabone
This commit is contained in:
+72
-21
@@ -21,14 +21,16 @@ import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadContext } from './context.mjs';
|
||||
import { loadContext, resolveTargetSelection } from './context.mjs';
|
||||
import { resolveFiles } from './live-inject.mjs';
|
||||
import { readLiveServerInfo } from './lib/impeccable-paths.mjs';
|
||||
import { resolveLiveTarget } from './live-target.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function liveCli() {
|
||||
const args = process.argv.slice(2);
|
||||
const liveTarget = resolveLiveTarget(process.cwd(), args);
|
||||
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(`Usage: node live.mjs
|
||||
@@ -38,37 +40,78 @@ Prepare everything for live variant mode in a single command:
|
||||
- Starts (or reuses) the live server in the background
|
||||
- Injects the browser script tag
|
||||
- Reads PRODUCT.md / DESIGN.md for project context
|
||||
- In monorepos, choose a child app first; --target <path> is the fallback/manual path
|
||||
|
||||
On success, prints a JSON blob with:
|
||||
{ ok, serverPort, serverToken, pageFile, hasContext, context }
|
||||
{ ok, serverPort, serverToken, pageFiles, projectRoot, repoRoot, targetPath, productPath, designPath }
|
||||
|
||||
On target_selection_required, prints:
|
||||
{ ok: false, error: "target_selection_required", targetCandidates }
|
||||
|
||||
On config_missing, prints:
|
||||
{ ok: false, error: "config_missing", configPath, hint }
|
||||
|
||||
The agent should then:
|
||||
1. If config_missing, create the config and re-run this script
|
||||
2. Optionally open the project's dev/preview URL in the browser (see reference/live.md—not serverPort)
|
||||
3. Enter the poll loop: node live-poll.mjs`);
|
||||
1. If target_selection_required, ask which app to use and rerun from that child cwd
|
||||
2. If config_missing, create the config and re-run this script
|
||||
3. Optionally open the project's dev/preview URL in the browser (see reference/live.md—not serverPort)
|
||||
4. Enter the poll loop: node live-poll.mjs`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const targetSelection = resolveTargetSelection(liveTarget.originalCwd, liveTarget.targetOptions);
|
||||
if (targetSelection) {
|
||||
console.log(JSON.stringify({
|
||||
ok: false,
|
||||
error: 'target_selection_required',
|
||||
...targetSelection,
|
||||
hint: 'Ask the user which app Impeccable should use, then rerun live from that child app cwd. Use --target <path> only as a fallback or explicit path diagnostic.',
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const ctx = loadContext(liveTarget.originalCwd, liveTarget.targetOptions);
|
||||
const activeCwd = ctx.projectRoot;
|
||||
const outputTargetPath = liveTarget.targetPath || null;
|
||||
|
||||
const missingContext = missingLiveContext(ctx);
|
||||
if (missingContext.length > 0) {
|
||||
console.log(JSON.stringify({
|
||||
ok: false,
|
||||
error: 'context_missing',
|
||||
missing: missingContext,
|
||||
nextCommand: missingContext.includes('PRODUCT.md') ? 'init' : 'document',
|
||||
targetPath: outputTargetPath,
|
||||
projectRoot: ctx.projectRoot,
|
||||
repoRoot: ctx.repoRoot,
|
||||
productPath: ctx.productPath,
|
||||
designPath: ctx.designPath,
|
||||
}, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 1. Check config (fail fast if missing — no point starting anything else)
|
||||
const checkOut = runScript('live-inject.mjs', ['--check']);
|
||||
const checkOut = runScript('live-inject.mjs', ['--check'], { cwd: activeCwd });
|
||||
const checkResult = safeParse(checkOut);
|
||||
if (!checkResult || !checkResult.ok) {
|
||||
console.log(JSON.stringify(checkResult || { ok: false, error: 'check_failed', raw: checkOut }));
|
||||
console.log(JSON.stringify({
|
||||
...(checkResult || { ok: false, error: 'check_failed', raw: checkOut }),
|
||||
targetPath: outputTargetPath,
|
||||
projectRoot: ctx.projectRoot,
|
||||
repoRoot: ctx.repoRoot,
|
||||
}));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// 2. Start server (or reuse existing)
|
||||
const serverInfo = ensureServerRunning();
|
||||
const serverInfo = ensureServerRunning(activeCwd);
|
||||
if (!serverInfo) {
|
||||
console.log(JSON.stringify({ ok: false, error: 'server_start_failed' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. Inject the script tag at the current port
|
||||
const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)]);
|
||||
const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)], { cwd: activeCwd });
|
||||
const injectResult = safeParse(injectOut);
|
||||
if (!injectResult || !injectResult.ok) {
|
||||
console.log(JSON.stringify({
|
||||
@@ -80,22 +123,23 @@ The agent should then:
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 4. Load PRODUCT.md + DESIGN.md context.
|
||||
const ctx = loadContext(process.cwd());
|
||||
|
||||
// 5. Compute drift-heal: compare resolved inject targets against the
|
||||
// 4. Compute drift-heal: compare resolved inject targets against the
|
||||
// project's HTML files. Orphans are HTML files not covered by config.
|
||||
// Warning only — the agent decides whether to act.
|
||||
const resolvedFiles = resolveFiles(process.cwd(), checkResult.config);
|
||||
const drift = scanForDrift(process.cwd(), resolvedFiles, checkResult.config);
|
||||
const resolvedFiles = resolveFiles(activeCwd, checkResult.config);
|
||||
const drift = scanForDrift(activeCwd, resolvedFiles, checkResult.config);
|
||||
|
||||
// 6. Emit everything the agent needs
|
||||
// 5. Emit everything the agent needs
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
serverPort: serverInfo.port,
|
||||
serverToken: serverInfo.token,
|
||||
pageFiles: resolvedFiles,
|
||||
liveConfigPath: checkResult.path,
|
||||
configDrift: drift,
|
||||
targetPath: outputTargetPath,
|
||||
projectRoot: ctx.projectRoot,
|
||||
repoRoot: ctx.repoRoot,
|
||||
hasProduct: ctx.hasProduct,
|
||||
product: ctx.product,
|
||||
productPath: ctx.productPath,
|
||||
@@ -105,6 +149,13 @@ The agent should then:
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
function missingLiveContext(ctx) {
|
||||
const missing = [];
|
||||
if (!ctx.hasProduct) missing.push('PRODUCT.md');
|
||||
if (!ctx.hasDesign) missing.push('DESIGN.md');
|
||||
return missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drift-heal scan. Walks the project for HTML files under common
|
||||
* page-source directories (public/, src/, app/, pages/) and reports any
|
||||
@@ -201,11 +252,11 @@ function globToRegex(pattern) {
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function runScript(name, args) {
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: process.cwd(), timeout: 15_000 });
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
@@ -219,10 +270,10 @@ function safeParse(out) {
|
||||
/**
|
||||
* Return { pid, port, token } for the running live server, starting one if needed.
|
||||
*/
|
||||
function ensureServerRunning() {
|
||||
function ensureServerRunning(cwd = process.cwd()) {
|
||||
// Try to reuse an existing server
|
||||
try {
|
||||
const existing = readLiveServerInfo(process.cwd())?.info;
|
||||
const existing = readLiveServerInfo(cwd)?.info;
|
||||
if (existing && existing.pid) {
|
||||
try {
|
||||
process.kill(existing.pid, 0); // throws if dead
|
||||
@@ -232,7 +283,7 @@ function ensureServerRunning() {
|
||||
} catch { /* no PID file */ }
|
||||
|
||||
// Start a new server
|
||||
const out = runScript('live-server.mjs', ['--background']);
|
||||
const out = runScript('live-server.mjs', ['--background'], { cwd });
|
||||
return safeParse(out);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user