diff --git a/packages/cli-workflow/__tests__/init-workspace.test.ts b/packages/cli-workflow/__tests__/init-workspace.test.ts index 7c682c9..07527d8 100644 --- a/packages/cli-workflow/__tests__/init-workspace.test.ts +++ b/packages/cli-workflow/__tests__/init-workspace.test.ts @@ -125,9 +125,6 @@ describe("init workspace", () => { }); test("errors on invalid workspace name", async () => { - const slash = await cmdInitWorkspace(parent, "a/b"); - expect(slash.ok).toBe(false); - const dots = await cmdInitWorkspace(parent, ".."); expect(dots.ok).toBe(false); @@ -135,6 +132,14 @@ describe("init workspace", () => { expect(empty.ok).toBe(false); }); + test("accepts nested path as workspace name", async () => { + const nested = await cmdInitWorkspace(parent, "a/b"); + expect(nested.ok).toBe(true); + if (nested.ok) { + expect(nested.value.rootPath).toContain("a/b"); + } + }); + test("usage lists init subcommands", () => { const u = formatCliUsage(); expect(u).toContain("init workspace "); diff --git a/packages/cli-workflow/src/commands/init/workspace.ts b/packages/cli-workflow/src/commands/init/workspace.ts index 8b3bea9..26c935c 100644 --- a/packages/cli-workflow/src/commands/init/workspace.ts +++ b/packages/cli-workflow/src/commands/init/workspace.ts @@ -1,5 +1,5 @@ import { mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { basename, join, resolve } from "node:path"; import { err, ok, type Result } from "@uncaged/workflow-protocol"; @@ -297,27 +297,30 @@ export async function cmdInitWorkspace( parentDir: string, workspaceName: string, ): Promise> { - const validated = validateWorkspaceSegment(workspaceName); - if (!validated.ok) { - return validated; + // Accept a relative/absolute path: resolve it and derive the dir name for package.json. + const resolved = resolve(parentDir, workspaceName); + const rootPath = resolved; + const dirName = basename(resolved); + + if (dirName === "" || dirName === "." || dirName === "..") { + return err(`invalid workspace path: ${workspaceName}`); } - const rootPath = join(parentDir, workspaceName); if (await pathExists(rootPath)) { return err(`directory already exists: ${rootPath}`); } - await mkdir(rootPath, { recursive: false }); - await mkdir(join(rootPath, "templates"), { recursive: false }); - await mkdir(join(rootPath, "workflows"), { recursive: false }); - await mkdir(join(rootPath, "scripts"), { recursive: false }); + await mkdir(rootPath, { recursive: true }); + await mkdir(join(rootPath, "templates"), { recursive: true }); + await mkdir(join(rootPath, "workflows"), { recursive: true }); + await mkdir(join(rootPath, "scripts"), { recursive: true }); await Promise.all([ - writeFile(join(rootPath, "package.json"), rootPackageJson(workspaceName), "utf8"), + writeFile(join(rootPath, "package.json"), rootPackageJson(dirName), "utf8"), writeFile(join(rootPath, "biome.json"), biomeJson(), "utf8"), writeFile(join(rootPath, "tsconfig.json"), tsconfigJson(), "utf8"), writeFile(join(rootPath, "AGENTS.md"), agentsMd(), "utf8"), - writeFile(join(rootPath, "README.md"), readmeMd(workspaceName), "utf8"), + writeFile(join(rootPath, "README.md"), readmeMd(dirName), "utf8"), writeFile(join(rootPath, "templates", ".gitkeep"), "", "utf8"), writeFile(join(rootPath, "workflows", "package.json"), workflowsPackageJson(), "utf8"), writeFile(join(rootPath, "workflows", ".gitkeep"), "", "utf8"), diff --git a/packages/cli-workflow/src/commands/setup/dispatch.ts b/packages/cli-workflow/src/commands/setup/dispatch.ts index 4d4f0d4..a1ba638 100644 --- a/packages/cli-workflow/src/commands/setup/dispatch.ts +++ b/packages/cli-workflow/src/commands/setup/dispatch.ts @@ -1,5 +1,7 @@ +import { existsSync } from "node:fs"; import { stdin as input, stdout as output } from "node:process"; import { createInterface } from "node:readline/promises"; +import { resolve } from "node:path"; import { err, ok, type Result } from "@uncaged/workflow-protocol"; @@ -317,15 +319,29 @@ async function collectInteractiveSetup(): Promise> const defaultModel = `${provider}/${bare}`; printCliLine(` → ${defaultModel}`); - const wsPath = await promptLine( - rl2, - "\nWorkflow workspace path (default: ./workflows, type 'skip' to skip): ", - ); + let initWorkspaceName: string | null = null; + // Loop until a valid workspace path is provided or the user skips. + while (true) { + const wsPath = await promptLine( + rl2, + "\nWorkflow workspace path (default: ./workflows, type 'skip' to skip): ", + ); + if (wsPath.toLowerCase() === "skip") { + break; + } + const candidate = wsPath === "" ? "./workflows" : wsPath; + // Validate path before passing to cmdSetup. + const resolved = resolve(process.cwd(), candidate); + if (existsSync(resolved)) { + printCliWarn(`directory already exists: ${resolved}`); + printCliLine("Please enter a different path, or type 'skip' to skip."); + continue; + } + initWorkspaceName = candidate; + break; + } rl2.close(); - const initWorkspaceName = - wsPath.toLowerCase() === "skip" ? null : wsPath === "" ? "./workflows" : wsPath; - return ok({ provider, baseUrl,