mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Add worktree reseed command
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
9f9a8cfa25
commit
46892ded18
3 changed files with 268 additions and 33 deletions
|
|
@ -9,6 +9,8 @@ import {
|
||||||
readSourceAttachmentBody,
|
readSourceAttachmentBody,
|
||||||
rebindWorkspaceCwd,
|
rebindWorkspaceCwd,
|
||||||
resolveSourceConfigPath,
|
resolveSourceConfigPath,
|
||||||
|
resolveWorktreeReseedSource,
|
||||||
|
resolveWorktreeReseedTargetPaths,
|
||||||
resolveGitWorktreeAddArgs,
|
resolveGitWorktreeAddArgs,
|
||||||
resolveWorktreeMakeTargetPath,
|
resolveWorktreeMakeTargetPath,
|
||||||
worktreeInitCommand,
|
worktreeInitCommand,
|
||||||
|
|
@ -482,27 +484,69 @@ describe("worktree helpers", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requires an explicit source for worktree reseed", async () => {
|
it("requires an explicit reseed source", () => {
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-source-"));
|
expect(() => resolveWorktreeReseedSource({})).toThrow(
|
||||||
const repoRoot = path.join(tempRoot, "repo");
|
"Pass --from <worktree> or --from-config/--from-instance explicitly so the reseed source is unambiguous.",
|
||||||
const originalCwd = process.cwd();
|
);
|
||||||
const originalPaperclipConfig = process.env.PAPERCLIP_CONFIG;
|
});
|
||||||
|
|
||||||
|
it("rejects mixed reseed source selectors", () => {
|
||||||
|
expect(() => resolveWorktreeReseedSource({
|
||||||
|
from: "current",
|
||||||
|
fromInstance: "default",
|
||||||
|
})).toThrow(
|
||||||
|
"Use either --from <worktree> or --from-config/--from-data-dir/--from-instance, not both.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives worktree reseed target paths from the adjacent env file", () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-"));
|
||||||
|
const worktreeRoot = path.join(tempRoot, "repo");
|
||||||
|
const configPath = path.join(worktreeRoot, ".paperclip", "config.json");
|
||||||
|
const envPath = path.join(worktreeRoot, ".paperclip", ".env");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(repoRoot, { recursive: true });
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||||
delete process.env.PAPERCLIP_CONFIG;
|
fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8");
|
||||||
process.chdir(repoRoot);
|
fs.writeFileSync(
|
||||||
|
envPath,
|
||||||
await expect(worktreeReseedCommand({ seed: false, yes: true })).rejects.toThrow(
|
[
|
||||||
"Reseed requires an explicit source.",
|
"PAPERCLIP_HOME=/tmp/paperclip-worktrees",
|
||||||
|
"PAPERCLIP_INSTANCE_ID=pap-1132-chat",
|
||||||
|
].join("\n"),
|
||||||
|
"utf8",
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
resolveWorktreeReseedTargetPaths({
|
||||||
|
configPath,
|
||||||
|
rootPath: worktreeRoot,
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
cwd: worktreeRoot,
|
||||||
|
homeDir: "/tmp/paperclip-worktrees",
|
||||||
|
instanceId: "pap-1132-chat",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects reseed targets without worktree env metadata", () => {
|
||||||
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-missing-"));
|
||||||
|
const worktreeRoot = path.join(tempRoot, "repo");
|
||||||
|
const configPath = path.join(worktreeRoot, ".paperclip", "config.json");
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(buildSourceConfig()), "utf8");
|
||||||
|
fs.writeFileSync(path.join(worktreeRoot, ".paperclip", ".env"), "", "utf8");
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
resolveWorktreeReseedTargetPaths({
|
||||||
|
configPath,
|
||||||
|
rootPath: worktreeRoot,
|
||||||
|
})).toThrow("does not look like a worktree-local Paperclip instance");
|
||||||
} finally {
|
} finally {
|
||||||
process.chdir(originalCwd);
|
|
||||||
if (originalPaperclipConfig === undefined) {
|
|
||||||
delete process.env.PAPERCLIP_CONFIG;
|
|
||||||
} else {
|
|
||||||
process.env.PAPERCLIP_CONFIG = originalPaperclipConfig;
|
|
||||||
}
|
|
||||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,17 @@ type WorktreeMergeHistoryOptions = {
|
||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WorktreeReseedOptions = {
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
fromConfig?: string;
|
||||||
|
fromDataDir?: string;
|
||||||
|
fromInstance?: string;
|
||||||
|
seedMode?: string;
|
||||||
|
yes?: boolean;
|
||||||
|
allowLiveTarget?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type EmbeddedPostgresInstance = {
|
type EmbeddedPostgresInstance = {
|
||||||
initialise(): Promise<void>;
|
initialise(): Promise<void>;
|
||||||
start(): Promise<void>;
|
start(): Promise<void>;
|
||||||
|
|
@ -738,6 +749,65 @@ export function resolveSourceConfigPath(opts: WorktreeInitOptions): string {
|
||||||
return path.resolve(sourceHome, "instances", sourceInstanceId, "config.json");
|
return path.resolve(sourceHome, "instances", sourceInstanceId, "config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveWorktreeReseedSource(input: WorktreeReseedOptions): ResolvedWorktreeReseedSource {
|
||||||
|
const fromSelector = nonEmpty(input.from);
|
||||||
|
const fromConfig = nonEmpty(input.fromConfig);
|
||||||
|
const fromDataDir = nonEmpty(input.fromDataDir);
|
||||||
|
const fromInstance = nonEmpty(input.fromInstance);
|
||||||
|
const hasExplicitConfigSource = Boolean(fromConfig || fromDataDir || fromInstance);
|
||||||
|
|
||||||
|
if (fromSelector && hasExplicitConfigSource) {
|
||||||
|
throw new Error(
|
||||||
|
"Use either --from <worktree> or --from-config/--from-data-dir/--from-instance, not both.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromSelector) {
|
||||||
|
const endpoint = resolveWorktreeEndpointFromSelector(fromSelector, { allowCurrent: true });
|
||||||
|
return {
|
||||||
|
configPath: endpoint.configPath,
|
||||||
|
label: endpoint.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasExplicitConfigSource) {
|
||||||
|
const configPath = resolveSourceConfigPath({
|
||||||
|
fromConfig: fromConfig ?? undefined,
|
||||||
|
fromDataDir: fromDataDir ?? undefined,
|
||||||
|
fromInstance: fromInstance ?? undefined,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
configPath,
|
||||||
|
label: configPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
"Pass --from <worktree> or --from-config/--from-instance explicitly so the reseed source is unambiguous.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorktreeReseedTargetPaths(input: {
|
||||||
|
configPath: string;
|
||||||
|
rootPath: string;
|
||||||
|
}): WorktreeLocalPaths {
|
||||||
|
const envEntries = readPaperclipEnvEntries(resolvePaperclipEnvFile(input.configPath));
|
||||||
|
const homeDir = nonEmpty(envEntries.PAPERCLIP_HOME);
|
||||||
|
const instanceId = nonEmpty(envEntries.PAPERCLIP_INSTANCE_ID);
|
||||||
|
|
||||||
|
if (!homeDir || !instanceId) {
|
||||||
|
throw new Error(
|
||||||
|
`Target config ${input.configPath} does not look like a worktree-local Paperclip instance. Expected PAPERCLIP_HOME and PAPERCLIP_INSTANCE_ID in the adjacent .env.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveWorktreeLocalPaths({
|
||||||
|
cwd: input.rootPath,
|
||||||
|
homeDir,
|
||||||
|
instanceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function resolveSourceConnectionString(config: PaperclipConfig, envEntries: Record<string, string>, portOverride?: number): string {
|
function resolveSourceConnectionString(config: PaperclipConfig, envEntries: Record<string, string>, portOverride?: number): string {
|
||||||
if (config.database.mode === "postgres") {
|
if (config.database.mode === "postgres") {
|
||||||
const connectionString = nonEmpty(envEntries.DATABASE_URL) ?? nonEmpty(config.database.connectionString);
|
const connectionString = nonEmpty(envEntries.DATABASE_URL) ?? nonEmpty(config.database.connectionString);
|
||||||
|
|
@ -1326,6 +1396,11 @@ type ResolvedWorktreeEndpoint = {
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ResolvedWorktreeReseedSource = {
|
||||||
|
configPath: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
function parseGitWorktreeList(cwd: string): GitWorktreeListEntry[] {
|
function parseGitWorktreeList(cwd: string): GitWorktreeListEntry[] {
|
||||||
const raw = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
const raw = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
||||||
cwd,
|
cwd,
|
||||||
|
|
@ -1819,6 +1894,13 @@ function renderMergePlan(plan: Awaited<ReturnType<typeof collectMergePlan>>["pla
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRunningEmbeddedPostgresPid(config: PaperclipConfig): number | null {
|
||||||
|
if (config.database.mode !== "embedded-postgres") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return readRunningPostmasterPid(path.resolve(config.database.embeddedPostgresDataDir, "postmaster.pid"));
|
||||||
|
}
|
||||||
|
|
||||||
async function collectMergePlan(input: {
|
async function collectMergePlan(input: {
|
||||||
sourceDb: ClosableDb;
|
sourceDb: ClosableDb;
|
||||||
targetDb: ClosableDb;
|
targetDb: ClosableDb;
|
||||||
|
|
@ -2760,6 +2842,89 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function worktreeReseedCommand(opts: WorktreeReseedOptions): Promise<void> {
|
||||||
|
printPaperclipCliBanner();
|
||||||
|
p.intro(pc.bgCyan(pc.black(" paperclipai worktree reseed ")));
|
||||||
|
|
||||||
|
const seedMode = opts.seedMode ?? "full";
|
||||||
|
if (!isWorktreeSeedMode(seedMode)) {
|
||||||
|
throw new Error(`Unsupported seed mode "${seedMode}". Expected one of: minimal, full.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetEndpoint = opts.to
|
||||||
|
? resolveWorktreeEndpointFromSelector(opts.to, { allowCurrent: true })
|
||||||
|
: resolveCurrentEndpoint();
|
||||||
|
const source = resolveWorktreeReseedSource(opts);
|
||||||
|
|
||||||
|
if (path.resolve(source.configPath) === path.resolve(targetEndpoint.configPath)) {
|
||||||
|
throw new Error("Source and target Paperclip configs are the same. Choose different --from/--to values.");
|
||||||
|
}
|
||||||
|
if (!existsSync(source.configPath)) {
|
||||||
|
throw new Error(`Source config not found at ${source.configPath}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetConfig = readConfig(targetEndpoint.configPath);
|
||||||
|
if (!targetConfig) {
|
||||||
|
throw new Error(`Target config not found at ${targetEndpoint.configPath}.`);
|
||||||
|
}
|
||||||
|
const sourceConfig = readConfig(source.configPath);
|
||||||
|
if (!sourceConfig) {
|
||||||
|
throw new Error(`Source config not found at ${source.configPath}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPaths = resolveWorktreeReseedTargetPaths({
|
||||||
|
configPath: targetEndpoint.configPath,
|
||||||
|
rootPath: targetEndpoint.rootPath,
|
||||||
|
});
|
||||||
|
const runningTargetPid = resolveRunningEmbeddedPostgresPid(targetConfig);
|
||||||
|
if (runningTargetPid && !opts.allowLiveTarget) {
|
||||||
|
throw new Error(
|
||||||
|
`Target worktree database appears to be running (pid ${runningTargetPid}). Stop Paperclip in ${targetEndpoint.rootPath} before reseeding, or re-run with --allow-live-target if you want to override this guard.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = opts.yes
|
||||||
|
? true
|
||||||
|
: await p.confirm({
|
||||||
|
message: `Overwrite the isolated Paperclip DB for ${targetEndpoint.label} from ${source.label} using ${seedMode} seed mode?`,
|
||||||
|
initialValue: false,
|
||||||
|
});
|
||||||
|
if (p.isCancel(confirmed) || !confirmed) {
|
||||||
|
p.log.warn("Reseed cancelled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runningTargetPid && opts.allowLiveTarget) {
|
||||||
|
p.log.warning(`Proceeding even though the target embedded PostgreSQL appears to be running (pid ${runningTargetPid}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const spinner = p.spinner();
|
||||||
|
spinner.start(`Reseeding ${targetEndpoint.label} from ${source.label} (${seedMode})...`);
|
||||||
|
try {
|
||||||
|
const seeded = await seedWorktreeDatabase({
|
||||||
|
sourceConfigPath: source.configPath,
|
||||||
|
sourceConfig,
|
||||||
|
targetConfig,
|
||||||
|
targetPaths,
|
||||||
|
instanceId: targetPaths.instanceId,
|
||||||
|
seedMode,
|
||||||
|
});
|
||||||
|
spinner.stop(`Reseeded ${targetEndpoint.label} (${seedMode}).`);
|
||||||
|
p.log.message(pc.dim(`Source: ${source.configPath}`));
|
||||||
|
p.log.message(pc.dim(`Target: ${targetEndpoint.configPath}`));
|
||||||
|
p.log.message(pc.dim(`Seed snapshot: ${seeded.backupSummary}`));
|
||||||
|
for (const rebound of seeded.reboundWorkspaces) {
|
||||||
|
p.log.message(
|
||||||
|
pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
p.outro(pc.green(`Reseed complete for ${targetEndpoint.label}.`));
|
||||||
|
} catch (error) {
|
||||||
|
spinner.stop(pc.red("Failed to reseed worktree database."));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function registerWorktreeCommands(program: Command): void {
|
export function registerWorktreeCommands(program: Command): void {
|
||||||
const worktree = program.command("worktree").description("Worktree-local Paperclip instance helpers");
|
const worktree = program.command("worktree").description("Worktree-local Paperclip instance helpers");
|
||||||
|
|
||||||
|
|
@ -2833,6 +2998,19 @@ export function registerWorktreeCommands(program: Command): void {
|
||||||
.option("--yes", "Skip the interactive confirmation prompt when applying", false)
|
.option("--yes", "Skip the interactive confirmation prompt when applying", false)
|
||||||
.action(worktreeMergeHistoryCommand);
|
.action(worktreeMergeHistoryCommand);
|
||||||
|
|
||||||
|
worktree
|
||||||
|
.command("reseed")
|
||||||
|
.description("Re-seed an existing worktree-local instance from another Paperclip instance or worktree")
|
||||||
|
.option("--from <worktree>", "Source worktree path, directory name, branch name, or current")
|
||||||
|
.option("--to <worktree>", "Target worktree path, directory name, branch name, or current (defaults to current)")
|
||||||
|
.option("--from-config <path>", "Source config.json to seed from")
|
||||||
|
.option("--from-data-dir <path>", "Source PAPERCLIP_HOME used when deriving the source config")
|
||||||
|
.option("--from-instance <id>", "Source instance id when deriving the source config")
|
||||||
|
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: full)", "full")
|
||||||
|
.option("--yes", "Skip the destructive confirmation prompt", false)
|
||||||
|
.option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false)
|
||||||
|
.action(worktreeReseedCommand);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("worktree:cleanup")
|
.command("worktree:cleanup")
|
||||||
.description("Safely remove a worktree, its branch, and its isolated instance data")
|
.description("Safely remove a worktree, its branch, and its isolated instance data")
|
||||||
|
|
|
||||||
|
|
@ -232,14 +232,38 @@ pnpm paperclipai worktree init --force --seed-mode minimal \
|
||||||
|
|
||||||
That rewrites the worktree-local `.paperclip/config.json` + `.paperclip/.env`, recreates the isolated instance under `~/.paperclip-worktrees/instances/<worktree-id>/`, and preserves the git worktree contents themselves.
|
That rewrites the worktree-local `.paperclip/config.json` + `.paperclip/.env`, recreates the isolated instance under `~/.paperclip-worktrees/instances/<worktree-id>/`, and preserves the git worktree contents themselves.
|
||||||
|
|
||||||
For existing worktrees, prefer the dedicated reseed command instead of rebuilding the `worktree init --force` flags manually:
|
For an already-created worktree where you want to keep the existing repo-local config/env and only overwrite the isolated database, use `worktree reseed` instead. Stop the target worktree's Paperclip server first so the command can replace the DB safely.
|
||||||
|
|
||||||
|
**`pnpm paperclipai worktree reseed [options]`** — Re-seed an existing worktree-local instance from another Paperclip instance or worktree while preserving the target worktree's current config, ports, and instance identity.
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|---|---|
|
||||||
|
| `--from <worktree>` | Source worktree path, directory name, branch name, or `current` |
|
||||||
|
| `--to <worktree>` | Target worktree path, directory name, branch name, or `current` (defaults to `current`) |
|
||||||
|
| `--from-config <path>` | Source config.json to seed from |
|
||||||
|
| `--from-data-dir <path>` | Source `PAPERCLIP_HOME` used when deriving the source config |
|
||||||
|
| `--from-instance <id>` | Source instance id when deriving the source config |
|
||||||
|
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `full`) |
|
||||||
|
| `--yes` | Skip the destructive confirmation prompt |
|
||||||
|
| `--allow-live-target` | Override the guard that requires the target worktree DB to be stopped first |
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd /path/to/existing/worktree
|
# From the main repo, reseed a worktree from the current default/master instance.
|
||||||
pnpm paperclipai worktree reseed --from-config /path/to/source/.paperclip/config.json --seed-mode full
|
cd /path/to/paperclip
|
||||||
```
|
pnpm paperclipai worktree reseed \
|
||||||
|
--from current \
|
||||||
|
--to PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat \
|
||||||
|
--seed-mode full \
|
||||||
|
--yes
|
||||||
|
|
||||||
`worktree reseed` preserves the current worktree's instance id, ports, and branding while replacing only that worktree's isolated Paperclip instance data from the chosen source.
|
# From inside a worktree, reseed it from the default instance config.
|
||||||
|
cd /path/to/paperclip/.paperclip/worktrees/PAP-1132-assistant-ui-pap-1131-make-issues-comments-be-like-a-chat
|
||||||
|
pnpm paperclipai worktree reseed \
|
||||||
|
--from-instance default \
|
||||||
|
--seed-mode full
|
||||||
|
```
|
||||||
|
|
||||||
**`pnpm paperclipai worktree:make <name> [options]`** — Create `~/NAME` as a git worktree, then initialize an isolated Paperclip instance inside it. This combines `git worktree add` with `worktree init` in a single step.
|
**`pnpm paperclipai worktree:make <name> [options]`** — Create `~/NAME` as a git worktree, then initialize an isolated Paperclip instance inside it. This combines `git worktree add` with `worktree init` in a single step.
|
||||||
|
|
||||||
|
|
@ -267,17 +291,6 @@ pnpm paperclipai worktree:make experiment --no-seed
|
||||||
|
|
||||||
**`pnpm paperclipai worktree env [options]`** — Print shell exports for the current worktree-local Paperclip instance.
|
**`pnpm paperclipai worktree env [options]`** — Print shell exports for the current worktree-local Paperclip instance.
|
||||||
|
|
||||||
**`pnpm paperclipai worktree reseed [options]`** — Replace the current worktree instance with a fresh seed from another Paperclip source while preserving the current worktree's ports and instance id.
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
|---|---|
|
|
||||||
| `--from-config <path>` | Source config.json to seed from |
|
|
||||||
| `--from-data-dir <path>` | Source `PAPERCLIP_HOME` used when deriving the source config |
|
|
||||||
| `--from-instance <id>` | Source instance id when deriving the source config |
|
|
||||||
| `--home <path>` | Home root for worktree instances (default: `~/.paperclip-worktrees`) |
|
|
||||||
| `--seed-mode <mode>` | Seed profile: `minimal` or `full` (default: `minimal`) |
|
|
||||||
| `--yes` | Skip the destructive confirmation prompt |
|
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `-c, --config <path>` | Path to config file |
|
| `-c, --config <path>` | Path to config file |
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue