Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ npx docs-cache add framework/core framework/other-repo

# Sync
npx docs-cache sync
npx docs-cache sync core other-source
npx docs-cache sync --frozen

# Pin current ref to commit SHA
npx docs-cache pin core
npx docs-cache pin --all
npx docs-cache pin core --dry-run

# Update selected sources
npx docs-cache update core
npx docs-cache update --all --dry-run

# Verify Integrity
npx docs-cache verify
Expand All @@ -52,6 +63,29 @@ npx docs-cache clean

> for more options: `npx docs-cache --help`

## Recommended Workflow

Use this flow to keep behavior predictable (similar to package manager manifest + lock workflows):

1. Keep source intent in config (`ref: "main"`, `ref: "v1"`, or a commit SHA).
2. Run `npx docs-cache update <id...>` (or `--all`) to refresh selected sources and lock data.
3. Use `npx docs-cache sync --frozen` in CI to fail fast when lock data drifts.
4. Use `npx docs-cache pin <id...>` only when you explicitly want to rewrite config refs to commit SHAs.

## Command Patterns

- `sync`
- `npx docs-cache sync` sync all sources.
- `npx docs-cache sync <id...>` sync only selected sources.
- `npx docs-cache sync --frozen` fail if resolved commits differ from the lock file.
- `update`
- `npx docs-cache update <id...>` refresh selected sources and lock/materialized output.
- `npx docs-cache update --all` refresh all sources.
- `npx docs-cache update --dry-run` preview what would change without writing files.
- `pin`
- `npx docs-cache pin <id...>` rewrite selected source `ref` values to resolved commit SHAs.
- `npx docs-cache pin --all --dry-run` preview pinning changes without rewriting config.

## Configuration

`docs.config.json` at project root (or a `docs-cache` field in `package.json`):
Expand Down
2 changes: 2 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export { parseArgs } from "#cli/parse-args";
export { cleanCache } from "#commands/clean";
export { cleanGitCache } from "#commands/clean-git-cache";
export { initConfig } from "#commands/init";
export { pinSources } from "#commands/pin";
export { pruneCache } from "#commands/prune";
export { removeSources } from "#commands/remove";
export { printSyncPlan, runSync } from "#commands/sync";
export { updateSources } from "#commands/update";
export { verifyCache } from "#commands/verify";
export { loadConfig } from "#config";
export { redactRepoUrl } from "#git/redact";
Expand Down
110 changes: 110 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Usage: ${CLI_NAME} <command> [options]
Commands:
add Add sources to the config (supports github:org/repo#ref)
remove Remove sources from the config and targets
pin Pin source refs to current commits
update Refresh selected sources and lock data
sync Synchronize cache with config
status Show cache status
clean Remove project cache
Expand All @@ -25,6 +27,9 @@ Commands:
Global options:
--config <path>
--cache-dir <path>
--all
--dry-run
Comment thread
fbosch marked this conversation as resolved.
Outdated
--frozen
--offline
--fail-on-miss
--lock-only
Expand All @@ -39,6 +44,14 @@ Add options:
--target <dir>
--target-dir <path>
--id <id>

Pin options:
--all
--dry-run

Update options:
--all
--dry-run
`;

const printHelp = () => {
Expand Down Expand Up @@ -154,6 +167,91 @@ const runRemove = async (
}
};

const runPin = async (parsed: Extract<CliCommand, { command: "pin" }>) => {
const options = parsed.options;
if (options.offline) {
throw new Error("Pin does not support --offline.");
}
if (!options.all && parsed.ids.length === 0) {
throw new Error("Usage: docs-cache pin <id...> [--all]");
}
const { pinSources } = await import("#commands/pin");
const result = await pinSources({
configPath: options.config,
ids: parsed.ids,
all: options.all,
dryRun: options.dryRun,
timeoutMs: options.timeoutMs,
});
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
for (const entry of result.updated) {
ui.item(symbols.success, entry.id, `${entry.fromRef} -> ${entry.toRef}`);
}
for (const id of result.unchanged) {
ui.item(symbols.info, id, "already pinned");
}
if (result.missing.length > 0) {
ui.line(
`${symbols.warn} Missing ${result.missing.length} source${result.missing.length === 1 ? "" : "s"}: ${result.missing.join(", ")}`,
);
}
if (result.dryRun) {
ui.line(
`${symbols.info} Dry run: no changes written to ${pc.gray(path.relative(process.cwd(), result.configPath) || "docs.config.json")}`,
);
return;
}
ui.line(
`${symbols.info} Updated ${pc.gray(path.relative(process.cwd(), result.configPath) || "docs.config.json")}`,
);
};

const runUpdate = async (
parsed: Extract<CliCommand, { command: "update" }>,
) => {
const options = parsed.options;
if (options.offline) {
throw new Error("Update does not support --offline.");
}
if (!options.all && parsed.ids.length === 0) {
throw new Error("Usage: docs-cache update <id...> [--all]");
}
const { printSyncPlan } = await import("#commands/sync");
const { updateSources } = await import("#commands/update");
const result = await updateSources({
configPath: options.config,
cacheDirOverride: options.cacheDir,
ids: parsed.ids,
all: options.all,
dryRun: options.dryRun,
json: options.json,
lockOnly: options.lockOnly,
failOnMiss: options.failOnMiss,
timeoutMs: options.timeoutMs,
verbose: options.verbose,
concurrency: options.concurrency,
frozen: options.frozen,
});
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
printSyncPlan(result.plan);
if (result.missing.length > 0) {
ui.line(
`${symbols.warn} Missing ${result.missing.length} source${result.missing.length === 1 ? "" : "s"}: ${result.missing.join(", ")}`,
);
}
if (result.dryRun) {
ui.line(
`${symbols.info} Dry run: no changes written to ${pc.gray(path.relative(process.cwd(), result.plan.configPath) || "docs.config.json")}`,
);
}
};

const runStatus = async (
parsed: Extract<CliCommand, { command: "status" }>,
) => {
Expand Down Expand Up @@ -248,13 +346,16 @@ const runSyncCommand = async (
) => {
const options = parsed.options;
const { printSyncPlan, runSync } = await import("#commands/sync");
const sourceFilter = parsed.ids.length > 0 ? parsed.ids : undefined;
const plan = await runSync({
configPath: options.config,
cacheDirOverride: options.cacheDir,
json: options.json,
lockOnly: options.lockOnly,
offline: options.offline,
failOnMiss: options.failOnMiss,
frozen: options.frozen,
sourceFilter,
timeoutMs: options.timeoutMs,
verbose: options.verbose,
});
Expand Down Expand Up @@ -315,6 +416,12 @@ const runCommand = async (parsed: CliCommand) => {
case "remove":
await runRemove(parsed);
return;
case "pin":
await runPin(parsed);
return;
case "update":
await runUpdate(parsed);
return;
case "status":
await runStatus(parsed);
return;
Expand Down Expand Up @@ -368,6 +475,9 @@ export async function main(): Promise<void> {
if (
parsed.command !== "add" &&
parsed.command !== "remove" &&
parsed.command !== "pin" &&
parsed.command !== "update" &&
parsed.command !== "sync" &&
parsed.positionals.length > 0
) {
printError(`${CLI_NAME}: unexpected arguments.`);
Expand Down
42 changes: 40 additions & 2 deletions src/cli/parse-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type { AddEntry, CliCommand, CliOptions } from "./types";
const COMMANDS = [
"add",
"remove",
"pin",
"update",
"sync",
"status",
"clean",
Expand All @@ -32,6 +34,7 @@ const ADD_ONLY_OPTIONS = new Set([
"--target-dir",
"--id",
]);
const SCOPED_SOURCE_OPTIONS = new Set(["--all", "--dry-run"]);
const POSITIONAL_SKIP_OPTIONS = new Set([
"--config",
"--cache-dir",
Expand Down Expand Up @@ -203,9 +206,32 @@ const parsePositionals = (rawArgs: string[]) => {

const assertAddOnlyOptions = (command: Command | null, rawArgs: string[]) => {
if (command === "add") {
for (const arg of rawArgs) {
if (SCOPED_SOURCE_OPTIONS.has(arg)) {
throw new Error(`${arg} is only valid for pin or update.`);
}
}
Comment thread
fbosch marked this conversation as resolved.
return;
}
if (command === "pin" || command === "update") {
for (const arg of rawArgs) {
if (ADD_ONLY_OPTIONS.has(arg)) {
throw new Error(`${arg} is only valid for add.`);
}
if (!arg.startsWith("--")) {
continue;
}
const [flag] = arg.split("=");
if (ADD_ONLY_OPTIONS_WITH_VALUES.has(flag)) {
throw new Error(`${flag} is only valid for add.`);
}
}
return;
}
for (const arg of rawArgs) {
if (SCOPED_SOURCE_OPTIONS.has(arg)) {
throw new Error(`${arg} is only valid for pin or update.`);
}
Comment thread
fbosch marked this conversation as resolved.
if (ADD_ONLY_OPTIONS.has(arg)) {
throw new Error(`${arg} is only valid for add.`);
}
Expand All @@ -227,6 +253,9 @@ const buildOptions = (result: ReturnType<ReturnType<typeof cac>["parse"]>) => {
failOnMiss: Boolean(result.options.failOnMiss),
lockOnly: Boolean(result.options.lockOnly),
prune: Boolean(result.options.prune),
all: Boolean(result.options.all),
dryRun: Boolean(result.options.dryRun),
frozen: Boolean(result.options.frozen),
concurrency: result.options.concurrency
? Number(result.options.concurrency)
: undefined,
Expand Down Expand Up @@ -294,8 +323,12 @@ const buildParsedCommand = (
};
case "remove":
return { command: "remove", ids: positionals, options };
case "pin":
return { command: "pin", ids: positionals, options };
case "update":
return { command: "update", ids: positionals, options };
case "sync":
return { command: "sync", options };
return { command: "sync", ids: positionals, options };
case "status":
return { command: "status", options };
case "clean":
Expand All @@ -320,6 +353,9 @@ export const parseArgs = (argv = process.argv): ParsedArgs => {
cli
.option("--config <path>", "Path to config file")
.option("--cache-dir <path>", "Override cache directory")
.option("--all", "Apply command to all sources")
.option("--dry-run", "Preview changes without writing files")
.option("--frozen", "Fail if lock and resolved refs differ")
.option("--offline", "Disable network access")
.option("--fail-on-miss", "Fail when required sources are missing")
.option("--lock-only", "Update lock without materializing files")
Expand All @@ -339,7 +375,9 @@ export const parseArgs = (argv = process.argv): ParsedArgs => {
.option("--id <id>", "Source id");

cli.command("remove <id...>", "Remove sources from the config and targets");
cli.command("sync", "Synchronize cache with config");
cli.command("pin [id...]", "Pin source refs to current commit");
cli.command("update [id...]", "Refresh selected sources and lock data");
cli.command("sync [id...]", "Synchronize cache with config");
cli.command("status", "Show cache status");
cli.command("clean", "Remove project cache");
cli.command("clean-cache", "Clear global git cache");
Expand Down
7 changes: 6 additions & 1 deletion src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export type CliOptions = {
failOnMiss: boolean;
lockOnly: boolean;
prune: boolean;
all: boolean;
dryRun: boolean;
frozen: boolean;
concurrency?: number;
json: boolean;
timeoutMs?: number;
Expand All @@ -21,7 +24,9 @@ export type AddEntry = {
export type CliCommand =
| { command: "add"; entries: AddEntry[]; options: CliOptions }
| { command: "remove"; ids: string[]; options: CliOptions }
| { command: "sync"; options: CliOptions }
| { command: "pin"; ids: string[]; options: CliOptions }
| { command: "update"; ids: string[]; options: CliOptions }
| { command: "sync"; ids: string[]; options: CliOptions }
| { command: "status"; options: CliOptions }
| { command: "clean"; options: CliOptions }
| { command: "clean-cache"; options: CliOptions }
Expand Down
Loading
Loading