mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 13:46:32 +03:00
* Fix: resolve unique --target names in monorepos (#700) Bare child names such as Cantaro.Web now match a unique workspace candidate instead of being reported missing. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: resolve --target once in the context CLI Reuse the resolved path for loadContext so a bare name does not walk workspace candidates twice. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix: match unique --target names after cwd absolutizing Live and other helpers resolve --target against cwd before context.mjs sees it. Treat a missing single-segment path the same as a bare workspace name so those callers still select the unique child. AI assistance: Cursor Grok 4.6 implemented this change. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
This commit is contained in:
co-authored by
Cursor
Paul Bakaus
parent
0330f61cef
commit
8b326fc81e
@@ -285,10 +285,31 @@ function resolveEnvContextDir(cwd) {
|
||||
return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
|
||||
}
|
||||
|
||||
function resolveTargetPath(cwd, targetPath) {
|
||||
const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(cwd, targetPath);
|
||||
if (fs.existsSync(abs)) return abs;
|
||||
return findUniqueBareTarget(cwd, targetPath) || abs;
|
||||
}
|
||||
|
||||
function findUniqueBareTarget(cwd, targetPath) {
|
||||
const absCwd = path.resolve(cwd);
|
||||
const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(absCwd, targetPath);
|
||||
const rel = path.relative(absCwd, abs);
|
||||
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
||||
const segments = rel.split(path.sep).filter(Boolean);
|
||||
if (segments.length !== 1) return null;
|
||||
const name = segments[0];
|
||||
const repoRoot = findMonorepoRoot(absCwd);
|
||||
if (!repoRoot) return null;
|
||||
const matches = discoverTargetCandidates(repoRoot).filter((candidate) => candidate.name === name);
|
||||
if (matches.length !== 1) return null;
|
||||
return path.resolve(repoRoot, matches[0].path);
|
||||
}
|
||||
|
||||
function resolveTargetDir(cwd, options = {}) {
|
||||
const targetPath = options && typeof options === 'object' ? options.targetPath : null;
|
||||
if (!targetPath || !String(targetPath).trim()) return cwd;
|
||||
const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(cwd, targetPath);
|
||||
const abs = resolveTargetPath(cwd, targetPath);
|
||||
try {
|
||||
const stat = fs.statSync(abs);
|
||||
return stat.isDirectory() ? abs : path.dirname(abs);
|
||||
@@ -1188,13 +1209,19 @@ async function cli() {
|
||||
throw err;
|
||||
}
|
||||
const targetProvided = hasTargetOption(cliOptions);
|
||||
const targetExists = targetProvided ? pathExistsForTarget(process.cwd(), cliOptions.targetPath) : null;
|
||||
const resolvedTargetPath = targetProvided
|
||||
? resolveTargetPath(process.cwd(), cliOptions.targetPath)
|
||||
: null;
|
||||
const targetExists = targetProvided ? fs.existsSync(resolvedTargetPath) : null;
|
||||
const selection = resolveTargetSelection(process.cwd(), cliOptions);
|
||||
if (selection) {
|
||||
process.stdout.write(buildTargetSelectionDirective(selection) + '\n');
|
||||
process.exit(0);
|
||||
}
|
||||
const ctx = loadContext(process.cwd(), cliOptions);
|
||||
const ctx = loadContext(
|
||||
process.cwd(),
|
||||
resolvedTargetPath ? { targetPath: resolvedTargetPath } : cliOptions,
|
||||
);
|
||||
const updateDirective = await computeUpdateDirective();
|
||||
|
||||
if (!ctx.hasProduct) {
|
||||
@@ -1308,11 +1335,6 @@ function hasTargetOption(options) {
|
||||
return !!(options && typeof options.targetPath === 'string' && options.targetPath.trim());
|
||||
}
|
||||
|
||||
function pathExistsForTarget(cwd, targetPath) {
|
||||
const abs = path.isAbsolute(targetPath) ? targetPath : path.resolve(cwd, targetPath);
|
||||
return fs.existsSync(abs);
|
||||
}
|
||||
|
||||
const HOOK_MANIFESTS_BY_PROVIDER = Object.freeze({
|
||||
'claude-code': ['.claude/settings.local.json', '.claude/settings.json'],
|
||||
codex: ['.codex/hooks.json'],
|
||||
|
||||
@@ -770,6 +770,95 @@ describe('loadContext (monorepo project context)', () => {
|
||||
assert.match(res.stdout, /MONOREPO_TARGET_REQUIRED/);
|
||||
});
|
||||
|
||||
it('resolves a unique workspace child by bare basename via --target', () => {
|
||||
writeMonorepo();
|
||||
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n\n## Platform\n\nweb\n');
|
||||
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'dashboard'], {
|
||||
cwd: scratch,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
||||
});
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /# Dashboard product/);
|
||||
assert.match(res.stdout, /"targetExists": true/);
|
||||
assert.match(res.stdout, /"targetPath": "dashboard"/);
|
||||
assert.doesNotMatch(res.stdout, /MONOREPO_TARGET_REQUIRED/);
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'dashboard' });
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'dashboard'));
|
||||
});
|
||||
|
||||
it('resolves a dotted workspace child name by bare basename via --target', () => {
|
||||
write('package.json', JSON.stringify({
|
||||
private: true,
|
||||
workspaces: ['src/*'],
|
||||
}, null, 2));
|
||||
write('src/Cantaro.Web/PRODUCT.md', '# Cantaro Web product\n\n## Platform\n\nweb\n');
|
||||
write('src/Cantaro.Api/package.json', JSON.stringify({ name: 'cantaro-api' }));
|
||||
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'Cantaro.Web'], {
|
||||
cwd: scratch,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
||||
});
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /# Cantaro Web product/);
|
||||
assert.match(res.stdout, /"targetExists": true/);
|
||||
assert.match(res.stdout, /"targetPath": "Cantaro\.Web"/);
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'Cantaro.Web' });
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'src', 'Cantaro.Web'));
|
||||
});
|
||||
|
||||
it('does not guess when a bare basename matches multiple workspace children', () => {
|
||||
write('package.json', JSON.stringify({
|
||||
private: true,
|
||||
workspaces: ['apps/*', 'packages/*'],
|
||||
}, null, 2));
|
||||
write('turbo.json', JSON.stringify({ tasks: {} }));
|
||||
write('apps/web/src/App.jsx', 'export default null;\n');
|
||||
write('packages/web/src/index.ts', 'export {};\n');
|
||||
const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'web'], {
|
||||
cwd: scratch,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
|
||||
});
|
||||
assert.equal(res.status, 0, res.stderr);
|
||||
assert.match(res.stdout, /"targetExists": false/);
|
||||
assert.match(res.stdout, /MONOREPO_TARGET_REQUIRED/);
|
||||
|
||||
const ctx = loadContext(scratch, { targetPath: 'web' });
|
||||
assert.equal(ctx.projectRoot, scratch);
|
||||
});
|
||||
|
||||
it('resolves a unique workspace child when the target was already resolved against cwd', () => {
|
||||
writeMonorepo();
|
||||
write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
|
||||
const ctx = loadContext(scratch, { targetPath: path.join(scratch, 'dashboard') });
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'dashboard'));
|
||||
});
|
||||
|
||||
it('does not guess a nested product basename in a non-monorepo repo', () => {
|
||||
write('packages/checkout/PRODUCT.md', '# Checkout product\n');
|
||||
write('packages/checkout/file.ts', 'export const x = 1;\n');
|
||||
const bare = loadContext(scratch, { targetPath: 'checkout' });
|
||||
assert.equal(bare.isMonorepo, false);
|
||||
assert.equal(bare.projectRoot, scratch);
|
||||
const explicit = loadContext(scratch, { targetPath: 'packages/checkout/file.ts' });
|
||||
assert.equal(explicit.projectRoot, path.join(scratch, 'packages', 'checkout'));
|
||||
assert.match(explicit.product, /Checkout product/);
|
||||
});
|
||||
|
||||
it('does not select a negated workspace child by basename', () => {
|
||||
write('pnpm-workspace.yaml', 'packages:\n - "packages/*"\n - "!packages/internal"\n');
|
||||
write('PRODUCT.md', '# Root product\n');
|
||||
write('packages/ui/src/index.ts', 'export {};\n');
|
||||
write('packages/internal/PRODUCT.md', '# Internal product\n');
|
||||
write('packages/internal/src/index.ts', 'export {};\n');
|
||||
const ctx = loadContext(scratch, { targetPath: 'internal' });
|
||||
assert.equal(ctx.projectRoot, scratch);
|
||||
assert.match(ctx.product, /Root product/);
|
||||
});
|
||||
|
||||
it('asks for app selection even when root PRODUCT.md is absent', () => {
|
||||
write('package.json', JSON.stringify({
|
||||
private: true,
|
||||
@@ -812,6 +901,16 @@ describe('loadContext (impeccable projectRoots config)', () => {
|
||||
assert.equal(ctx.productPath, 'PRODUCT.md');
|
||||
});
|
||||
|
||||
it('resolves a unique projectRoots child by bare basename', () => {
|
||||
writeSkinsConfig();
|
||||
write('docs/design/skins/neon-seoul/DESIGN.md', '# Neon Seoul design\n');
|
||||
write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
|
||||
const ctx = loadContext(scratch, { targetPath: 'neon-seoul' });
|
||||
assert.equal(ctx.projectRoot, path.join(scratch, 'docs', 'design', 'skins', 'neon-seoul'));
|
||||
assert.match(ctx.design, /Neon Seoul design/);
|
||||
assert.match(ctx.product, /Root product/);
|
||||
});
|
||||
|
||||
it('resolves a config-declared child from cwd inside the folder', () => {
|
||||
writeSkinsConfig();
|
||||
write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
|
||||
|
||||
Reference in New Issue
Block a user