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
13 changes: 7 additions & 6 deletions .agents/skills/verify-venfork/references/features/clone.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ tests:

- Needs gh auth first (`AuthenticationError`).
- Refuses an empty or invalid repo ("Invalid vendor repository") and an existing `./<name>` directory ("Directory '<name>' already exists."). Both exit 1.
- `gh repo clone <owner/name> <name>`, then reads `venfork-config` from the mirror. When it exists it is authoritative: `--no-public` on a standard config and a different `--upstream` are refused.
- Legacy mirror without a config: guesses the public fork by stripping `-private` (`gh repo view`), then the upstream from the fork's parent. Each failed guess prompts for a URL. `--no-public` skips the fork; `--upstream` skips the parent lookup.
- `gh repo clone <owner/name> <name>`, then reads `venfork-config` from the mirror. When it exists it is authoritative: `--no-public` on a standard config, `--public-fork` on a no-public config, and an `--upstream` or `--public-fork` naming another repo are refused (exit 1). A flag naming the recorded repo in another URL form is accepted.
- Legacy mirror without a config: guesses the public fork by stripping `-private` (`gh repo view`), then the upstream from the fork's parent. Each failed guess prompts for a URL; an empty answer is rejected by `validate`, and the fork answer must be a GitHub URL or `owner/repo`. `--no-public` skips the fork; `--public-fork <url>` (URL or `owner/repo`) skips the fork guess and its prompt, and the parent lookup then runs on that fork; `--upstream` skips the parent lookup. `--public-fork` with `--no-public` is refused, and an invalid `--public-fork` exits 1 before cloning.
- Adds `public` (standard mode) and `upstream` with push URL `DISABLE`. URLs follow `gh config get git_protocol`. Runs `gh repo set-default`.
- A cancelled URL prompt exits 1, not 130. A prompt that hits stdin EOF exits 130.
- Never prompts without a TTY on stdin, with `CI` set, with `VENFORK_NONINTERACTIVE=1` or with `--no-input`: a URL prompt exits 2 (`MissingInputError`) naming `--upstream <url>` (or `--no-public --upstream <url>` for the public fork URL). `--yes` is refused: clone has no confirm. Any failure or cancel after the clone removes the cloned directory, so the rerun with the named flag works.
- A cancelled URL prompt exits 130 (`CommandExitError(130)`), and so does a prompt that hits stdin EOF.
- Never prompts without a TTY on stdin, with `CI` set, with `VENFORK_NONINTERACTIVE=1` or with `--no-input`: a URL prompt exits 2 (`MissingInputError`) naming `--public-fork <url>` for the public fork URL and `--upstream <url>` for the upstream URL. `--yes` is refused: clone has no confirm. Any failure or cancel after the clone removes the cloned directory, so the rerun with the named flag works.
- It never writes `venfork-config` and never pushes.

## How a user reaches it

`venfork clone <vendor-repo> [--no-public] [--upstream <url>] [--no-input]`. `<vendor-repo>` is the private mirror as a GitHub URL or `owner/repo`.
`venfork clone <vendor-repo> [--no-public] [--upstream <url>] [--public-fork <url>] [--no-input]`. `<vendor-repo>` is the private mirror as a GitHub URL or `owner/repo`.

## How to drive it

Expand All @@ -39,7 +39,8 @@ venfork clone a/b-private </dev/null 2>&1 | grep "gh stub" # exit
```

- Already current: a second clone into the same directory is refused, which is the idempotence guard.
- Cancel at a URL prompt, the exit-2 URL prompts and the concurrent lease path are not reachable on the fixture: the first gh call fails first. `tests/unit/commands.test.ts` pins the `--no-input` exit-2 path.
- The fixture's gh stub fails `gh repo clone`, so the legacy paths need a gh that clones the fixture origin. `tests/integration/cli.test.ts` ("clone on a legacy mirror without venfork-config") deletes origin's `venfork-config` branch and uses such a stub: `clone acme/legacy-private --public-fork other-org/legacy` exits 0 with `public` = `git@github.com:other-org/legacy.git` and `upstream` = the fixture upstream (push `DISABLE`), and the same clone without `--public-fork` and without a TTY exits 2 with "Pass --public-fork <url>.".
- Cancel at a URL prompt needs a TTY; `tests/unit/commands.test.ts` pins the exit-130 paths and the `validate` rules.

## What proves it

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/verify-venfork/references/features/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ tests:
## What exists

- Needs gh auth first (`AuthenticationError`: "GitHub CLI is not authenticated. Please run: gh auth login").
- Prompts for the upstream when it is missing and for the mirror name when it is missing (default `<repo>-private`). With no `--org` and no `VENFORK_ORG`, asks "Continue with personal account?"; `--yes` answers it.
- Prompts for the upstream when it is missing and for the mirror name when it is missing (default `<repo>-private`). With no `--org` and no `VENFORK_ORG`, asks "Continue with personal account?"; `--yes` answers it. `--org` set to your own gh username (any case) is consent: the repos go under the personal account (no `gh repo fork --org`) and the confirm is skipped, with or without a TTY. `VENFORK_ORG` set to your username is not consent: the repos still go under the personal account, but the confirm runs (exit 2 naming `--yes` without a TTY).
- Never prompts without a TTY on stdin, with `CI` set (not `false`/`0`), with `VENFORK_NONINTERACTIVE=1` or with `--no-input`. Then a missing mirror name becomes `<repo>-private` (logged on stderr), a missing upstream exits 2 naming "the <upstream> argument", and the personal-account confirm without `--yes` exits 2 naming `--yes` (`MissingInputError`). stdout stays empty.
- Standard mode: `gh repo fork` (public fork), `gh repo create --private` (mirror), seeds a new mirror from a temp clone of upstream in chunks (`VENFORK_SEED_CHUNK`, `VENFORK_SEED_RETRY_MS`), `gh repo clone`s the mirror into `./<name>`, `gh repo set-default`.
- Writes the orphan `venfork-config` branch. Never overwrites one: an existing config that agrees is kept, one that disagrees is a `ConfigError` ("The existing venfork-config disagrees with this setup").
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Creates the complete vendor workflow setup:
- `--org <organization>` - (Optional) Create repos under organization instead of personal account
- `--fork-name <repo>` - (Optional) Public fork repo name under that org/user (default: same basename as upstream)
- `--no-public` - (Optional) Skip the public-fork hop; stage pushes directly to upstream
- `--yes`, `-y` - (Optional) Answer the personal-account confirm with yes
- `--yes`, `-y` - (Optional) Answer the personal-account confirm with yes. `--org <your username>` (compared with your gh login ignoring case) also counts as consent: the repos go under your personal account with no confirm. `VENFORK_ORG` set to your username does not: it is ambient, so the confirm still runs (and needs `--yes` without a terminal).
- `--no-input` - (Optional) Never prompt. A missing `name` defaults to `{repo}-private` (the choice is logged on stderr). A missing `upstream`, or a personal account without `--yes`, exits 2. See [Non-interactive use](#non-interactive-use).

**Examples:**
Expand Down Expand Up @@ -174,7 +174,7 @@ venfork setup my-org/foo my-foo-private --org my-org --no-public

Pure failures (for example name taken by a **different** repo) still abort setup. If recovery sync stops due to divergent default branches, fix or move those commits, then run **`venfork sync`** again from inside the private mirror.

### `venfork clone <vendor-repo> [--no-public] [--upstream <url>] [--no-input]`
### `venfork clone <vendor-repo> [--no-public] [--upstream <url>] [--public-fork <url>] [--no-input]`

Clone an existing vendor setup and automatically configure all remotes.

Expand All @@ -186,12 +186,13 @@ Clone an existing vendor setup and automatically configure all remotes.
- Falls back to auto-detection when the config branch is absent:
- Public fork (by stripping `-private` suffix)
- Upstream repository (from public fork's parent)
- Configures the appropriate remotes (origin/upstream — plus public in standard mode)
- Configures the appropriate remotes (origin/upstream, plus public in standard mode)
- Disables push to upstream (read-only)

**Options** (only meaningful for **legacy mirrors** without a `venfork-config` branch — when config is present it's authoritative and the flags will error on conflict):
**Options** (only meaningful for **legacy mirrors** without a `venfork-config` branch. When config is present it is authoritative: a flag that names the recorded value is accepted, and a flag that contradicts it is an error):
- `--no-public` - Declare a no-public layout (origin + upstream only). Use when the original setup was created with `venfork setup --no-public` but the config branch is missing.
- `--upstream <url>` - Provide the upstream URL explicitly. Useful with `--no-public` (no public fork to derive it from) or to skip the auto-detect prompt.
- `--public-fork <url>` - Provide the public fork (GitHub URL or `owner/repo`) explicitly. Skips the public-fork detection and prompt; upstream is still read from the fork's parent unless `--upstream` is given. Cannot be combined with `--no-public`.

`--no-input` never prompts: a URL that clone would ask for exits 2 and names the flag that supplies it. `clone` has no confirm, so it does not take `--yes`. A clone that fails or is cancelled after the repository was cloned removes the cloned directory, so you can rerun it with the named flag.

Expand Down Expand Up @@ -220,7 +221,8 @@ venfork clone git@github.com:acme-corp/awesome-project-private.git
**Interactive prompts:**
- If public fork cannot be auto-detected, you'll be prompted for the URL
- If upstream cannot be auto-detected (no parent), you'll be prompted for the URL
- Without a terminal (see [Non-interactive use](#non-interactive-use)) these prompts exit 2 instead: pass `--upstream <url>`, or `--no-public --upstream <url>` when the mirror has no public fork
- An empty answer is rejected and the prompt asks again; cancelling a prompt (Ctrl-C) exits 130
- Without a terminal (see [Non-interactive use](#non-interactive-use)) these prompts exit 2 instead and name the flag: `--public-fork <url>` for the public fork, `--upstream <url>` for upstream. Use `--no-public --upstream <url>` when the mirror has no public fork

### `venfork sync [branch] [--report-issues]`

Expand Down
3 changes: 3 additions & 0 deletions docs/agents/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ After `bun run check`, re-read a file before editing by string match.

## Subagents must not run gh stack
Stack metadata is shared in `.git` across worktrees. Only the main checkout drives a stack.

## execa `$` does not parse quotes
In a `$` template, literal text is split on spaces and quotes are kept: `--jq '.parent.url'` passes `'.parent.url'` with the quotes to gh. Interpolate a value (`--jq ${expr}`) or write an unquoted single word.
14 changes: 11 additions & 3 deletions src/clone-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,26 @@ export type ParsedCloneArgs = {
vendorRepoUrl?: string;
noPublic: boolean;
upstreamUrl?: string;
publicForkUrl?: string;
/** `--no-input`; clone has no confirm, so `yes` is always false. */
prompt: PromptContext;
};

const USAGE =
'venfork clone <vendor-repo> [--no-public] [--upstream <url>] [--no-input]';
'venfork clone <vendor-repo> [--no-public] [--upstream <url>] [--public-fork <url>] [--no-input]';

/**
* Parse `venfork clone ...` argv after the `clone` token.
*
* `--no-public` and `--upstream` are escape hatches for **legacy mirrors**
* `--no-public`, `--upstream` and `--public-fork` are escape hatches for **legacy mirrors**
* that pre-date the `venfork-config` branch. When the config branch is
* present, the layout is read from there and these flags are unnecessary
* (and will conflict with the recorded `mode` if set inconsistently).
* (clone refuses any that contradict the recorded layout).
*/
export function parseCloneCliArgs(args: string[]): ParsedCloneArgs {
let noPublic = false;
let upstreamUrl: string | undefined;
let publicForkUrl: string | undefined;
const promptFlags = parsePromptFlags({ allowYes: false });

const positional = scanArgs(args, USAGE, (a, i) => {
Expand All @@ -41,6 +43,11 @@ export function parseCloneCliArgs(args: string[]): ParsedCloneArgs {
upstreamUrl = value;
return consumed;
}
if (a === '--public-fork' || a.startsWith('--public-fork=')) {
const { value, consumed } = consumeValue('--public-fork', args, i);
publicForkUrl = value;
return consumed;
}
return undefined;
});
if (positional.length > 1) throw unexpectedArgument(positional[1], USAGE);
Expand All @@ -49,6 +56,7 @@ export function parseCloneCliArgs(args: string[]): ParsedCloneArgs {
vendorRepoUrl: positional[0],
noPublic,
upstreamUrl: upstreamUrl?.trim() || undefined,
publicForkUrl: publicForkUrl?.trim() || undefined,
prompt: promptFlags.prompt,
};
}
Loading
Loading