Skip to content
Open
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
15 changes: 15 additions & 0 deletions packages/loop-js/src/engine/fresh-safety.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect, test } from "bun:test"
import { dirname, join, resolve } from "node:path"
import { assertFreshable } from "./loop.ts"

const root = resolve("fresh-safety-project")

test("fresh refuses the project root and any ancestor that contains it", () => {
expect(() => assertFreshable({ root, workspaceDir: root })).toThrow(/project root/)
expect(() => assertFreshable({ root, workspaceDir: dirname(root) })).toThrow(/contains the project root/)
})

test("fresh still allows a child workspace and an external sibling workspace", () => {
expect(() => assertFreshable({ root, workspaceDir: join(root, "workspace") })).not.toThrow()
expect(() => assertFreshable({ root, workspaceDir: join(dirname(root), "other-workspace") })).not.toThrow()
})
15 changes: 12 additions & 3 deletions packages/loop-js/src/engine/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { rmSync } from "node:fs"
import { join, resolve } from "node:path"
import { isAbsolute, join, relative, resolve, sep } from "node:path"
import type {
PromptCtx,
Exit,
Expand Down Expand Up @@ -55,10 +55,19 @@ const interrupt = (cause: InterruptCause, reason: string): Exit => ({ settled: f
/** The one Verdict → wire projection — `verdict` events and Status can never disagree on shape. */
const verdictWire = (v: Verdict): VerdictWire => ({ ok: v.ok, impossible: v.ok ? false : v.impossible, reason: v.reason })

function assertFreshable(paths: LoopPaths): void {
if (resolve(paths.workspaceDir) === resolve(paths.root)) {
export function assertFreshable(paths: Pick<LoopPaths, "root" | "workspaceDir">): void {
const root = resolve(paths.root)
const workspace = resolve(paths.workspaceDir)
const fromWorkspaceToRoot = relative(workspace, root)
const workspaceContainsRoot =
fromWorkspaceToRoot === "" ||
(fromWorkspaceToRoot !== ".." && !fromWorkspaceToRoot.startsWith(`..${sep}`) && !isAbsolute(fromWorkspaceToRoot))

if (!workspaceContainsRoot) return
if (fromWorkspaceToRoot === "") {
throw new Error("loop: refusing `fresh` when workspace is the project root — 'clear' has no safe meaning")
}
throw new Error("loop: refusing `fresh` when workspace contains the project root — recursive clear would delete the project")
}

function applyFresh(paths: LoopPaths): void {
Expand Down