Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { GitRunner } from "./git.ts";
import { SystemGit } from "./git.ts";
import {
exists,
findDefaultManifestPath,
loadManifest,
manifestPaths,
resolveRepositoryPath,
Expand Down Expand Up @@ -39,7 +40,7 @@ class CliHelp extends Error {}
interface CliOptions {
command: string;
subcommand?: string;
manifestPath: string;
manifestPath?: string;
json: boolean;
stale: boolean;
dryRun: boolean;
Expand All @@ -61,7 +62,7 @@ Usage:
wspace validate

Options:
--manifest <path> Manifest path (default: repos.json)
--manifest <path> Manifest path (default: wspace.json / workspace.json / repos.json)
--json Machine-readable output
--stale Filter worktrees fully merged into origin/<default> (or missing branch)
--dry-run Preview environment sync operations without modifying files
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -351,7 +352,9 @@ Required setup steps may include:

export async function run(args: string[]): Promise<number> {
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());
Expand Down
18 changes: 18 additions & 0 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,31 @@ 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;
worktreesDirectory: string;
vaultDirectory: string;
}

export async function findDefaultManifestPath(
cwd: string = Deno.cwd(),
): Promise<string> {
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`);
Expand Down
34 changes: 33 additions & 1 deletion tests/manifest_test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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 });
}
});
Loading