docs-cache 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/cli.mjs +13 -13
  2. package/dist/esm/api.d.ts +14 -0
  3. package/dist/esm/api.mjs +14 -0
  4. package/dist/esm/cache/cache-layout.d.ts +1 -0
  5. package/dist/esm/cache/cache-layout.mjs +12 -0
  6. package/dist/esm/cache/lock.d.ts +21 -0
  7. package/dist/esm/cache/lock.mjs +91 -0
  8. package/dist/esm/cache/manifest.d.ts +11 -0
  9. package/dist/esm/cache/manifest.mjs +68 -0
  10. package/dist/esm/cache/materialize.d.ts +26 -0
  11. package/dist/esm/cache/materialize.mjs +442 -0
  12. package/dist/esm/cache/targets.d.ts +19 -0
  13. package/dist/esm/cache/targets.mjs +66 -0
  14. package/dist/esm/cache/toc.d.ts +12 -0
  15. package/dist/esm/cache/toc.mjs +167 -0
  16. package/dist/esm/cli/exit-code.d.ts +11 -0
  17. package/dist/esm/cli/exit-code.mjs +5 -0
  18. package/dist/esm/cli/index.d.ts +5 -0
  19. package/dist/esm/cli/index.mjs +345 -0
  20. package/dist/esm/cli/live-output.d.ts +12 -0
  21. package/dist/esm/cli/live-output.mjs +30 -0
  22. package/dist/esm/cli/parse-args.d.ts +13 -0
  23. package/dist/esm/cli/parse-args.mjs +295 -0
  24. package/dist/esm/cli/run.d.ts +1 -0
  25. package/dist/esm/cli/run.mjs +2 -0
  26. package/dist/esm/cli/task-reporter.d.ts +32 -0
  27. package/dist/esm/cli/task-reporter.mjs +122 -0
  28. package/dist/esm/cli/types.d.ts +51 -0
  29. package/dist/esm/cli/types.mjs +0 -0
  30. package/dist/esm/cli/ui.d.ts +21 -0
  31. package/dist/esm/cli/ui.mjs +64 -0
  32. package/dist/esm/commands/add.d.ts +20 -0
  33. package/dist/esm/commands/add.mjs +81 -0
  34. package/dist/esm/commands/clean-git-cache.d.ts +10 -0
  35. package/dist/esm/commands/clean-git-cache.mjs +48 -0
  36. package/dist/esm/commands/clean.d.ts +10 -0
  37. package/dist/esm/commands/clean.mjs +27 -0
  38. package/dist/esm/commands/init.d.ts +19 -0
  39. package/dist/esm/commands/init.mjs +179 -0
  40. package/dist/esm/commands/prune.d.ts +11 -0
  41. package/dist/esm/commands/prune.mjs +52 -0
  42. package/dist/esm/commands/remove.d.ts +12 -0
  43. package/dist/esm/commands/remove.mjs +87 -0
  44. package/dist/esm/commands/status.d.ts +16 -0
  45. package/dist/esm/commands/status.mjs +78 -0
  46. package/dist/esm/commands/sync.d.ts +33 -0
  47. package/dist/esm/commands/sync.mjs +730 -0
  48. package/dist/esm/commands/verify.d.ts +11 -0
  49. package/dist/esm/commands/verify.mjs +120 -0
  50. package/dist/esm/config/index.d.ts +15 -0
  51. package/dist/esm/config/index.mjs +196 -0
  52. package/dist/esm/config/io.d.ts +30 -0
  53. package/dist/esm/config/io.mjs +112 -0
  54. package/dist/esm/config/schema.d.ts +171 -0
  55. package/dist/esm/config/schema.mjs +69 -0
  56. package/dist/esm/errors.d.ts +3 -0
  57. package/dist/esm/errors.mjs +2 -0
  58. package/dist/esm/git/cache-dir.d.ts +16 -0
  59. package/dist/esm/git/cache-dir.mjs +23 -0
  60. package/dist/esm/git/fetch-source.d.ts +19 -0
  61. package/dist/esm/git/fetch-source.mjs +477 -0
  62. package/dist/esm/git/redact.d.ts +1 -0
  63. package/dist/esm/git/redact.mjs +4 -0
  64. package/dist/esm/git/resolve-remote.d.ts +15 -0
  65. package/dist/esm/git/resolve-remote.mjs +87 -0
  66. package/dist/esm/git/resolve-repo.d.ts +5 -0
  67. package/dist/esm/git/resolve-repo.mjs +52 -0
  68. package/dist/esm/gitignore.d.ts +18 -0
  69. package/dist/esm/gitignore.mjs +80 -0
  70. package/dist/esm/paths.d.ts +8 -0
  71. package/dist/esm/paths.mjs +34 -0
  72. package/dist/esm/source-id.d.ts +1 -0
  73. package/dist/esm/source-id.mjs +29 -0
  74. package/dist/esm/types/sync.d.ts +25 -0
  75. package/dist/esm/types/sync.mjs +0 -0
  76. package/package.json +138 -91
  77. package/dist/chunks/add.mjs +0 -3
  78. package/dist/chunks/clean-git-cache.mjs +0 -2
  79. package/dist/chunks/clean.mjs +0 -2
  80. package/dist/chunks/init.mjs +0 -3
  81. package/dist/chunks/prune.mjs +0 -2
  82. package/dist/chunks/remove.mjs +0 -3
  83. package/dist/chunks/status.mjs +0 -2
  84. package/dist/chunks/sync.mjs +0 -9
  85. package/dist/chunks/verify.mjs +0 -2
  86. package/dist/shared/docs-cache.BOr9BnyP.mjs +0 -5
  87. package/dist/shared/docs-cache.BSvQNKuf.mjs +0 -2
  88. package/dist/shared/docs-cache.CQiaFDb_.mjs +0 -7
  89. package/dist/shared/docs-cache.CaOcl4OS.mjs +0 -3
  90. package/dist/shared/docs-cache.kK1DPQIQ.mjs +0 -2
@@ -0,0 +1,295 @@
1
+ import process from "node:process";
2
+ import cac from "cac";
3
+ import { ExitCode } from "#cli/exit-code";
4
+ const COMMANDS = [
5
+ "add",
6
+ "remove",
7
+ "sync",
8
+ "status",
9
+ "clean",
10
+ "clean-cache",
11
+ "prune",
12
+ "verify",
13
+ "init"
14
+ ];
15
+ const ADD_ONLY_OPTIONS = /* @__PURE__ */ new Set([
16
+ "--source",
17
+ "--target",
18
+ "--target-dir",
19
+ "--id"
20
+ ]);
21
+ const POSITIONAL_SKIP_OPTIONS = /* @__PURE__ */ new Set([
22
+ "--config",
23
+ "--cache-dir",
24
+ "--concurrency",
25
+ "--timeout-ms"
26
+ ]);
27
+ const ADD_ONLY_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set([
28
+ "--id",
29
+ "--source",
30
+ "--target",
31
+ "--target-dir"
32
+ ]);
33
+ const ADD_ENTRY_SKIP_OPTIONS = /* @__PURE__ */ new Set([
34
+ "--config",
35
+ "--cache-dir",
36
+ "--concurrency",
37
+ "--timeout-ms"
38
+ ]);
39
+ const VALUE_FLAGS = /* @__PURE__ */ new Set([
40
+ ...POSITIONAL_SKIP_OPTIONS,
41
+ ...ADD_ONLY_OPTIONS_WITH_VALUES
42
+ ]);
43
+ const getArgValue = (arg, next, flag) => {
44
+ const rawValue = arg === flag ? next : arg.slice(flag.length + 1);
45
+ if (!rawValue || rawValue.startsWith("-")) {
46
+ throw new Error(`${flag} expects a value.`);
47
+ }
48
+ return rawValue;
49
+ };
50
+ const addEntry = (state, repo) => {
51
+ state.entries.push({
52
+ repo,
53
+ ...state.pendingId ? { id: state.pendingId } : {}
54
+ });
55
+ state.lastIndex = state.entries.length - 1;
56
+ state.pendingId = null;
57
+ state.lastWasRepoAdded = true;
58
+ };
59
+ const applyPendingId = (state, value) => {
60
+ const canApply = state.lastWasRepoAdded && state.lastIndex !== -1 && state.entries[state.lastIndex]?.id === void 0 && state.pendingId === null;
61
+ if (!canApply) {
62
+ if (state.pendingId !== null) {
63
+ throw new Error("--id must be followed by a source.");
64
+ }
65
+ state.pendingId = value;
66
+ state.lastWasRepoAdded = false;
67
+ return;
68
+ }
69
+ state.entries[state.lastIndex].id = value;
70
+ state.lastWasRepoAdded = false;
71
+ };
72
+ const setTarget = (state, targetDir) => {
73
+ if (state.lastIndex === -1) {
74
+ throw new Error("--target must follow a --source entry.");
75
+ }
76
+ state.entries[state.lastIndex].targetDir = targetDir;
77
+ state.lastWasRepoAdded = false;
78
+ };
79
+ const findCommandIndex = (rawArgs) => {
80
+ for (let index = 0; index < rawArgs.length; index += 1) {
81
+ const arg = rawArgs[index];
82
+ if (arg.startsWith("--")) {
83
+ const [flag] = arg.split("=");
84
+ if (VALUE_FLAGS.has(flag) && !arg.includes("=")) {
85
+ index += 1;
86
+ }
87
+ continue;
88
+ }
89
+ return index;
90
+ }
91
+ return -1;
92
+ };
93
+ const parseAddEntries = (rawArgs) => {
94
+ const commandIndex = findCommandIndex(rawArgs);
95
+ const tail = commandIndex === -1 ? [] : rawArgs.slice(commandIndex + 1);
96
+ const state = {
97
+ entries: [],
98
+ lastIndex: -1,
99
+ pendingId: null,
100
+ lastWasRepoAdded: false
101
+ };
102
+ for (let index = 0; index < tail.length; index += 1) {
103
+ const arg = tail[index];
104
+ if (arg === "--id" || arg.startsWith("--id=")) {
105
+ const value = getArgValue(arg, tail[index + 1], "--id");
106
+ if (arg === "--id") {
107
+ index += 1;
108
+ }
109
+ applyPendingId(state, value);
110
+ continue;
111
+ }
112
+ if (arg === "--source" || arg.startsWith("--source=")) {
113
+ const value = getArgValue(arg, tail[index + 1], "--source");
114
+ addEntry(state, value);
115
+ if (arg === "--source") {
116
+ index += 1;
117
+ }
118
+ continue;
119
+ }
120
+ if (arg === "--target" || arg.startsWith("--target=")) {
121
+ const value = getArgValue(arg, tail[index + 1], "--target");
122
+ setTarget(state, value);
123
+ if (arg === "--target") {
124
+ index += 1;
125
+ }
126
+ continue;
127
+ }
128
+ if (arg === "--target-dir" || arg.startsWith("--target-dir=")) {
129
+ const value = getArgValue(arg, tail[index + 1], "--target-dir");
130
+ setTarget(state, value);
131
+ if (arg === "--target-dir") {
132
+ index += 1;
133
+ }
134
+ continue;
135
+ }
136
+ if (ADD_ENTRY_SKIP_OPTIONS.has(arg)) {
137
+ index += 1;
138
+ state.lastWasRepoAdded = false;
139
+ continue;
140
+ }
141
+ if (arg.startsWith("--")) {
142
+ state.lastWasRepoAdded = false;
143
+ continue;
144
+ }
145
+ addEntry(state, arg);
146
+ }
147
+ if (state.pendingId !== null) {
148
+ throw new Error("--id must be followed by a source.");
149
+ }
150
+ return state.entries;
151
+ };
152
+ const parsePositionals = (rawArgs) => {
153
+ const commandIndex = findCommandIndex(rawArgs);
154
+ const tail = commandIndex === -1 ? [] : rawArgs.slice(commandIndex + 1);
155
+ const positionals = [];
156
+ for (let index = 0; index < tail.length; index += 1) {
157
+ const arg = tail[index];
158
+ if (POSITIONAL_SKIP_OPTIONS.has(arg)) {
159
+ index += 1;
160
+ continue;
161
+ }
162
+ if (arg.startsWith("--")) {
163
+ continue;
164
+ }
165
+ positionals.push(arg);
166
+ }
167
+ return positionals;
168
+ };
169
+ const assertAddOnlyOptions = (command, rawArgs) => {
170
+ if (command === "add") {
171
+ return;
172
+ }
173
+ for (const arg of rawArgs) {
174
+ if (ADD_ONLY_OPTIONS.has(arg)) {
175
+ throw new Error(`${arg} is only valid for add.`);
176
+ }
177
+ if (!arg.startsWith("--")) {
178
+ continue;
179
+ }
180
+ const [flag] = arg.split("=");
181
+ if (ADD_ONLY_OPTIONS_WITH_VALUES.has(flag)) {
182
+ throw new Error(`${flag} is only valid for add.`);
183
+ }
184
+ }
185
+ };
186
+ const buildOptions = (result) => {
187
+ const options = {
188
+ config: result.options.config,
189
+ cacheDir: result.options.cacheDir,
190
+ offline: Boolean(result.options.offline),
191
+ failOnMiss: Boolean(result.options.failOnMiss),
192
+ lockOnly: Boolean(result.options.lockOnly),
193
+ prune: Boolean(result.options.prune),
194
+ concurrency: result.options.concurrency ? Number(result.options.concurrency) : void 0,
195
+ json: Boolean(result.options.json),
196
+ timeoutMs: result.options.timeoutMs ? Number(result.options.timeoutMs) : void 0,
197
+ silent: Boolean(result.options.silent),
198
+ verbose: Boolean(result.options.verbose)
199
+ };
200
+ if (options.concurrency !== void 0 && options.concurrency < 1) {
201
+ throw new Error("--concurrency must be a positive number.");
202
+ }
203
+ if (options.concurrency !== void 0 && !Number.isFinite(options.concurrency)) {
204
+ throw new Error("--concurrency must be a positive number.");
205
+ }
206
+ if (options.timeoutMs !== void 0 && options.timeoutMs < 1) {
207
+ throw new Error("--timeout-ms must be a positive number.");
208
+ }
209
+ if (options.timeoutMs !== void 0 && !Number.isFinite(options.timeoutMs)) {
210
+ throw new Error("--timeout-ms must be a positive number.");
211
+ }
212
+ return options;
213
+ };
214
+ const getCommandFromArgs = (rawArgs) => {
215
+ const commandIndex = findCommandIndex(rawArgs);
216
+ const command = commandIndex === -1 ? void 0 : rawArgs[commandIndex];
217
+ if (command && !COMMANDS.includes(command)) {
218
+ throw new Error(`Unknown command '${command}'.`);
219
+ }
220
+ return command ?? null;
221
+ };
222
+ const getPositionals = (command, rawArgs, entries) => {
223
+ if (command === "add") {
224
+ const addEntries = entries ?? parseAddEntries(rawArgs);
225
+ return { positionals: addEntries.map((entry) => entry.repo), addEntries };
226
+ }
227
+ return { positionals: parsePositionals(rawArgs), addEntries: entries };
228
+ };
229
+ const buildParsedCommand = (command, options, positionals, addEntries) => {
230
+ switch (command) {
231
+ case "add":
232
+ return {
233
+ command: "add",
234
+ entries: addEntries ?? [],
235
+ options
236
+ };
237
+ case "remove":
238
+ return { command: "remove", ids: positionals, options };
239
+ case "sync":
240
+ return { command: "sync", options };
241
+ case "status":
242
+ return { command: "status", options };
243
+ case "clean":
244
+ return { command: "clean", options };
245
+ case "clean-cache":
246
+ return { command: "clean-cache", options };
247
+ case "prune":
248
+ return { command: "prune", options };
249
+ case "verify":
250
+ return { command: "verify", options };
251
+ case "init":
252
+ return { command: "init", options };
253
+ default:
254
+ return { command: null, options };
255
+ }
256
+ };
257
+ export const parseArgs = (argv = process.argv) => {
258
+ try {
259
+ const cli = cac("docs-cache");
260
+ cli.option("--config <path>", "Path to config file").option("--cache-dir <path>", "Override cache directory").option("--offline", "Disable network access").option("--fail-on-miss", "Fail when required sources are missing").option("--lock-only", "Update lock without materializing files").option("--prune", "Prune cache on remove").option("--concurrency <n>", "Concurrency limit").option("--json", "Output JSON").option("--timeout-ms <n>", "Network timeout in milliseconds").option("--silent", "Suppress non-error output").option("--verbose", "Enable verbose logging").help();
261
+ cli.command("add [repo...]", "Add sources to the config").option("--source <repo>", "Source repo").option("--target <dir>", "Target directory for source").option("--target-dir <path>", "Target directory for source").option("--id <id>", "Source id");
262
+ cli.command("remove <id...>", "Remove sources from the config and targets");
263
+ cli.command("sync", "Synchronize cache with config");
264
+ cli.command("status", "Show cache status");
265
+ cli.command("clean", "Remove project cache");
266
+ cli.command("clean-cache", "Clear global git cache");
267
+ cli.command("prune", "Remove unused data");
268
+ cli.command("verify", "Validate cache integrity");
269
+ cli.command("init", "Create a new config interactively");
270
+ const result = cli.parse(argv, { run: false });
271
+ const rawArgs = argv.slice(2);
272
+ const command = getCommandFromArgs(rawArgs);
273
+ const options = buildOptions(result);
274
+ assertAddOnlyOptions(command, rawArgs);
275
+ const { positionals, addEntries } = getPositionals(command, rawArgs, null);
276
+ const parsed = buildParsedCommand(
277
+ command,
278
+ options,
279
+ positionals,
280
+ addEntries
281
+ );
282
+ return {
283
+ command,
284
+ options,
285
+ positionals,
286
+ rawArgs,
287
+ help: Boolean(result.options.help),
288
+ parsed
289
+ };
290
+ } catch (error) {
291
+ const message = error instanceof Error ? error.message : String(error);
292
+ console.error(message);
293
+ process.exit(ExitCode.InvalidArgument);
294
+ }
295
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { main } from "#cli/index";
2
+ main();
@@ -0,0 +1,32 @@
1
+ import { type LiveOutput } from "#cli/live-output";
2
+ export type TaskReporterOptions = {
3
+ maxLiveLines?: number;
4
+ output?: LiveOutput;
5
+ };
6
+ export declare class TaskReporter {
7
+ private readonly output;
8
+ private readonly maxLiveLines;
9
+ private readonly startTime;
10
+ private readonly tasks;
11
+ private readonly results;
12
+ private readonly liveLines;
13
+ private readonly hasTty;
14
+ private timer;
15
+ private warnings;
16
+ private errors;
17
+ constructor(options?: TaskReporterOptions);
18
+ start(label: string): void;
19
+ info(label: string, details?: string): void;
20
+ warn(label: string, details?: string): void;
21
+ error(label: string, details?: string): void;
22
+ success(label: string, details?: string, icon?: string): void;
23
+ debug(text: string): void;
24
+ finish(summary?: string): void;
25
+ stop(): void;
26
+ private render;
27
+ private startTimer;
28
+ private stopTimer;
29
+ private composeView;
30
+ private hasRunningTasks;
31
+ private formatLine;
32
+ }
@@ -0,0 +1,122 @@
1
+ import pc from "picocolors";
2
+ import { createLiveOutput } from "#cli/live-output";
3
+ import { symbols } from "#cli/ui";
4
+ const formatDuration = (ms) => {
5
+ const seconds = Math.max(0, ms / 1e3);
6
+ if (seconds < 60) {
7
+ return `${seconds.toFixed(1)}s`;
8
+ }
9
+ const minutes = Math.floor(seconds / 60);
10
+ const remainder = seconds % 60;
11
+ return `${minutes}m ${remainder.toFixed(1)}s`;
12
+ };
13
+ export class TaskReporter {
14
+ output;
15
+ maxLiveLines;
16
+ startTime = Date.now();
17
+ tasks = /* @__PURE__ */ new Map();
18
+ results = [];
19
+ liveLines = [];
20
+ hasTty = Boolean(process.stdout.isTTY);
21
+ timer = null;
22
+ warnings = 0;
23
+ errors = 0;
24
+ constructor(options = {}) {
25
+ this.output = options.output ?? createLiveOutput();
26
+ this.maxLiveLines = options.maxLiveLines ?? 4;
27
+ this.startTimer();
28
+ }
29
+ start(label) {
30
+ this.tasks.set(label, "running");
31
+ this.render();
32
+ }
33
+ info(label, details) {
34
+ this.results.push(this.formatLine(symbols.info, label, details));
35
+ this.render();
36
+ }
37
+ warn(label, details) {
38
+ this.warnings += 1;
39
+ this.results.push(this.formatLine(symbols.warn, label, details));
40
+ this.render();
41
+ }
42
+ error(label, details) {
43
+ this.errors += 1;
44
+ this.results.push(this.formatLine(symbols.error, label, details));
45
+ this.render();
46
+ }
47
+ success(label, details, icon = symbols.success) {
48
+ this.tasks.set(label, "success");
49
+ this.results.push(this.formatLine(icon, label, details));
50
+ this.liveLines.length = 0;
51
+ this.render();
52
+ }
53
+ debug(text) {
54
+ this.liveLines.push(pc.dim(text));
55
+ if (this.liveLines.length > this.maxLiveLines) {
56
+ this.liveLines.splice(0, this.liveLines.length - this.maxLiveLines);
57
+ }
58
+ this.render();
59
+ }
60
+ finish(summary) {
61
+ this.liveLines.length = 0;
62
+ const durationMs = Date.now() - this.startTime;
63
+ const parts = [
64
+ `Completed in ${formatDuration(durationMs)}`,
65
+ this.warnings ? `${this.warnings} warning${this.warnings === 1 ? "" : "s"}` : null,
66
+ this.errors ? `${this.errors} error${this.errors === 1 ? "" : "s"}` : null
67
+ ].filter(Boolean);
68
+ const suffix = parts.length ? ` \xB7 ${parts.join(" \xB7 ")}` : "";
69
+ const message = summary ? `${summary}${suffix}` : `${symbols.info}${suffix}`;
70
+ this.output.persist(this.composeView([message]));
71
+ this.stopTimer();
72
+ }
73
+ stop() {
74
+ this.output.stop();
75
+ this.stopTimer();
76
+ }
77
+ render() {
78
+ if (!this.hasTty) return;
79
+ this.output.render(this.composeView());
80
+ }
81
+ startTimer() {
82
+ if (!this.hasTty) return;
83
+ this.timer = setInterval(() => {
84
+ if (this.hasRunningTasks()) {
85
+ this.render();
86
+ }
87
+ }, 100);
88
+ this.timer.unref?.();
89
+ }
90
+ stopTimer() {
91
+ if (!this.timer) return;
92
+ clearInterval(this.timer);
93
+ this.timer = null;
94
+ }
95
+ composeView(extraFooter) {
96
+ const running = Array.from(this.tasks.entries()).filter(([, state]) => state === "running").map(([label]) => `${pc.cyan("\u2192")} ${label}`);
97
+ const runningCount = running.length;
98
+ const completedCount = this.results.length;
99
+ const elapsed = this.hasRunningTasks() ? pc.dim(
100
+ `elapsed: ${formatDuration(Date.now() - this.startTime)} \xB7 ${runningCount} running \xB7 ${completedCount} completed`
101
+ ) : "";
102
+ const lines = [
103
+ ...this.results,
104
+ ...running,
105
+ ...this.liveLines,
106
+ elapsed,
107
+ ...extraFooter ?? []
108
+ ].filter((line) => line.length > 0);
109
+ return lines.length > 0 ? lines : [" "];
110
+ }
111
+ hasRunningTasks() {
112
+ for (const state of this.tasks.values()) {
113
+ if (state === "running") return true;
114
+ }
115
+ return false;
116
+ }
117
+ formatLine(icon, label, details) {
118
+ const partLabel = pc.bold(label);
119
+ const partDetails = details ? pc.gray(details) : "";
120
+ return ` ${icon} ${partLabel} ${partDetails}`.trimEnd();
121
+ }
122
+ }
@@ -0,0 +1,51 @@
1
+ export type CliOptions = {
2
+ config?: string;
3
+ cacheDir?: string;
4
+ offline: boolean;
5
+ failOnMiss: boolean;
6
+ lockOnly: boolean;
7
+ prune: boolean;
8
+ concurrency?: number;
9
+ json: boolean;
10
+ timeoutMs?: number;
11
+ silent: boolean;
12
+ verbose: boolean;
13
+ };
14
+ export type AddEntry = {
15
+ id?: string;
16
+ repo: string;
17
+ targetDir?: string;
18
+ };
19
+ export type CliCommand = {
20
+ command: "add";
21
+ entries: AddEntry[];
22
+ options: CliOptions;
23
+ } | {
24
+ command: "remove";
25
+ ids: string[];
26
+ options: CliOptions;
27
+ } | {
28
+ command: "sync";
29
+ options: CliOptions;
30
+ } | {
31
+ command: "status";
32
+ options: CliOptions;
33
+ } | {
34
+ command: "clean";
35
+ options: CliOptions;
36
+ } | {
37
+ command: "clean-cache";
38
+ options: CliOptions;
39
+ } | {
40
+ command: "prune";
41
+ options: CliOptions;
42
+ } | {
43
+ command: "verify";
44
+ options: CliOptions;
45
+ } | {
46
+ command: "init";
47
+ options: CliOptions;
48
+ } | {
49
+ command: null;
50
+ options: CliOptions;
51
+ };
File without changes
@@ -0,0 +1,21 @@
1
+ export declare const symbols: {
2
+ error: any;
3
+ success: any;
4
+ synced: any;
5
+ info: any;
6
+ warn: any;
7
+ };
8
+ export declare const setSilentMode: (silent: boolean) => void;
9
+ export declare const setVerboseMode: (verbose: boolean) => void;
10
+ export declare const isSilentMode: () => boolean;
11
+ export declare const isVerboseMode: () => boolean;
12
+ export declare const ui: {
13
+ path: (value: string) => any;
14
+ hash: (value: string | null | undefined) => string;
15
+ pad: (value: string, length: number) => string;
16
+ line: (text?: string) => void;
17
+ header: (label: string, value: string) => void;
18
+ item: (icon: string, label: string, details?: string) => void;
19
+ step: (action: string, subject: string, details?: string) => void;
20
+ debug: (text: string) => void;
21
+ };
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+ import pc from "picocolors";
3
+ import { toPosixPath } from "#core/paths";
4
+ export const symbols = {
5
+ error: pc.red("\u2716"),
6
+ success: pc.green("\u2714"),
7
+ synced: pc.green("\u25CF"),
8
+ info: pc.blue("\u2139"),
9
+ warn: pc.yellow("\u26A0")
10
+ };
11
+ let _silentMode = false;
12
+ let _verboseMode = false;
13
+ export const setSilentMode = (silent) => {
14
+ _silentMode = silent;
15
+ };
16
+ export const setVerboseMode = (verbose) => {
17
+ _verboseMode = verbose;
18
+ };
19
+ export const isSilentMode = () => _silentMode;
20
+ export const isVerboseMode = () => _verboseMode;
21
+ export const ui = {
22
+ // Formatters
23
+ path: (value) => {
24
+ const rel = path.relative(process.cwd(), value);
25
+ const selected = rel.length < value.length ? rel : value;
26
+ return toPosixPath(selected);
27
+ },
28
+ hash: (value) => {
29
+ return value ? value.slice(0, 7) : "-";
30
+ },
31
+ // Layout
32
+ pad: (value, length) => value.padEnd(length),
33
+ // Components
34
+ line: (text = "") => {
35
+ if (_silentMode) return;
36
+ process.stdout.write(`${text}
37
+ `);
38
+ },
39
+ header: (label, value) => {
40
+ if (_silentMode) return;
41
+ process.stdout.write(`${pc.blue("\u2139")} ${label.padEnd(10)} ${value}
42
+ `);
43
+ },
44
+ item: (icon, label, details) => {
45
+ if (_silentMode) return;
46
+ const partLabel = pc.bold(label);
47
+ const partDetails = details ? pc.gray(details) : "";
48
+ process.stdout.write(` ${icon} ${partLabel} ${partDetails}
49
+ `);
50
+ },
51
+ step: (action, subject, details) => {
52
+ if (_silentMode) return;
53
+ const icon = pc.cyan("\u2192");
54
+ process.stdout.write(
55
+ ` ${icon} ${action} ${pc.bold(subject)}${details ? ` ${pc.dim(details)}` : ""}
56
+ `
57
+ );
58
+ },
59
+ debug: (text) => {
60
+ if (_silentMode || !_verboseMode) return;
61
+ process.stdout.write(`${pc.dim("\u2022")} ${pc.dim(text)}
62
+ `);
63
+ }
64
+ };
@@ -0,0 +1,20 @@
1
+ export declare const addSources: (params: {
2
+ configPath?: string;
3
+ entries: Array<{
4
+ id?: string;
5
+ repo: string;
6
+ targetDir?: string;
7
+ }>;
8
+ }) => Promise<{
9
+ configPath: any;
10
+ sources: {
11
+ id: string;
12
+ repo: string;
13
+ targetDir?: string;
14
+ ref?: string;
15
+ }[];
16
+ skipped: string[];
17
+ created: boolean;
18
+ gitignoreUpdated: any;
19
+ gitignorePath: any;
20
+ }>;
@@ -0,0 +1,81 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_CACHE_DIR } from "#config";
3
+ import {
4
+ mergeConfigBase,
5
+ readConfigAtPath,
6
+ resolveConfigTarget,
7
+ writeConfigFile
8
+ } from "#config/io";
9
+ import { ensureGitignoreEntry } from "#core/gitignore";
10
+ import { resolveTargetDir } from "#core/paths";
11
+ import { assertSafeSourceId } from "#core/source-id";
12
+ import { resolveRepoInput } from "#git/resolve-repo";
13
+ const buildNewSources = (entries, config, resolvedPath) => {
14
+ const existingIds = new Set(config.sources.map((source) => source.id));
15
+ const skipped = [];
16
+ const newSources = entries.map((entry) => {
17
+ const resolved = resolveRepoInput(entry.repo);
18
+ const sourceId = entry.id || resolved.inferredId;
19
+ if (!sourceId) {
20
+ throw new Error("Unable to infer id. Provide an explicit id.");
21
+ }
22
+ const safeId = assertSafeSourceId(sourceId, "source id");
23
+ if (existingIds.has(safeId)) {
24
+ skipped.push(safeId);
25
+ return null;
26
+ }
27
+ existingIds.add(safeId);
28
+ if (entry.targetDir) {
29
+ resolveTargetDir(resolvedPath, entry.targetDir);
30
+ }
31
+ return {
32
+ id: safeId,
33
+ repo: resolved.repoUrl,
34
+ ...entry.targetDir ? { targetDir: entry.targetDir } : {},
35
+ ...resolved.ref ? { ref: resolved.ref } : {}
36
+ };
37
+ }).filter(Boolean);
38
+ return { newSources, skipped };
39
+ };
40
+ const ensureGitignore = async (resolvedPath, cacheDir, shouldWrite) => {
41
+ if (!shouldWrite) {
42
+ return null;
43
+ }
44
+ return ensureGitignoreEntry(path.dirname(resolvedPath), cacheDir);
45
+ };
46
+ export const addSources = async (params) => {
47
+ const target = await resolveConfigTarget(params.configPath);
48
+ const resolvedPath = target.resolvedPath;
49
+ const { config, rawConfig, rawPackage, hadDocsCacheConfig } = await readConfigAtPath(target, { allowMissing: true });
50
+ const { newSources, skipped } = buildNewSources(
51
+ params.entries,
52
+ config,
53
+ resolvedPath
54
+ );
55
+ if (newSources.length === 0) {
56
+ throw new Error("All sources already exist in config.");
57
+ }
58
+ const nextConfig = mergeConfigBase(rawConfig ?? config, [
59
+ ...config.sources,
60
+ ...newSources
61
+ ]);
62
+ await writeConfigFile({
63
+ mode: target.mode,
64
+ resolvedPath,
65
+ config: nextConfig,
66
+ rawPackage
67
+ });
68
+ const gitignoreResult = await ensureGitignore(
69
+ resolvedPath,
70
+ rawConfig?.cacheDir ?? DEFAULT_CACHE_DIR,
71
+ !hadDocsCacheConfig
72
+ );
73
+ return {
74
+ configPath: resolvedPath,
75
+ sources: newSources,
76
+ skipped,
77
+ created: true,
78
+ gitignoreUpdated: gitignoreResult?.updated ?? false,
79
+ gitignorePath: gitignoreResult?.gitignorePath ?? null
80
+ };
81
+ };