Skip to content
Open
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"license": "Apache-2.0",
"dependencies": {
"commander": "^12.1.0",
"simple-git": "^3.27.0"
"simple-git": "^3.27.0",
"tar": "^7.5.2"
},
"devDependencies": {
"@types/node": "^22.10.0",
Expand All @@ -44,4 +45,3 @@
"node": ">=18.0.0"
}
}

45 changes: 22 additions & 23 deletions src/commands/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { parsePackageSpec, resolvePackage } from "../lib/registry.js";
import { detectInstalledVersion } from "../lib/version.js";
import {
fetchSource,
packageExists,
listSources,
readMetadata,
} from "../lib/git.js";
import { fetchSource as fetchSourceGit } from "../lib/git.js";
import { fetchSource as fetchSourceNpm, listSources } from "../lib/npm.js";
import { packageExists, readMetadata } from "../lib/common.js";
import { ensureGitignore } from "../lib/gitignore.js";
import { ensureTsconfigExclude } from "../lib/tsconfig.js";
import { updateAgentsMd } from "../lib/agents.js";
Expand All @@ -16,10 +13,14 @@ import {
import { confirm } from "../lib/prompt.js";
import type { FetchResult } from "../types.js";

export type SourceType = "git" | "npm";

export interface FetchOptions {
cwd?: string;
/** Override file modification permission: true = allow, false = deny, undefined = prompt */
allowModifications?: boolean;
/** Source download method: git (clone from GitHub) or npm (download tarball) */
source?: SourceType;
}

/**
Expand Down Expand Up @@ -52,7 +53,9 @@ async function checkFileModificationPermission(
}

// Prompt user for permission
console.log("\nopensrc can update the following files for better integration:");
console.log(
"\nopensrc can update the following files for better integration:",
);
console.log(" • .gitignore - add opensrc/ to ignore list");
console.log(" • tsconfig.json - exclude opensrc/ from compilation");
console.log(" • AGENTS.md - add source code reference section\n");
Expand All @@ -79,19 +82,20 @@ export async function fetchCommand(
options: FetchOptions = {},
): Promise<FetchResult[]> {
const cwd = options.cwd || process.cwd();
const source = options.source || "git";
const results: FetchResult[] = [];

// Check if we're allowed to modify files
const canModifyFiles = await checkFileModificationPermission(cwd, options.allowModifications);
const canModifyFiles = await checkFileModificationPermission(
cwd,
options.allowModifications,
);

if (canModifyFiles) {
// Ensure .gitignore has opensrc/ entry
const gitignoreUpdated = await ensureGitignore(cwd);
if (gitignoreUpdated) {
console.log("✓ Added opensrc/ to .gitignore");
}

// Ensure tsconfig.json excludes opensrc/
const tsconfigUpdated = await ensureTsconfigExclude(cwd);
if (tsconfigUpdated) {
console.log("✓ Added opensrc/ to tsconfig.json exclude");
Expand All @@ -104,11 +108,9 @@ export async function fetchCommand(
console.log(`\nFetching ${name}...`);

try {
// Determine target version
let version = explicitVersion;

if (!version) {
// Try to detect from installed packages
const installedVersion = await detectInstalledVersion(name, cwd);
if (installedVersion) {
version = installedVersion;
Expand All @@ -120,7 +122,6 @@ export async function fetchCommand(
console.log(` → Using specified version: ${version}`);
}

// Check if already exists with the same version
if (packageExists(name, cwd)) {
const existingMeta = await readMetadata(name, cwd);
if (existingMeta && existingMeta.version === version) {
Expand All @@ -141,23 +142,24 @@ export async function fetchCommand(
}
}

// Resolve package info from npm registry
console.log(` → Resolving repository...`);
const resolved = await resolvePackage(name, version);
console.log(` → Found: ${resolved.repoUrl}`);

if (resolved.repoDirectory) {
console.log(` → Monorepo path: ${resolved.repoDirectory}`);
}

// Fetch the source
console.log(` → Cloning at ${resolved.gitTag}...`);
const result = await fetchSource(resolved, cwd);
const fetchFunc = source === "npm" ? fetchSourceNpm : fetchSourceGit;
const sourceMsg =
source === "npm"
? "Downloading from npm..."
: `Cloning at ${resolved.gitTag}...`;
console.log(` → ${sourceMsg}`);
const result = await fetchFunc(resolved, cwd);

if (result.success) {
console.log(` ✓ Saved to ${result.path}`);
if (result.error) {
// Warning message (e.g., tag not found)
console.log(` ⚠ ${result.error}`);
}
} else {
Expand All @@ -178,21 +180,18 @@ export async function fetchCommand(
}
}

// Summary
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;

console.log(`\nDone: ${successful} succeeded, ${failed} failed`);

// Update AGENTS.md with all fetched sources (only if permission granted)
if (successful > 0 && canModifyFiles) {
const allSources = await listSources(cwd);
const agentsUpdated = await updateAgentsMd(allSources, cwd);
if (agentsUpdated) {
console.log("✓ Updated AGENTS.md");
}
} else if (successful > 0 && !canModifyFiles) {
// Still update the sources.json index even without modifying AGENTS.md
const allSources = await listSources(cwd);
const { updatePackageIndex } = await import("../lib/agents.js");
await updatePackageIndex(allSources, cwd);
Expand Down
3 changes: 2 additions & 1 deletion src/commands/remove.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { removeSource, packageExists, listSources } from "../lib/git.js";
import { removeSource, packageExists } from "../lib/common.js";
import { listSources } from "../lib/git.js";
import { updateAgentsMd } from "../lib/agents.js";

export interface RemoveOptions {
Expand Down
43 changes: 27 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,33 @@ program
program
.argument("[packages...]", "packages to fetch (e.g., zod, react@18.2.0)")
.option("--cwd <path>", "working directory (default: current directory)")
.option("--modify [value]", "allow/deny modifying .gitignore, tsconfig.json, AGENTS.md", (val) => {
if (val === undefined || val === "" || val === "true") return true;
if (val === "false") return false;
return true;
})
.action(async (packages: string[], options: { cwd?: string; modify?: boolean }) => {
if (packages.length === 0) {
program.help();
return;
}

await fetchCommand(packages, {
cwd: options.cwd,
allowModifications: options.modify,
});
});
.option(
"--modify [value]",
"allow/deny modifying .gitignore, tsconfig.json, AGENTS.md",
(val) => {
if (val === undefined || val === "" || val === "true") return true;
if (val === "false") return false;
return true;
},
)
.option("--source <type>", "download source from npm or git", "git")
.action(
async (
packages: string[],
options: { cwd?: string; modify?: boolean; source?: string },
) => {
if (packages.length === 0) {
program.help();
return;
}

await fetchCommand(packages, {
cwd: options.cwd,
allowModifications: options.modify,
source: options.source as "git" | "npm",
});
},
);

// List command
program
Expand Down
91 changes: 91 additions & 0 deletions src/lib/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { join } from "path";
import { existsSync } from "fs";
import { rm, readFile } from "fs/promises";

const OPENSRC_DIR = "opensrc";

/**
* Get the opensrc directory path
*/
export function getOpensrcDir(cwd: string = process.cwd()): string {
return join(cwd, OPENSRC_DIR);
}

/**
* Get the path where a package's source will be stored
*/
export function getPackagePath(
packageName: string,
cwd: string = process.cwd(),
): string {
return join(getOpensrcDir(cwd), packageName);
}

/**
* Check if a package source already exists
*/
export function packageExists(
packageName: string,
cwd: string = process.cwd(),
): boolean {
return existsSync(getPackagePath(packageName, cwd));
}

/**
* Read metadata for a fetched package
*/
export async function readMetadata(
packageName: string,
cwd: string = process.cwd(),
): Promise<{
name: string;
version: string;
repoUrl: string;
repoDirectory?: string;
fetchedTag: string;
fetchedAt: string;
downloadMethod?: "git" | "npm";
} | null> {
const packagePath = getPackagePath(packageName, cwd);
const metadataPath = join(packagePath, ".opensrc-meta.json");

if (!existsSync(metadataPath)) {
return null;
}

try {
const content = await readFile(metadataPath, "utf-8");
return JSON.parse(content);
} catch {
return null;
}
}

/**
* Remove source code for a package
*/
export async function removeSource(
packageName: string,
cwd: string = process.cwd(),
): Promise<boolean> {
const packagePath = getPackagePath(packageName, cwd);

if (!existsSync(packagePath)) {
return false;
}

await rm(packagePath, { recursive: true, force: true });

if (packageName.startsWith("@")) {
const scopeDir = join(getOpensrcDir(cwd), packageName.split("/")[0]);
try {
const { readdir } = await import("fs/promises");
const contents = await readdir(scopeDir);
if (contents.length === 0) {
await rm(scopeDir, { recursive: true, force: true });
}
} catch {}
}

return true;
}
33 changes: 3 additions & 30 deletions src/lib/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,9 @@ import { simpleGit, SimpleGit } from "simple-git";
import { rm, mkdir, readFile, writeFile } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { getOpensrcDir, getPackagePath, packageExists } from "./common.js";
import type { ResolvedPackage, FetchResult } from "../types.js";

const OPENSRC_DIR = "opensrc";

/**
* Get the opensrc directory path
*/
export function getOpensrcDir(cwd: string = process.cwd()): string {
return join(cwd, OPENSRC_DIR);
}

/**
* Get the path where a package's source will be stored
*/
export function getPackagePath(
packageName: string,
cwd: string = process.cwd(),
): string {
// Handle scoped packages: @scope/name -> @scope/name (keep the structure)
return join(getOpensrcDir(cwd), packageName);
}

/**
* Check if a package source already exists
*/
export function packageExists(
packageName: string,
cwd: string = process.cwd(),
): boolean {
return existsSync(getPackagePath(packageName, cwd));
}

/**
* Try to clone at a specific tag, with fallbacks
*/
Expand Down Expand Up @@ -93,6 +64,7 @@ async function writeMetadata(
repoDirectory: resolved.repoDirectory,
fetchedTag: actualTag,
fetchedAt: new Date().toISOString(),
downloadMethod: "git" as const,
};

const metadataPath = join(packagePath, ".opensrc-meta.json");
Expand All @@ -112,6 +84,7 @@ export async function readMetadata(
repoDirectory?: string;
fetchedTag: string;
fetchedAt: string;
downloadMethod?: "git" | "npm";
} | null> {
const packagePath = getPackagePath(packageName, cwd);
const metadataPath = join(packagePath, ".opensrc-meta.json");
Expand Down
Loading