pkgref 0.0.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.
- package/CHANGELOG.md +7 -0
- package/README.md +40 -0
- package/dist/cli.d.mts +31 -0
- package/dist/cli.mjs +464 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# pkgref
|
|
2
|
+
|
|
3
|
+
[GitHub repository](https://github.com/yue4u/pkgref)
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm dlx pkgref
|
|
7
|
+
# list package.json deps in interactive mode for select and clone repo to target dir (default to docs/pkg-reference).
|
|
8
|
+
pnpm dlx pkgref --pkgs=react,vitest --dir docs/repo
|
|
9
|
+
# specify pkg and dir to clone with cli param
|
|
10
|
+
pnpm dlx pkgref update
|
|
11
|
+
# fast-forward all clones in docs/pkg-reference
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- list docs/example dir in cloned repo to root as index markdown for search
|
|
17
|
+
|
|
18
|
+
## Future
|
|
19
|
+
|
|
20
|
+
- support more than package.json
|
|
21
|
+
|
|
22
|
+
## Development
|
|
23
|
+
|
|
24
|
+
- Install dependencies:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
vp install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Run the unit tests:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
vp test
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- Build the library:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
vp pack
|
|
40
|
+
```
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type RepositoryRef = {
|
|
3
|
+
cloneUrl: string;
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
type RepositoryGroup = RepositoryRef & {
|
|
8
|
+
packages: string[];
|
|
9
|
+
};
|
|
10
|
+
type IndexRepository = {
|
|
11
|
+
directory: string;
|
|
12
|
+
packages: string[];
|
|
13
|
+
root: string;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/cli.d.ts
|
|
17
|
+
type GitResult = {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
output: string;
|
|
20
|
+
};
|
|
21
|
+
type GitRunner = (args: string[], cwd?: string) => Promise<GitResult>;
|
|
22
|
+
declare function runCli(argv?: string[], cwd?: string): Promise<0 | 1>;
|
|
23
|
+
declare function resolveRepository(packageName: string, cwd: string): Promise<RepositoryRef>;
|
|
24
|
+
declare function processRepository(group: RepositoryGroup, target: string, runGit?: GitRunner): Promise<{
|
|
25
|
+
failed: boolean;
|
|
26
|
+
index?: IndexRepository;
|
|
27
|
+
}>;
|
|
28
|
+
declare function pullRepository(root: string, name: string, prune?: boolean, runGit?: GitRunner): Promise<"dirty" | "failed" | "updated">;
|
|
29
|
+
declare function invalidCloneReason(root: string, expected: RepositoryGroup, runGit?: GitRunner): Promise<string | undefined>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { invalidCloneReason, processRepository, pullRepository, resolveRepository, runCli };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { parseArgs, promisify } from "node:util";
|
|
6
|
+
import { cancel, confirm, isCancel, multiselect, note, text } from "@clack/prompts";
|
|
7
|
+
//#region package.json
|
|
8
|
+
var version = "0.0.1";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/core.ts
|
|
11
|
+
const INDEX_MARKER = "<!-- generated by pkgref; do not edit -->";
|
|
12
|
+
const AGENTS_START = "<!-- PKGREF START -->";
|
|
13
|
+
const AGENTS_END = "<!-- PKGREF END -->";
|
|
14
|
+
const DEPENDENCY_FIELDS = [
|
|
15
|
+
"dependencies",
|
|
16
|
+
"devDependencies",
|
|
17
|
+
"peerDependencies",
|
|
18
|
+
"optionalDependencies"
|
|
19
|
+
];
|
|
20
|
+
const REFERENCE_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
21
|
+
"doc",
|
|
22
|
+
"docs",
|
|
23
|
+
"example",
|
|
24
|
+
"examples"
|
|
25
|
+
]);
|
|
26
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
27
|
+
function discoverDependencies(manifest) {
|
|
28
|
+
return [...new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(manifest[field] ?? {})))].sort();
|
|
29
|
+
}
|
|
30
|
+
async function readManifest(path) {
|
|
31
|
+
try {
|
|
32
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
33
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw new Error("expected a JSON object");
|
|
34
|
+
return manifest;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const action = error.code ? "Cannot read" : "Invalid";
|
|
37
|
+
throw new Error(`${action} package manifest at ${path}: ${errorMessage(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function parsePackageList(value) {
|
|
41
|
+
return [...new Set(value.split(",").map((name) => name.trim()).filter(Boolean))];
|
|
42
|
+
}
|
|
43
|
+
function renderAgentsReference(contents, indexPath) {
|
|
44
|
+
const block = `${AGENTS_START}
|
|
45
|
+
|
|
46
|
+
## Package references
|
|
47
|
+
|
|
48
|
+
Before working with dependency APIs, consult the
|
|
49
|
+
[package reference index](${indexPath}).
|
|
50
|
+
It links to cloned upstream documentation and examples.
|
|
51
|
+
|
|
52
|
+
${AGENTS_END}`;
|
|
53
|
+
const marked = new RegExp(`${AGENTS_START}[\\s\\S]*?${AGENTS_END}`);
|
|
54
|
+
return `${(marked.test(contents) ? contents.replace(marked, block) : contents.trim() ? `${contents.trimEnd()}\n\n${block}` : block).trimEnd()}\n`;
|
|
55
|
+
}
|
|
56
|
+
function repositoryFromMetadata(metadata) {
|
|
57
|
+
const repository = metadata.repository;
|
|
58
|
+
const url = typeof repository === "string" ? repository : repository?.url;
|
|
59
|
+
if (!url) throw new Error("package metadata does not declare a repository");
|
|
60
|
+
return normalizeRepository(url);
|
|
61
|
+
}
|
|
62
|
+
function normalizeRepository(rawUrl) {
|
|
63
|
+
let url = rawUrl.trim().replace(/^git\+/, "");
|
|
64
|
+
if (!url) throw new Error("repository URL is empty");
|
|
65
|
+
if (/^github:/i.test(url)) url = `https://github.com/${url.slice(7)}`;
|
|
66
|
+
else if (/^[\w.-]+\/[\w.-]+$/.test(url)) url = `https://github.com/${url}`;
|
|
67
|
+
else if (/^git@[^:]+:/.test(url)) url = url.replace(/^git@([^:]+):/, "ssh://git@$1/");
|
|
68
|
+
else if (url.startsWith("git://")) url = `https://${url.slice(6)}`;
|
|
69
|
+
if (!URL.canParse(url)) throw new Error(`unsupported repository URL: ${rawUrl}`);
|
|
70
|
+
const parsed = new URL(url);
|
|
71
|
+
if (![
|
|
72
|
+
"http:",
|
|
73
|
+
"https:",
|
|
74
|
+
"ssh:"
|
|
75
|
+
].includes(parsed.protocol)) throw new Error(`unsupported repository protocol: ${parsed.protocol}`);
|
|
76
|
+
const path = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
|
|
77
|
+
const name = basename(path);
|
|
78
|
+
if (!parsed.hostname || !path || !name || name === "." || name === "..") throw new Error(`invalid repository URL: ${rawUrl}`);
|
|
79
|
+
const user = parsed.protocol === "ssh:" && parsed.username ? `${parsed.username}@` : "";
|
|
80
|
+
return {
|
|
81
|
+
cloneUrl: parsed.protocol === "ssh:" ? `ssh://${user}${parsed.hostname}/${path}.git` : `${parsed.protocol}//${parsed.host}/${path}.git`,
|
|
82
|
+
key: `${parsed.hostname}/${path}`.toLowerCase(),
|
|
83
|
+
name
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function groupRepositories(packages) {
|
|
87
|
+
const groups = /* @__PURE__ */ new Map();
|
|
88
|
+
const folders = /* @__PURE__ */ new Map();
|
|
89
|
+
for (const { name, repository } of packages) {
|
|
90
|
+
const owner = folders.get(repository.name);
|
|
91
|
+
if (owner && owner !== repository.key) throw new Error(`repository folder collision: different repositories resolve to "${repository.name}"`);
|
|
92
|
+
folders.set(repository.name, repository.key);
|
|
93
|
+
const group = groups.get(repository.key);
|
|
94
|
+
if (group) group.packages.push(name);
|
|
95
|
+
else groups.set(repository.key, {
|
|
96
|
+
...repository,
|
|
97
|
+
packages: [name]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return [...groups.values()].map((group) => ({
|
|
101
|
+
...group,
|
|
102
|
+
packages: [...new Set(group.packages)].sort()
|
|
103
|
+
})).sort(byName);
|
|
104
|
+
}
|
|
105
|
+
async function findReferenceDirectories(repositoryRoot, targetRoot) {
|
|
106
|
+
const found = [];
|
|
107
|
+
async function walk(directory) {
|
|
108
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
109
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
110
|
+
const path = join(directory, entry.name);
|
|
111
|
+
if (REFERENCE_DIRECTORIES.has(entry.name.toLowerCase())) found.push({
|
|
112
|
+
label: posixPath(relative(repositoryRoot, path)),
|
|
113
|
+
path: posixPath(relative(targetRoot, path))
|
|
114
|
+
});
|
|
115
|
+
else await walk(path);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await walk(repositoryRoot);
|
|
119
|
+
return found.sort((a, b) => a.path.localeCompare(b.path));
|
|
120
|
+
}
|
|
121
|
+
async function renderIndex(repositories, targetRoot) {
|
|
122
|
+
const sections = await Promise.all([...repositories].sort((a, b) => a.directory.localeCompare(b.directory)).map(async ({ directory, packages, root }) => {
|
|
123
|
+
const references = await findReferenceDirectories(root, targetRoot);
|
|
124
|
+
const links = references.length ? references.map((item) => `- [${escapeLabel(item.label)}](./${encodePath(item.path)})`) : [`- [Repository root](./${encodePath(posixPath(relative(targetRoot, root)))})`];
|
|
125
|
+
return `## ${directory}
|
|
126
|
+
|
|
127
|
+
Packages: ${[...packages].sort().map((name) => `\`${name}\``).join(", ")}
|
|
128
|
+
|
|
129
|
+
${links.join("\n")}`;
|
|
130
|
+
}));
|
|
131
|
+
return `${INDEX_MARKER}\n\n# Package references\n\n${sections.join("\n\n")}\n`;
|
|
132
|
+
}
|
|
133
|
+
function mergeIndexes(existing, generated) {
|
|
134
|
+
const oldIndex = splitIndex(existing);
|
|
135
|
+
const newIndex = splitIndex(generated);
|
|
136
|
+
const sections = new Map([...oldIndex.sections, ...newIndex.sections]);
|
|
137
|
+
return `${[oldIndex.preamble, ...[...sections].sort(([a], [b]) => a.localeCompare(b)).map(([, value]) => value)].filter(Boolean).join("\n\n")}\n`;
|
|
138
|
+
}
|
|
139
|
+
function errorMessage(error) {
|
|
140
|
+
return error instanceof Error ? error.message : String(error);
|
|
141
|
+
}
|
|
142
|
+
const posixPath = (path) => path.split(sep).join("/");
|
|
143
|
+
const encodePath = (path) => path.split("/").map(encodeURIComponent).join("/");
|
|
144
|
+
const escapeLabel = (label) => label.replaceAll("\\", "\\\\").replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
145
|
+
function splitIndex(contents) {
|
|
146
|
+
const matches = [...contents.matchAll(/^## (.+)$/gm)];
|
|
147
|
+
const sections = /* @__PURE__ */ new Map();
|
|
148
|
+
matches.forEach((match, index) => {
|
|
149
|
+
sections.set(match[1].trim(), contents.slice(match.index, matches[index + 1]?.index).trimEnd());
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
preamble: contents.slice(0, matches[0]?.index ?? contents.length).trimEnd(),
|
|
153
|
+
sections
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/cli.ts
|
|
158
|
+
const exec = promisify(execFile);
|
|
159
|
+
const DEFAULT_DIR = "docs/pkg-reference";
|
|
160
|
+
const CANCELLED = Symbol("cancelled");
|
|
161
|
+
const OPTIONS = {
|
|
162
|
+
dir: { type: "string" },
|
|
163
|
+
help: {
|
|
164
|
+
type: "boolean",
|
|
165
|
+
short: "h"
|
|
166
|
+
},
|
|
167
|
+
pkgs: { type: "string" },
|
|
168
|
+
version: {
|
|
169
|
+
type: "boolean",
|
|
170
|
+
short: "v"
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const HELP = `pkgref - clone package source references
|
|
174
|
+
|
|
175
|
+
Usage:
|
|
176
|
+
pkgref [--pkgs=<name,...>] [--dir=<path>]
|
|
177
|
+
pkgref update [--dir=<path>]
|
|
178
|
+
|
|
179
|
+
Options:
|
|
180
|
+
--pkgs Comma-separated packages. Undeclared packages are allowed with a warning.
|
|
181
|
+
--dir Target directory (default: ${DEFAULT_DIR}).
|
|
182
|
+
-h, --help Show this help.
|
|
183
|
+
-v, --version Show the installed version.
|
|
184
|
+
|
|
185
|
+
Commands:
|
|
186
|
+
update Fast-forward all cloned repositories to their latest upstream revision.
|
|
187
|
+
`;
|
|
188
|
+
async function runCli(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
189
|
+
try {
|
|
190
|
+
const { values, positionals } = parseArgs({
|
|
191
|
+
args: argv,
|
|
192
|
+
allowPositionals: true,
|
|
193
|
+
options: OPTIONS,
|
|
194
|
+
strict: true
|
|
195
|
+
});
|
|
196
|
+
if (values.help) {
|
|
197
|
+
console.log(HELP);
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
if (values.version) {
|
|
201
|
+
console.log(version);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
if (positionals.length > 1 || positionals[0] && positionals[0] !== "update") throw new Error(`unknown command: ${positionals.join(" ")}`);
|
|
205
|
+
if (positionals[0] === "update") {
|
|
206
|
+
if (values.pkgs !== void 0) throw new Error("--pkgs cannot be used with the update command");
|
|
207
|
+
return await updateRepositories(resolve(cwd, values.dir ?? DEFAULT_DIR));
|
|
208
|
+
}
|
|
209
|
+
return await cloneRepositories(cwd, values.pkgs, values.dir);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error === CANCELLED) return 0;
|
|
212
|
+
console.error(`Error: ${errorMessage(error)}`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function updateRepositories(target) {
|
|
217
|
+
if (!await exists(target)) {
|
|
218
|
+
console.log(`No package reference directory found at ${target}.`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
const repositories = [];
|
|
222
|
+
for (const entry of await readdir(target, { withFileTypes: true })) if (entry.isDirectory() && await exists(join(target, entry.name, ".git"))) repositories.push(entry.name);
|
|
223
|
+
repositories.sort();
|
|
224
|
+
if (!repositories.length) {
|
|
225
|
+
console.log(`No cloned repositories found in ${target}.`);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
const updated = (await Promise.all(repositories.map((name) => pullRepository(join(target, name), name, true)))).filter((result) => result === "updated").length;
|
|
229
|
+
const failed = updated !== repositories.length;
|
|
230
|
+
console.log(`Finished: ${updated} of ${repositories.length} repositories updated${failed ? " with errors" : ""}.`);
|
|
231
|
+
return failed ? 1 : 0;
|
|
232
|
+
}
|
|
233
|
+
async function cloneRepositories(cwd, packageArgument, directory) {
|
|
234
|
+
const manifestPath = join(cwd, "package.json");
|
|
235
|
+
const declared = discoverDependencies(await readManifest(manifestPath));
|
|
236
|
+
const interactive = packageArgument === void 0;
|
|
237
|
+
let addToAgents = false;
|
|
238
|
+
const packages = interactive ? await selectPackages(declared) : parsePackageList(packageArgument);
|
|
239
|
+
if (!interactive) {
|
|
240
|
+
if (!packages.length) throw new Error("--pkgs must contain at least one package name");
|
|
241
|
+
for (const name of packages.filter((name) => !declared.includes(name))) console.warn(`Warning: "${name}" is not declared in ${manifestPath}; resolving it anyway.`);
|
|
242
|
+
}
|
|
243
|
+
const resolved = (await Promise.all(packages.map(async (name) => {
|
|
244
|
+
try {
|
|
245
|
+
return {
|
|
246
|
+
name,
|
|
247
|
+
repository: await resolveRepository(name, cwd)
|
|
248
|
+
};
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(`Failed to resolve ${name}: ${errorMessage(error)}`);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}))).filter((item) => item !== null);
|
|
254
|
+
let failed = resolved.length !== packages.length;
|
|
255
|
+
const groups = groupRepositories(resolved);
|
|
256
|
+
if (!groups.length) return failed ? 1 : 0;
|
|
257
|
+
if (interactive) {
|
|
258
|
+
await confirmRepositories(groups);
|
|
259
|
+
directory = (directory ?? await ask(text({
|
|
260
|
+
message: "Where should package references be stored?",
|
|
261
|
+
initialValue: DEFAULT_DIR,
|
|
262
|
+
validate: (value) => !value?.trim() ? "Target directory is required." : void 0
|
|
263
|
+
}))).trim();
|
|
264
|
+
addToAgents = await confirmChoice("Add the package reference index to AGENTS.md?");
|
|
265
|
+
}
|
|
266
|
+
const target = resolve(cwd, directory ?? DEFAULT_DIR);
|
|
267
|
+
const indexPath = join(target, "INDEX.md");
|
|
268
|
+
const existingIndex = await readOwnedIndex(indexPath);
|
|
269
|
+
await mkdir(target, { recursive: true });
|
|
270
|
+
const indexed = [];
|
|
271
|
+
for (const group of groups) {
|
|
272
|
+
const result = await processRepository(group, target);
|
|
273
|
+
failed ||= result.failed;
|
|
274
|
+
if (result.index) indexed.push(result.index);
|
|
275
|
+
}
|
|
276
|
+
const generated = await renderIndex(indexed, target);
|
|
277
|
+
await writeFile(indexPath, existingIndex ? mergeIndexes(existingIndex, generated) : generated);
|
|
278
|
+
console.log(`Wrote ${indexPath}`);
|
|
279
|
+
if (addToAgents) await addAgentsReference(cwd, indexPath);
|
|
280
|
+
console.log(`Finished: ${indexed.length} repositories indexed, ${failed ? "with errors" : "successfully"}.`);
|
|
281
|
+
return failed ? 1 : 0;
|
|
282
|
+
}
|
|
283
|
+
async function resolveRepository(packageName, cwd) {
|
|
284
|
+
const installedManifest = join(cwd, "node_modules", packageName, "package.json");
|
|
285
|
+
if (await exists(installedManifest)) return repositoryFromMetadata(await readManifest(installedManifest));
|
|
286
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { headers: { accept: "application/json" } });
|
|
287
|
+
if (!response.ok) throw new Error(`npm registry returned ${response.status} ${response.statusText}`);
|
|
288
|
+
return repositoryFromMetadata(await response.json());
|
|
289
|
+
}
|
|
290
|
+
async function processRepository(group, target, runGit = git) {
|
|
291
|
+
const root = join(target, group.name);
|
|
292
|
+
const index = {
|
|
293
|
+
directory: group.name,
|
|
294
|
+
packages: group.packages,
|
|
295
|
+
root
|
|
296
|
+
};
|
|
297
|
+
if (!await exists(root)) {
|
|
298
|
+
console.log(`Cloning ${group.name} (${group.packages.join(", ")})...`);
|
|
299
|
+
const result = await runGit([
|
|
300
|
+
"clone",
|
|
301
|
+
"--depth",
|
|
302
|
+
"1",
|
|
303
|
+
"--",
|
|
304
|
+
group.cloneUrl,
|
|
305
|
+
root
|
|
306
|
+
]);
|
|
307
|
+
if (!result.ok) {
|
|
308
|
+
console.error(`Failed to clone ${group.name}: ${result.output}`);
|
|
309
|
+
return { failed: true };
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
failed: false,
|
|
313
|
+
index
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const invalidReason = await invalidCloneReason(root, group, runGit);
|
|
317
|
+
if (invalidReason) {
|
|
318
|
+
console.warn(`Skipping ${group.name}: ${invalidReason}`);
|
|
319
|
+
return { failed: false };
|
|
320
|
+
}
|
|
321
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !await confirmUpdate(group.name)) {
|
|
322
|
+
console.log(`Skipped update for ${group.name}.`);
|
|
323
|
+
return {
|
|
324
|
+
failed: false,
|
|
325
|
+
index
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
failed: await pullRepository(root, group.name, false, runGit) === "failed",
|
|
330
|
+
index
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function pullRepository(root, name, prune = false, runGit = git) {
|
|
334
|
+
const status = await runGit(["status", "--porcelain"], root);
|
|
335
|
+
if (!status.ok) {
|
|
336
|
+
console.error(`Failed to inspect ${name}: ${status.output}`);
|
|
337
|
+
return "failed";
|
|
338
|
+
}
|
|
339
|
+
if (status.output) {
|
|
340
|
+
console.warn(`Skipped ${name}: the worktree has local changes.`);
|
|
341
|
+
return "dirty";
|
|
342
|
+
}
|
|
343
|
+
console.log(`Updating ${name}...`);
|
|
344
|
+
const pull = await runGit([
|
|
345
|
+
"pull",
|
|
346
|
+
"--ff-only",
|
|
347
|
+
...prune ? ["--prune"] : []
|
|
348
|
+
], root);
|
|
349
|
+
if (!pull.ok) {
|
|
350
|
+
console.error(`Failed to update ${name}: ${pull.output}`);
|
|
351
|
+
return "failed";
|
|
352
|
+
}
|
|
353
|
+
console.log(`Updated ${name}.`);
|
|
354
|
+
return "updated";
|
|
355
|
+
}
|
|
356
|
+
async function invalidCloneReason(root, expected, runGit = git) {
|
|
357
|
+
const origin = await runGit([
|
|
358
|
+
"remote",
|
|
359
|
+
"get-url",
|
|
360
|
+
"origin"
|
|
361
|
+
], root);
|
|
362
|
+
if (!origin.ok) return "the destination is not a Git clone with an origin";
|
|
363
|
+
try {
|
|
364
|
+
return normalizeRepository(origin.output).key === expected.key ? void 0 : "the existing clone has a different origin";
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return `cannot read its origin: ${errorMessage(error)}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function readOwnedIndex(path) {
|
|
370
|
+
const contents = await readOptional(path);
|
|
371
|
+
if (contents === void 0) return;
|
|
372
|
+
if (!contents.startsWith("<!-- generated by pkgref; do not edit -->")) throw new Error(`refusing to overwrite user-owned index at ${path}`);
|
|
373
|
+
return contents;
|
|
374
|
+
}
|
|
375
|
+
async function selectPackages(packages) {
|
|
376
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("--pkgs is required when pkgref is not running in an interactive terminal");
|
|
377
|
+
if (!packages.length) stop("No dependencies found in package.json.");
|
|
378
|
+
const selected = await ask(multiselect({
|
|
379
|
+
message: "Select packages to clone",
|
|
380
|
+
options: packages.map((value) => ({
|
|
381
|
+
label: value,
|
|
382
|
+
value
|
|
383
|
+
})),
|
|
384
|
+
required: false,
|
|
385
|
+
maxItems: 10
|
|
386
|
+
}));
|
|
387
|
+
if (!selected.length) stop("No packages selected.");
|
|
388
|
+
return selected;
|
|
389
|
+
}
|
|
390
|
+
async function confirmRepositories(repositories) {
|
|
391
|
+
note(repositories.map(({ name, packages, cloneUrl }) => `${name} (${packages.join(", ")})\n${cloneUrl}`).join("\n\n"), "Git repositories");
|
|
392
|
+
if (!await confirmChoice("Clone these Git repositories?", "Operation cancelled. No repositories were cloned.")) abort("Operation cancelled. No repositories were cloned.");
|
|
393
|
+
}
|
|
394
|
+
const confirmChoice = (message, cancelMessage) => ask(confirm({
|
|
395
|
+
message,
|
|
396
|
+
initialValue: true
|
|
397
|
+
}), cancelMessage);
|
|
398
|
+
async function confirmUpdate(name) {
|
|
399
|
+
const value = await confirm({
|
|
400
|
+
message: `${name} already exists. Update it?`,
|
|
401
|
+
initialValue: false
|
|
402
|
+
});
|
|
403
|
+
if (isCancel(value)) {
|
|
404
|
+
cancel("Update skipped.");
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
async function ask(answer, message = "Operation cancelled.") {
|
|
410
|
+
const value = await answer;
|
|
411
|
+
if (isCancel(value)) abort(message);
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
function abort(message) {
|
|
415
|
+
cancel(message);
|
|
416
|
+
throw CANCELLED;
|
|
417
|
+
}
|
|
418
|
+
function stop(message) {
|
|
419
|
+
console.log(message);
|
|
420
|
+
throw CANCELLED;
|
|
421
|
+
}
|
|
422
|
+
async function addAgentsReference(cwd, indexPath) {
|
|
423
|
+
const path = join(cwd, "AGENTS.md");
|
|
424
|
+
const link = relative(cwd, indexPath).split(sep).map(encodeURIComponent).join("/");
|
|
425
|
+
await writeFile(path, renderAgentsReference(await readOptional(path) ?? "", link));
|
|
426
|
+
console.log(`Updated ${path}`);
|
|
427
|
+
}
|
|
428
|
+
async function git(args, cwd) {
|
|
429
|
+
try {
|
|
430
|
+
const { stdout, stderr } = await exec("git", args, {
|
|
431
|
+
cwd,
|
|
432
|
+
encoding: "utf8"
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
ok: true,
|
|
436
|
+
output: (stderr || stdout).trim()
|
|
437
|
+
};
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const result = error;
|
|
440
|
+
return {
|
|
441
|
+
ok: false,
|
|
442
|
+
output: (result.stderr || result.stdout || result.message).trim()
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function readOptional(path) {
|
|
447
|
+
try {
|
|
448
|
+
return await readFile(path, "utf8");
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (error.code === "ENOENT") return;
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async function exists(path) {
|
|
455
|
+
try {
|
|
456
|
+
await access(path);
|
|
457
|
+
return true;
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (import.meta.url === `file://${process.argv[1]}`) process.exitCode = await runCli();
|
|
463
|
+
//#endregion
|
|
464
|
+
export { invalidCloneReason, processRepository, pullRepository, resolveRepository, runCli };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pkgref",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Clone dependency source repositories and index their docs and examples.",
|
|
5
|
+
"homepage": "https://github.com/yue4u/pkgref#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/yue4u/pkgref/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/yue4u/pkgref.git"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"pkgref": "./dist/cli.mjs"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@clack/prompts": "^1.7.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^26.1.1",
|
|
32
|
+
"bumpp": "^11.1.0",
|
|
33
|
+
"typescript": "^7.0.2",
|
|
34
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.5",
|
|
35
|
+
"vite-plus": "0.2.5"
|
|
36
|
+
},
|
|
37
|
+
"devEngines": {
|
|
38
|
+
"packageManager": {
|
|
39
|
+
"name": "pnpm",
|
|
40
|
+
"version": "11.17.0",
|
|
41
|
+
"onFail": "download"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "vp pack",
|
|
49
|
+
"dev": "vp pack --watch",
|
|
50
|
+
"test": "vp test",
|
|
51
|
+
"check": "vp check"
|
|
52
|
+
}
|
|
53
|
+
}
|