Skip to content

Commit 2e9cdbe

Browse files
committed
fix: count only a real .. segment as leaving the report outDir
checkReportOutDir treated any relative path starting with .. as outside the target, so a project folder named like ..app counted as outside its parent. With force, buildReport() or pangular build could then empty an ancestor of the project. The guard now uses the escapes() helper, which matches only a .. segment, plus an absolute path. Tests cover a root named ..app, whose parent is refused even with force.
1 parent 84dd801 commit 2e9cdbe

2 files changed

Lines changed: 21 additions & 1 deletion

File tree

‎packages/devtools/src/__tests__/cli.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ describe('pangular build --outDir', () => {
3636
}
3737
});
3838

39+
it('treats a folder named like ..app as a child, so its parent is refused even with --force', () => {
40+
const parent = fixtureDir('pangular-cli-dots-');
41+
const cwd = join(parent, '..app');
42+
mkdirSync(join(cwd, 'src'), { recursive: true });
43+
for (const outDir of [parent, '..']) {
44+
expect(() => checkReportOutDir(outDir, { cwd, force: true })).toThrow(
45+
/working directory or one of its parents/,
46+
);
47+
}
48+
expect(() =>
49+
checkReportOutDir(join(cwd, 'dist-report'), { cwd: parent, invokedFrom: cwd, force: true }),
50+
).not.toThrow();
51+
expect(() =>
52+
checkReportOutDir(parent, { cwd: join(cwd, 'src'), invokedFrom: cwd, force: true }),
53+
).toThrow(/working directory or one of its parents/);
54+
});
55+
3956
it('sees through symlinks to the working directory and its parents, even with --force', () => {
4057
const root = fixtureDir('pangular-cli-link-');
4158
const cwd = join(root, 'workspace');

‎packages/devtools/src/cli.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { pangularMcp } from './agent-mcp.ts';
2727
import { PANGULAR_MCP_TOKEN_ENV, loadMcpToken } from './mcp-token.ts';
2828
import { registerSkillsCommand } from './skills-install.ts';
2929
import { registerLintCommand } from './lint-export.ts';
30+
import { escapes } from './rpc/source-scan.ts';
3031
import pkg from '../package.json' with { type: 'json' };
3132

3233
export const PANGULAR_CONFIG_FILE = 'pangular.config.json';
@@ -55,7 +56,9 @@ export function checkReportOutDir(
5556
const kept = [cwd, ...(options.invokedFrom ? [canonical(resolve(options.invokedFrom))] : [])];
5657
const holds = (dir: string) => {
5758
const up = relative(target, dir);
58-
return up === '' || (!up.startsWith('..') && !isAbsolute(up));
59+
// Only a real `..` segment leaves the target: a folder named `..app` is inside it.
60+
const leaves = escapes(up) || isAbsolute(up);
61+
return up === '' || !leaves;
5962
};
6063
if (kept.some(holds)) {
6164
throw new Error(

0 commit comments

Comments
 (0)