diff --git a/src/cli.ts b/src/cli.ts index 2bb99d0..23bbefc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -6,6 +6,7 @@ import type { GitRunner } from "./git.ts"; import { SystemGit } from "./git.ts"; import { exists, + findDefaultManifestPath, loadManifest, manifestPaths, resolveRepositoryPath, @@ -39,7 +40,7 @@ class CliHelp extends Error {} interface CliOptions { command: string; subcommand?: string; - manifestPath: string; + manifestPath?: string; json: boolean; stale: boolean; dryRun: boolean; @@ -61,7 +62,7 @@ Usage: wspace validate Options: - --manifest Manifest path (default: repos.json) + --manifest Manifest path (default: wspace.json / workspace.json / repos.json) --json Machine-readable output --stale Filter worktrees fully merged into origin/ (or missing branch) --dry-run Preview environment sync operations without modifying files @@ -93,7 +94,7 @@ function parseCliArgs(args: string[]): CliOptions { return { command, subcommand: positional[1], - manifestPath: parsed.manifest ?? "repos.json", + manifestPath: parsed.manifest, json: parsed.json ?? false, stale: parsed.stale ?? false, dryRun: parsed["dry-run"] ?? false, @@ -351,7 +352,9 @@ Required setup steps may include: export async function run(args: string[]): Promise { const opts = parseCliArgs(args); - const manifestPath = resolve(Deno.cwd(), opts.manifestPath); + const manifestPath = opts.manifestPath + ? resolve(Deno.cwd(), opts.manifestPath) + : await findDefaultManifestPath(); const manifest = await loadManifest(manifestPath); const paths = manifestPaths(manifest, manifestPath); return await runCommand(opts, manifest, paths, new SystemGit()); diff --git a/src/manifest.ts b/src/manifest.ts index 47497f7..ea4bcf0 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -3,6 +3,12 @@ import type { RepositoryEntry, WorkspaceManifest } from "./types.ts"; export const CURRENT_SCHEMA_VERSION = 1; +export const DEFAULT_MANIFEST_FILENAMES = [ + "wspace.json", + "workspace.json", + "repos.json", +]; + export interface ManifestPaths { root: string; repositoriesDirectory: string; @@ -10,6 +16,18 @@ export interface ManifestPaths { vaultDirectory: string; } +export async function findDefaultManifestPath( + cwd: string = Deno.cwd(), +): Promise { + for (const filename of DEFAULT_MANIFEST_FILENAMES) { + const candidate = resolve(cwd, filename); + if (await exists(candidate)) { + return candidate; + } + } + return resolve(cwd, DEFAULT_MANIFEST_FILENAMES[0]); +} + export function validateSafeName(name: string, contextName = "Name"): void { if (!name || typeof name !== "string" || name.trim() === "") { throw new Error(`${contextName} cannot be empty`); diff --git a/tests/manifest_test.ts b/tests/manifest_test.ts index 855c0bb..d40de45 100644 --- a/tests/manifest_test.ts +++ b/tests/manifest_test.ts @@ -1,6 +1,10 @@ import { assertEquals, assertThrows } from "@std/assert"; import { dirname, join } from "@std/path"; -import { manifestPaths, validateManifest } from "../src/manifest.ts"; +import { + findDefaultManifestPath, + manifestPaths, + validateManifest, +} from "../src/manifest.ts"; import type { WorkspaceManifest } from "../src/types.ts"; Deno.test("validateManifest accepts a valid manifest", () => { @@ -80,3 +84,31 @@ Deno.test("validateManifest rejects invalid repo names or traversal", () => { "invalid characters or path traversal", ); }); + +Deno.test("findDefaultManifestPath respects fallback order wspace.json -> workspace.json -> repos.json", async () => { + const tempDir = await Deno.makeTempDir(); + try { + // When no manifest exists, defaults to wspace.json + assertEquals( + await findDefaultManifestPath(tempDir), + join(tempDir, "wspace.json"), + ); + + // If repos.json exists, resolves repos.json + const reposPath = join(tempDir, "repos.json"); + await Deno.writeTextFile(reposPath, "{}"); + assertEquals(await findDefaultManifestPath(tempDir), reposPath); + + // If workspace.json exists, takes priority over repos.json + const workspacePath = join(tempDir, "workspace.json"); + await Deno.writeTextFile(workspacePath, "{}"); + assertEquals(await findDefaultManifestPath(tempDir), workspacePath); + + // If wspace.json exists, takes top priority + const wspacePath = join(tempDir, "wspace.json"); + await Deno.writeTextFile(wspacePath, "{}"); + assertEquals(await findDefaultManifestPath(tempDir), wspacePath); + } finally { + await Deno.remove(tempDir, { recursive: true }); + } +});