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
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"node": ">=22.12"
},
"packageManager": "pnpm@12.3.4",
"bundleBudgetKB": 601,
"bundleBudgetKB": 603,
"scripts": {
"build": "tsup && node scripts/check-bundle-size.mjs",
"build:check-size": "node scripts/check-bundle-size.mjs",
Expand Down
5 changes: 5 additions & 0 deletions cli/scripts/check-bundle-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
// display and the subcommand. It deletes 198 lines from a plugin, which the bundle does
// not carry either way - the trade is a plugin script that had drifted for bytes that are
// measured. Same 2.2 KB headroom as the three raises before it.
// 603 was set when a marketplace registry that cannot be read began saying so: measured
// 600.7 -> 601.2 KB, +0.5 KB for one guard and the sentence it prints. The smallest raise so
// far, for the smallest change - and the one that showed the budget had 0.3 KB of headroom
// left, which is less than a correctness fix costs. Same 2.2 KB headroom as the four raises
// before it.
const budgetKB = pkg.bundleBudgetKB ?? 500;
const budgetBytes = budgetKB * 1024;

Expand Down
28 changes: 26 additions & 2 deletions cli/src/infrastructure/adapters/marketplace-registry-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ interface RegistryFile {
marketplaces: MarketplaceData[];
}

/** A registry file that exists but cannot be read as one is never read as an empty
* registry. `save()` reads this same list, appends to it and writes the whole file back, so
* a silent empty read would not merely hide the marketplaces a person registered - it would
* delete them on the very next write. A file that is simply absent is a different answer and
* keeps its own: no file, no marketplaces, nothing to lose. */
function unreadable(path: string, reason: string): Error {
return new Error(
`Cannot read the marketplace registry at ${path}: ${reason}. Repair the file, or ` +
`delete it to start from an empty registry.`
);
}

/** The registry's own list, or the reason the file cannot supply one. */
function registryEntries(raw: string, path: string): MarketplaceData[] {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (error) {
throw unreadable(path, error instanceof Error ? error.message : "it is not valid JSON");
}
const entries = (parsed as Partial<RegistryFile> | null)?.marketplaces;
if (!Array.isArray(entries)) throw unreadable(path, "it carries no \`marketplaces\` list");
return entries;
}

export class MarketplaceRegistryAdapter implements MarketplaceRegistry {
async list(projectRoot: string): Promise<readonly Marketplace[]> {
const project = await this.read(this.projectPath(projectRoot), "project");
Expand Down Expand Up @@ -85,8 +110,7 @@ export class MarketplaceRegistryAdapter implements MarketplaceRegistry {
} catch {
return [];
}
const parsed = JSON.parse(raw) as RegistryFile;
return parsed.marketplaces.map((m) => Marketplace.fromJSON({ ...m, scope }));
return registryEntries(raw, path).map((m) => Marketplace.fromJSON({ ...m, scope }));
}

private async write(path: string, entries: readonly Marketplace[]): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,37 @@ describe("MarketplaceRegistryAdapter", () => {
expect(result).toEqual([]);
});

// A registry file that exists but does not hold the list is not an empty registry, and
// reading it as one is how a person loses every marketplace they registered: `save()`
// reads this same list, appends to it and writes the whole file back, so one silent
// empty read turns into a file with one entry where there were five. Found on a real
// `~/.config/aidd/marketplaces.json` holding `{"version":1}`, which crashed
// `aidd marketplace list` with "Cannot read properties of undefined (reading 'map')" -
// a stack trace naming nothing a person can act on.
it("refuses a registry file that carries no list, naming the file rather than crashing", async () => {
const userFile = join(homeDir, ".config", "aidd", "marketplaces.json");
await mkdir(join(homeDir, ".config", "aidd"), { recursive: true });
await writeFile(userFile, '{"version":1}', "utf-8");

await expect(adapter.list(projectRoot)).rejects.toThrow(userFile);
});

it("refuses a registry file that is not JSON at all, naming the file", async () => {
const userFile = join(homeDir, ".config", "aidd", "marketplaces.json");
await mkdir(join(homeDir, ".config", "aidd"), { recursive: true });
await writeFile(userFile, "not json", "utf-8");

await expect(adapter.list(projectRoot)).rejects.toThrow(userFile);
});

it("reads a registry file whose list is present and empty as an empty registry", async () => {
const userFile = join(homeDir, ".config", "aidd", "marketplaces.json");
await mkdir(join(homeDir, ".config", "aidd"), { recursive: true });
await writeFile(userFile, '{"version":1,"marketplaces":[]}', "utf-8");

await expect(adapter.list(projectRoot)).resolves.toEqual([]);
});

it("returns project entries first, user entries after", async () => {
const project = Marketplace.fromJSON(baseData({ name: "p1" }));
const user = Marketplace.fromJSON(baseData({ name: "u1", scope: "user" }));
Expand Down