name a published extension for itself, not "extension.zip"
extension.zip says nothing once it is in a Downloads folder — not what it is, not which server it came from, and not which of two it is. The name comes from the extension's own manifest.json, which already carries what a browser will show: "Officer Browser Relay" becomes officer-browser-relay.zip. Generic, since extensions are meant to be a marketplace of them: any plugin shipping extension/ gets a file named after its own manifest, falling back to <appName>-extension.zip when that cannot be read — still better than extension.zip, and it never throws, because a broken extension manifest should fail the install for its own reason rather than because a file could not be named. Uninstall removes both names. An install from earlier today published extension.zip, and leaving that behind would serve a stale extension for a plugin that is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
deleteScreenForAllUsers,
|
||||
getDockPaths,
|
||||
@@ -20,7 +21,7 @@ import { publishAffordances, unpublishAffordances } from '../affordances/publish
|
||||
import { publishNotifications, unpublishNotifications } from '../notifications/publish';
|
||||
import { publishSkills, unpublishSkills } from '../skills/publish';
|
||||
import { refreshPluginMounts, snapshotPlugins } from './mount';
|
||||
import { publishExtension, unpublishExtension } from './zip';
|
||||
import { publishExtension, unpublishExtension, extensionFileName } from './zip';
|
||||
import { generatePluginSchemas, pushPluginSchema } from './schema';
|
||||
import { declaredTables, dropTables, existingTables } from './tables';
|
||||
import type { OnLog } from './run-logged';
|
||||
@@ -397,7 +398,8 @@ export async function installPlugin(
|
||||
// A browser extension, zipped from `extension/` and served UNAUTHENTICATED — see zip.ts. Built here
|
||||
// rather than committed, so it cannot fall behind the directory it is made from.
|
||||
if (await publishExtension(appName, plugin.dir).catch(() => false)) {
|
||||
await step(steps, onStep, `extension: published to /plugins/${appName}/extension.zip`);
|
||||
const file = extensionFileName(appName, join(plugin.dir, 'extension'));
|
||||
await step(steps, onStep, `extension: published to /plugins/${appName}/${file}`);
|
||||
}
|
||||
|
||||
// What this plugin needs an AGENT to know about itself. A plugin is a new surface arriving at
|
||||
@@ -529,7 +531,7 @@ export async function uninstallPlugin(appName: string, onStep?: OnStep, onLog?:
|
||||
}
|
||||
|
||||
await unpublishAssets(appName);
|
||||
await unpublishExtension(appName).catch(() => {});
|
||||
await unpublishExtension(appName, plugin?.dir).catch(() => {});
|
||||
// Same reasoning: a copy, with the original still in the plugin's directory. Removed so an agent stops
|
||||
// being told about a surface that is no longer there — an affordance for an uninstalled plugin is
|
||||
// worse than none, because it reads as true.
|
||||
|
||||
@@ -146,11 +146,41 @@ export async function publishExtension(appName: string, pluginDir: string): Prom
|
||||
|
||||
const target = join(process.cwd(), 'public', 'plugins', appName);
|
||||
mkdirSync(target, { recursive: true });
|
||||
await Bun.write(join(target, 'extension.zip'), zipDirectory(source));
|
||||
await Bun.write(join(target, extensionFileName(appName, source)), zipDirectory(source));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the download is called, from the extension's OWN name.
|
||||
*
|
||||
* `extension.zip` is what this shipped as first, and it is the wrong name the moment it reaches a
|
||||
* Downloads folder: it says nothing about what it is or which server it came from. The extension's
|
||||
* `manifest.json` already carries the name a browser will show — "Officer Browser Relay" — so the file is
|
||||
* named for that and a person can tell two of them apart.
|
||||
*
|
||||
* Falls back to `<appName>-extension.zip` when the manifest cannot be read, which is still better than
|
||||
* `extension.zip` and never throws: a plugin that ships a broken extension manifest should fail at
|
||||
* install for that reason, not because the zip could not be named.
|
||||
*/
|
||||
export function extensionFileName(appName: string, extensionDir: string): string {
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(join(extensionDir, 'manifest.json'), 'utf8')) as { name?: string };
|
||||
const slug = String(raw.name ?? '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
if (slug) return `${slug}.zip`;
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return `${appName}-extension.zip`;
|
||||
}
|
||||
|
||||
/** Remove it when the plugin goes. A copy, like the assets beside it — the source is in the plugin. */
|
||||
export async function unpublishExtension(appName: string): Promise<void> {
|
||||
rmSync(join(process.cwd(), 'public', 'plugins', appName, 'extension.zip'), { force: true });
|
||||
export async function unpublishExtension(appName: string, pluginDir?: string): Promise<void> {
|
||||
const dir = join(process.cwd(), 'public', 'plugins', appName);
|
||||
// Both names: an install from before the naming change published `extension.zip`, and leaving that
|
||||
// behind would serve a stale copy of an uninstalled plugin's extension for ever.
|
||||
rmSync(join(dir, 'extension.zip'), { force: true });
|
||||
if (pluginDir) rmSync(join(dir, extensionFileName(appName, join(pluginDir, 'extension'))), { force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user