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
7 changes: 5 additions & 2 deletions docs/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ is the `registry/dist/` folder of the SDK's repository served as static JSON.
When that host cannot be reached, `lousho add` reads the same committed folder
from GitHub instead: the release tag of the installed SDK version
(`https://raw.githubusercontent.com/LinuxDevil/agent-sdk/<version>/registry/dist/index.json`),
then the same tag with a `v` prefix (older releases), then `main`;
only the default falls back, never a registry you named.
then the same tag with a `v` prefix (older releases), then `main`.
If GitHub cannot be reached either (offline, an outage, a version whose tag was
never pushed), `lousho add` reads the copy of `registry/dist/` that ships inside
the installed package and prints a one-line notice that this bundled registry
may be older than the online one. Only the default falls back, never a registry you named.
`--registry` and `LOUSHO_REGISTRY` point it at another registry, and the value
`none` (in either place) disables the registry entirely — `lousho add` then
fails with `LOUSHO_CONFIG_INVALID`, for offline or locked-down use.
Expand Down
7 changes: 5 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15777,8 +15777,11 @@ is the `registry/dist/` folder of the SDK's repository served as static JSON.
When that host cannot be reached, `lousho add` reads the same committed folder
from GitHub instead: the release tag of the installed SDK version
(`https://raw.githubusercontent.com/LinuxDevil/agent-sdk/<version>/registry/dist/index.json`),
then the same tag with a `v` prefix (older releases), then `main`;
only the default falls back, never a registry you named.
then the same tag with a `v` prefix (older releases), then `main`.
If GitHub cannot be reached either (offline, an outage, a version whose tag was
never pushed), `lousho add` reads the copy of `registry/dist/` that ships inside
the installed package and prints a one-line notice that this bundled registry
may be older than the online one. Only the default falls back, never a registry you named.
`--registry` and `LOUSHO_REGISTRY` point it at another registry, and the value
`none` (in either place) disables the registry entirely — `lousho add` then
fails with `LOUSHO_CONFIG_INVALID`, for offline or locked-down use.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
"!src/**/__snapshots__",
"!src/**/__cassettes__",
"bin",
"registry/dist",
"apps/agent-forge/package.json",
"apps/agent-forge/dist",
"apps/agent-forge/dist-server",
Expand Down
23 changes: 17 additions & 6 deletions src/cli/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,23 @@ describe('lousho add', () => {
expect(explicit.code).toBe(1);
expect(explicit.err).toContain('LOUSHO_REGISTRY_UNREACHABLE');

// Everything down: one error naming every URL tried.
const down = await add(['--list'], { fetch: stubWhere(undefined, []) });
expect(down.code).toBe(1);
expect(down.err).toContain('the default registry is unreachable');
for (const url of [DEFAULT_REGISTRY, ...DEFAULT_REGISTRY_FALLBACKS]) expect(down.err).toContain(url);
expect(down.err).toContain('--registry');
// Everything down: the registry bundled in the package answers (index and item), with a notice.
const seenDown: string[] = [];
const down = await add(['--list'], { fetch: stubWhere(undefined, seenDown) });
expect(down.code).toBe(0);
expect(seenDown).toEqual([DEFAULT_REGISTRY, ...DEFAULT_REGISTRY_FALLBACKS]);
expect(down.err).toContain('using the registry bundled with this lousho version');
expect(down.out).toContain('changelog');
const offline = await add(['code-review', '--yes', '--dir', 'agent'], { fetch: stubWhere(undefined, []) });
expect(offline.code).toBe(0);
expect(offline.out).toContain('Added code-review');
expect(readReceipt().items['code-review'].registry).toBe(path.join(dist, 'index.json'));

// An explicit registry still fails when it is down, with no bundled fallback.
const explicitDown = await add(['--list', '--registry', 'https://example.invalid/index.json'], { fetch: stubWhere(undefined, []) });
expect(explicitDown.code).toBe(1);
expect(explicitDown.err).toContain('LOUSHO_REGISTRY_UNREACHABLE');
expect(explicitDown.err).not.toContain('bundled');
});

it('suggests the closest name for an unknown item', async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/cli/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ async function install(args: AddArgs, registry: string, item: RegistryItem, io:
}

async function runParsed(args: AddArgs, io: AddIo): Promise<number> {
const { registry, index } = await openRegistry({ registry: args.registry, env: io.env, fetch: io.fetch });
const { registry, index } = await openRegistry({ registry: args.registry, env: io.env, fetch: io.fetch, onNotice: (message) => io.stderr.write(`${message}
`) });
if (args.list || !args.name) {
io.stdout.write(`${listLines(index).join('\n')}\n`);
return 0;
Expand Down
63 changes: 48 additions & 15 deletions src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
* document per item, at a URL or a local path. Reading is all this does; nothing
* from a registry is executed or imported. See docs/registry.md.
*/
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import * as path from 'node:path';
import { z } from 'zod';
import { findSdkRoot } from '../deploy/bundle';
import { SDKError } from '../execution/errors';
import { isHostPattern } from '../security/hostPattern';
import { closestMatch } from '../utils/closestMatch';
Expand Down Expand Up @@ -112,6 +114,8 @@ export interface RegistryOptions {
registry?: string;
env?: Record<string, string | undefined>;
fetch?: typeof fetch;
/** Called with a one-line notice when the default registry is served from the copy bundled in the package. */
onNotice?: (message: string) => void;
}

const isHttp = (source: string): boolean => /^https?:\/\//i.test(source);
Expand Down Expand Up @@ -170,6 +174,16 @@ function registrySource(options: RegistryOptions): string {
return source || DEFAULT_REGISTRY;
}

/** The `registry/dist/index.json` shipped inside the installed package, or undefined when this copy has none (e.g. a trimmed install). */
function bundledIndexPath(): string | undefined {
try {
const file = path.join(findSdkRoot(), 'registry', 'dist', 'index.json');
return existsSync(file) ? file : undefined;
} catch {
return undefined;
}
}

async function loadIndex(registry: string, options: RegistryOptions): Promise<RegistryIndex> {
return readJson(registry, IndexSchema, options);
}
Expand All @@ -178,30 +192,49 @@ async function loadIndex(registry: string, options: RegistryOptions): Promise<Re
* Loads the index of the configured registry (see `registrySource`) and
* returns it with the location that answered, which item references resolve
* against. The default registry falls back to `DEFAULT_REGISTRY_FALLBACKS`
* when its host is unreachable.
* when its host is unreachable, and then to the `registry/dist/` copy bundled
* in the package (announced through `onNotice`).
*/
export async function openRegistry(options: RegistryOptions): Promise<{ registry: string; index: RegistryIndex }> {
const registry = registrySource(options);
try {
return { registry, index: await loadIndex(registry, options) };
} catch (error) {
if (registry !== DEFAULT_REGISTRY || !(error instanceof SDKError) || error.code !== 'LOUSHO_REGISTRY_UNREACHABLE') throw error;
const reason = (e: SDKError) => e.detail.replace(/^lousho add: /, '');
const failures: string[] = [];
for (const fallback of DEFAULT_REGISTRY_FALLBACKS) {
try {
return { registry: fallback, index: await loadIndex(fallback, options) };
} catch (fallbackError) {
if (!(fallbackError instanceof SDKError) || fallbackError.code !== 'LOUSHO_REGISTRY_UNREACHABLE') throw fallbackError;
failures.push(reason(fallbackError));
}
return openDefaultFallback(error, options);
}
}

const reasonOf = (error: SDKError): string => error.detail.replace(/^lousho add: /, '');

/** The default registry's host is down: try each GitHub fallback, then the copy bundled in the package. */
async function openDefaultFallback(unreachableError: SDKError, options: RegistryOptions): Promise<{ registry: string; index: RegistryIndex }> {
const failures: string[] = [];
for (const fallback of DEFAULT_REGISTRY_FALLBACKS) {
try {
return { registry: fallback, index: await loadIndex(fallback, options) };
} catch (fallbackError) {
if (!(fallbackError instanceof SDKError) || fallbackError.code !== 'LOUSHO_REGISTRY_UNREACHABLE') throw fallbackError;
failures.push(reasonOf(fallbackError));
}
}
const bundled = bundledIndexPath();
if (bundled) {
try {
const index = await loadIndex(bundled, options);
options.onNotice?.(
`lousho add: the default registry is unreachable; using the registry bundled with this lousho version (${VERSION}), which may be older than the online one.`
);
return { registry: bundled, index };
} catch {
// an unreadable bundled copy is the same as none: report the network failure below
}
throw new SDKError(
`lousho add: the default registry is unreachable: ${reason(error)}; and its fallbacks: ${failures.join('; ')}`,
'LOUSHO_REGISTRY_UNREACHABLE',
{ cause: error, hint: 'Check that you are online, or pass --registry <url-or-path> (e.g. a local copy of registry/dist/index.json).' }
);
}
throw new SDKError(
`lousho add: the default registry is unreachable: ${reasonOf(unreachableError)}; and its fallbacks: ${failures.join('; ')}`,
'LOUSHO_REGISTRY_UNREACHABLE',
{ cause: unreachableError, hint: 'Check that you are online, or pass --registry <url-or-path> (e.g. a local copy of registry/dist/index.json).' }
);
}

/** Resolves an index entry's `url` / `path` against the registry's own location. */
Expand Down
Loading