Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/calm-badgers-rebuild.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@neuledge/context": patch
---

Preserve installed documentation when a rebuild or install fails. Stage and validate replacements before renaming them into place, and keep temporary databases out of package discovery.
6 changes: 6 additions & 0 deletions packages/context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,12 @@ context add ./my-project --name my-lib --pkg-version 2.0 --save ./packages/
context add ./packages/my-lib@2.0.db
```

Rebuilds and installs stage a replacement beside the destination, then close and
validate it before replacing the installed package. A failed build, download, or
replacement leaves the previous package available. Temporary files are excluded
from package discovery. If the operating system blocks replacement of an open
file (for example, on Windows), close the reader and retry the install.

---

## :whale: Docker
Expand Down
67 changes: 15 additions & 52 deletions packages/context/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
#!/usr/bin/env node

import {
copyFileSync,
createWriteStream,
existsSync,
mkdirSync,
readdirSync,
renameSync,
statSync,
unlinkSync,
} from "node:fs";
Expand Down Expand Up @@ -55,11 +52,13 @@ import {
buildPackage,
type MarkdownFile,
} from "./package-builder.js";
import { copyPackageFile, createPackageTempFile } from "./package-file.js";
import { type SearchResult, search } from "./search.js";
import { ContextServer } from "./server.js";
import {
getPackageFileName,
isAllowedLibrary,
loadPackages,
type PackageInfo,
PackageStore,
packageKey,
Expand Down Expand Up @@ -497,7 +496,7 @@ function savePackageCopy(
destPath = join(resolvedSavePath, getPackageFileName(packageName, version));
}

copyFileSync(sourcePath, destPath);
copyPackageFile(sourcePath, destPath);
console.log(`✓ Saved to ${destPath}`);
}

Expand All @@ -506,21 +505,6 @@ function ensureDataDir(): void {
mkdirSync(DATA_DIR, { recursive: true });
}

/** Load all packages from the data directory into the store. */
function loadPackages(store: PackageStore): void {
if (!existsSync(DATA_DIR)) return;

for (const file of readdirSync(DATA_DIR)) {
if (!file.endsWith(".db")) continue;
try {
const info = readPackageInfo(join(DATA_DIR, file));
store.add(info);
} catch {
// Skip invalid packages
}
}
}

/**
* Report a finished install.
*
Expand Down Expand Up @@ -562,7 +546,7 @@ function reportInstalled(pkg: {
);

const store = new PackageStore();
loadPackages(store);
loadPackages(store, DATA_DIR);
const preferred = store.get(pkg.name);
if (!preferred || preferred.version === pkg.version) return;

Expand Down Expand Up @@ -648,7 +632,7 @@ function addFromFile(source: string, options: { save?: string }): void {
const destPath = join(DATA_DIR, destName);

if (resolve(sourcePath) !== destPath) {
copyFileSync(sourcePath, destPath);
copyPackageFile(sourcePath, destPath);
console.log(`✓ Copied to ${destPath}`);
info.path = destPath;
}
Expand All @@ -668,47 +652,26 @@ async function addFromUrl(
): Promise<void> {
console.log(`Downloading ${url}...`);

// Extract filename from URL for temp file
const urlObj = new URL(url);
const filename = basename(urlObj.pathname) || "package.db";

// Download to temp location first
ensureDataDir();
const tempPath = join(DATA_DIR, `.downloading-${Date.now()}-${filename}`);
const temp = createPackageTempFile(DATA_DIR);

try {
await downloadFile(url, tempPath);
await downloadFile(url, temp.path);
console.log(`✓ Downloaded`);

// Validate the package
const info = readPackageInfo(tempPath);
const info = temp.install();
console.log(`✓ Validated package`);

// Move to final location
const destName = getPackageFileName(info.name, info.version);
const destPath = join(DATA_DIR, destName);

// Remove old version if it exists
if (existsSync(destPath)) {
unlinkSync(destPath);
}

// Rename temp to final
renameSync(tempPath, destPath);
info.path = destPath;

// Save to custom path if specified
if (options.save) {
savePackageCopy(destPath, options.save, info.name, info.version);
savePackageCopy(info.path, options.save, info.name, info.version);
}

reportInstalled(info);
} catch (err) {
// Clean up temp file on error
if (existsSync(tempPath)) {
unlinkSync(tempPath);
}
throw err;
} finally {
temp.cleanup();
}
}

Expand Down Expand Up @@ -1088,7 +1051,7 @@ program
.description("Show installed packages")
.action(() => {
const store = new PackageStore();
loadPackages(store);
loadPackages(store, DATA_DIR);
const packages = store.list();

if (packages.length === 0) {
Expand Down Expand Up @@ -1158,7 +1121,7 @@ program
.argument("<name>", "Package name (e.g., 'next' or 'next@v16.2.0')")
.action((name: string) => {
const store = new PackageStore();
loadPackages(store);
loadPackages(store, DATA_DIR);

const target = resolveRemoveTarget(name, store.list());
if ("error" in target) {
Expand Down Expand Up @@ -1196,7 +1159,7 @@ program
libs?: string[];
}) => {
const store = new PackageStore();
loadPackages(store);
loadPackages(store, DATA_DIR);

const allowedLibraries = options.libs
? resolveAllowedLibraries(options.libs, store.list())
Expand Down Expand Up @@ -1265,7 +1228,7 @@ program
.argument("<topic>", GET_DOCS_TOPIC_DESCRIPTION)
.action((library: string, topic: string) => {
const store = new PackageStore();
loadPackages(store);
loadPackages(store, DATA_DIR);

const packages = store.list();
const pkg = store.get(library);
Expand Down
Loading
Loading