fix: address review feedback on !include and folder workflow

- Fix nested !include: pass customTags recursively, scoped to included file's dir
- Add path traversal guard: !include paths must resolve within base directory
- Fix discoverProjectWorkflows: scan both .workflow/ and .workflows/ (consistent with findWorkflowInDir)
- Add tests: path traversal blocking, nested !include, absolute path rejection
This commit is contained in:
2026-05-31 04:26:54 +00:00
parent 88c251fc14
commit da1678ffef
3 changed files with 62 additions and 9 deletions
+23 -6
View File
@@ -21,13 +21,10 @@ export type ProjectWorkflowEntry = {
};
/**
* Scan `<projectRoot>/.workflows/*.yaml` (non-recursive) and return discovered entries.
* Returns an empty array if the directory does not exist.
* Scan a single directory for workflow entries (flat YAML files + folder/index.yaml).
* Returns discovered entries. Returns empty array if directory does not exist.
*/
export async function discoverProjectWorkflows(
projectRoot: string,
): Promise<ProjectWorkflowEntry[]> {
const dir = join(projectRoot, ".workflows");
async function scanWorkflowDir(dir: string): Promise<ProjectWorkflowEntry[]> {
let dirents: Dirent[];
try {
dirents = await readdir(dir, { withFileTypes: true });
@@ -60,6 +57,26 @@ export async function discoverProjectWorkflows(
return result;
}
/**
* Scan `<projectRoot>/.workflow/` (preferred) and `.workflows/` (legacy) for workflow entries.
* .workflow/ takes priority: if a name is found in both, .workflow/ wins.
* Returns an empty array if neither directory exists.
*/
export async function discoverProjectWorkflows(
projectRoot: string,
): Promise<ProjectWorkflowEntry[]> {
const primary = await scanWorkflowDir(join(projectRoot, ".workflow"));
const legacy = await scanWorkflowDir(join(projectRoot, ".workflows"));
const seen = new Set(primary.map((e) => e.name));
const merged = [...primary];
for (const entry of legacy) {
if (!seen.has(entry.name)) {
merged.push(entry);
}
}
return merged;
}
/** Default filesystem root for uwf data (`~/.uncaged/workflow`). */
export function getDefaultStorageRoot(): string {
return join(homedir(), ".uncaged", "workflow");