plugnz 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugnz.mjs +20000 -0
- package/package.json +13 -10
- package/bin/plgnz.mjs +0 -3
- package/bin/plugnz.mjs +0 -5
- package/src/cli.ts +0 -489
- package/src/compatibility.ts +0 -23
- package/src/consumer-profiles.ts +0 -77
- package/src/conversion.ts +0 -544
- package/src/doctor.ts +0 -277
- package/src/exec.ts +0 -91
- package/src/fingerprint.ts +0 -77
- package/src/globals.d.ts +0 -97
- package/src/hermes-identity.ts +0 -4
- package/src/host.ts +0 -131
- package/src/hosts/claude-code-writer.ts +0 -589
- package/src/hosts/claude-code.ts +0 -124
- package/src/hosts/codex-writer.ts +0 -418
- package/src/hosts/codex.ts +0 -183
- package/src/hosts/cursor-writer.ts +0 -279
- package/src/hosts/cursor.ts +0 -107
- package/src/hosts/dcode-writer.ts +0 -150
- package/src/hosts/dcode.ts +0 -74
- package/src/hosts/grok-writer.ts +0 -254
- package/src/hosts/grok.ts +0 -94
- package/src/hosts/hermes-writer.ts +0 -223
- package/src/hosts/hermes.ts +0 -88
- package/src/hosts/index.ts +0 -25
- package/src/hosts/kimi-writer.ts +0 -238
- package/src/hosts/kimi.ts +0 -140
- package/src/hosts/omp-writer.ts +0 -179
- package/src/hosts/omp.ts +0 -153
- package/src/hosts/opencode-writer.ts +0 -112
- package/src/hosts/opencode.ts +0 -38
- package/src/hosts/pi-writer.ts +0 -111
- package/src/hosts/pi.ts +0 -48
- package/src/hosts/writers.ts +0 -23
- package/src/hosts/zcode-cli-writer.ts +0 -239
- package/src/hosts/zcode-cli.ts +0 -195
- package/src/mcp-write.ts +0 -81
- package/src/mcp.ts +0 -161
- package/src/paths.ts +0 -101
- package/src/pin.ts +0 -135
- package/src/source.ts +0 -224
- package/src/state-write.ts +0 -25
- package/src/state.ts +0 -118
- package/src/update.ts +0 -230
package/src/hosts/kimi.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* kimi host reader.
|
|
3
|
-
*
|
|
4
|
-
* Reader only — the writer (add/pin/remove) lives in
|
|
5
|
-
* src/hosts/kimi-writer.ts so loading a reader never evaluates writer code
|
|
6
|
-
* (AGENTS.md: doctor is read-only by construction).
|
|
7
|
-
*
|
|
8
|
-
* Real store layout (measured 2026-09-16, evidence in docs/hosts/kimi.md):
|
|
9
|
-
* ~/.kimi-code/plugins/installed.json — {version:1, plugins:[{id, root, source,
|
|
10
|
-
* enabled, installedAt, updatedAt, originalSource}]}
|
|
11
|
-
* ~/.kimi-code/plugins/managed/<id>/ — install dirs (`.kimi-plugin/plugin.json`
|
|
12
|
-
* native manifest with inline `mcpServers`,
|
|
13
|
-
* plus spec `.plugin/plugin.json` and
|
|
14
|
-
* `.mcp.json`/`mcp.json` copies)
|
|
15
|
-
*
|
|
16
|
-
* User-level MCP would be `[mcp_servers.<name>]` in ~/.kimi-code/config.toml
|
|
17
|
-
* (kimi is a TOML-config host like codex). None exist on this machine as of
|
|
18
|
-
* 2026-09-16, so that half of the reader is defensive and its shadow
|
|
19
|
-
* semantics are unverified — see docs/hosts/kimi.md.
|
|
20
|
-
*/
|
|
21
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import { parse as parseToml } from 'smol-toml';
|
|
24
|
-
import type { HostReader, InstalledPlugin, McpServerEntry } from '../host';
|
|
25
|
-
import { kimiRoot } from '../paths';
|
|
26
|
-
import { collectPluginServers, readJson, type PluginMcpCandidate, type RawServerDef } from '../mcp';
|
|
27
|
-
|
|
28
|
-
declare const Bun: any;
|
|
29
|
-
declare const TextDecoder: any;
|
|
30
|
-
|
|
31
|
-
export function pluginsDir(): string {
|
|
32
|
-
return join(kimiRoot(), 'plugins');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Where a plugin declares MCP servers. The native manifest carries them inline
|
|
37
|
-
* (measured), ahead of the spec copies `npx plugins` also dereferences.
|
|
38
|
-
*/
|
|
39
|
-
export function mcpCandidates(): PluginMcpCandidate[] {
|
|
40
|
-
return [
|
|
41
|
-
{ kind: 'inline', manifest: 'kimi.plugin.json' },
|
|
42
|
-
{ kind: 'inline', manifest: join('.kimi-plugin', 'plugin.json') },
|
|
43
|
-
{ kind: 'spec', file: '.mcp.json' },
|
|
44
|
-
{ kind: 'spec', file: 'mcp.json' },
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function configFile(): string {
|
|
49
|
-
return join(kimiRoot(), 'config.toml');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const kimi: HostReader = {
|
|
53
|
-
id: 'kimi',
|
|
54
|
-
gui: false,
|
|
55
|
-
|
|
56
|
-
detect(): boolean {
|
|
57
|
-
return existsSync(kimiRoot()) || explicitCurrentKimiBinary();
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
stores(): string[] {
|
|
61
|
-
return [join(pluginsDir(), 'managed')];
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
listInstalled(): InstalledPlugin[] {
|
|
65
|
-
const root = readJson(join(pluginsDir(), 'installed.json'));
|
|
66
|
-
if (root === null) return [];
|
|
67
|
-
const plugins = root['plugins'];
|
|
68
|
-
if (!Array.isArray(plugins)) return [];
|
|
69
|
-
const out: InstalledPlugin[] = [];
|
|
70
|
-
for (const raw of plugins) {
|
|
71
|
-
if (typeof raw !== 'object' || raw === null) continue;
|
|
72
|
-
const rec = raw as Record<string, unknown>;
|
|
73
|
-
const id = typeof rec['id'] === 'string' ? rec['id'] : null;
|
|
74
|
-
if (id === null) continue;
|
|
75
|
-
const path = typeof rec['root'] === 'string' ? rec['root'] : undefined;
|
|
76
|
-
const marker = path === undefined ? null : readJson(join(path, '.plgnz-install.json'));
|
|
77
|
-
const markerId = marker?.['pluginId'];
|
|
78
|
-
const ownedId = typeof markerId === 'string' && (markerId === id || new RegExp(`^${escapeRegExp(id)}@[a-z0-9][a-z0-9._-]*$`, 'iu').test(markerId)) ? markerId : id;
|
|
79
|
-
const at = ownedId.indexOf('@');
|
|
80
|
-
const plugin: InstalledPlugin = { id: ownedId, name: at < 0 ? ownedId : ownedId.slice(0, at) };
|
|
81
|
-
if (at >= 0) plugin.marketplace = ownedId.slice(at + 1);
|
|
82
|
-
if (path !== undefined && existsSync(path)) plugin.path = path;
|
|
83
|
-
if (rec['enabled'] === false) plugin.enabled = false;
|
|
84
|
-
out.push(plugin);
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
mcpEntries(): McpServerEntry[] {
|
|
90
|
-
const entries: McpServerEntry[] = [];
|
|
91
|
-
if (existsSync(configFile())) {
|
|
92
|
-
try {
|
|
93
|
-
const parsed: unknown = parseToml(readFileSync(configFile(), 'utf8'));
|
|
94
|
-
const config = typeof parsed === 'object' && parsed !== null ? (parsed as Record<string, unknown>) : null;
|
|
95
|
-
const servers =
|
|
96
|
-
config !== null && typeof config['mcp_servers'] === 'object' && config['mcp_servers'] !== null
|
|
97
|
-
? (config['mcp_servers'] as Record<string, unknown>)
|
|
98
|
-
: null;
|
|
99
|
-
if (servers !== null) {
|
|
100
|
-
for (const [name, value] of Object.entries(servers)) {
|
|
101
|
-
if (typeof value !== 'object' || value === null) continue;
|
|
102
|
-
const def = value as RawServerDef;
|
|
103
|
-
const transport =
|
|
104
|
-
typeof def['url'] === 'string' ? 'http' : typeof def['command'] === 'string' ? 'stdio' : null;
|
|
105
|
-
if (transport === null) continue;
|
|
106
|
-
const entry: McpServerEntry = { name, transport, origin: 'user', file: configFile(), baseDir: kimiRoot() };
|
|
107
|
-
const command = def['command'];
|
|
108
|
-
if (typeof command === 'string') entry.command = command;
|
|
109
|
-
const rawArgs = def['args'];
|
|
110
|
-
if (Array.isArray(rawArgs)) entry.args = rawArgs.filter((a): a is string => typeof a === 'string');
|
|
111
|
-
if (def['enabled'] === false) entry.enabled = false;
|
|
112
|
-
entries.push(entry);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch {
|
|
116
|
-
// malformed config.toml — treated as no user-level MCP entries
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
for (const plugin of this.listInstalled()) {
|
|
120
|
-
if (plugin.path === undefined || !existsSync(plugin.path)) continue;
|
|
121
|
-
entries.push(...collectPluginServers(plugin.id, plugin.path, mcpCandidates()));
|
|
122
|
-
}
|
|
123
|
-
return entries;
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
/** An explicit current native binary is sufficient presence evidence; its writer initializes the selected root. */
|
|
128
|
-
function explicitCurrentKimiBinary(): boolean {
|
|
129
|
-
const binary = process.env['OPEN_PLUGIN_KIMI_BIN'];
|
|
130
|
-
if (!binary || !existsSync(binary)) return false;
|
|
131
|
-
try {
|
|
132
|
-
const result = Bun.spawnSync([binary, '--version'], { stdout: 'pipe', stderr: 'pipe', timeout: 10_000 });
|
|
133
|
-
if (result.exitCode !== 0 || !(result.stdout instanceof Uint8Array)) return false;
|
|
134
|
-
const version = new TextDecoder().decode(result.stdout).trim();
|
|
135
|
-
const match = /^(\d+)\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.exec(version);
|
|
136
|
-
return match !== null && Number(match[1]) > 0;
|
|
137
|
-
} catch { return false; }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); }
|
package/src/hosts/omp-writer.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/** Transactional writer for OMP's native npm/link extension-package lane. */
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import type { AddOptions, HostWriter, InstalledPlugin, PinOptions, PinOutcome } from '../host';
|
|
6
|
-
import type { PluginSource, ResolvedSource } from '../source';
|
|
7
|
-
import { projectPluginForOmp } from '../conversion';
|
|
8
|
-
import { omp, mcpCandidates, pluginsDir } from './omp';
|
|
9
|
-
import { pinPluginMcpFiles } from '../mcp-write';
|
|
10
|
-
|
|
11
|
-
const MARKER = '.plgnz-install.json';
|
|
12
|
-
const MANAGED = 'plgnz';
|
|
13
|
-
type Ownership = { source: string; pluginId: string; fingerprint: string; packageName: string };
|
|
14
|
-
type Doc = Record<string, unknown>;
|
|
15
|
-
|
|
16
|
-
declare const TextEncoder: { new (): { encode(input?: string): Uint8Array } };
|
|
17
|
-
|
|
18
|
-
export const ompWriter: HostWriter = {
|
|
19
|
-
...omp,
|
|
20
|
-
supportsAdoption: true,
|
|
21
|
-
async add(plugin: PluginSource, resolved: ResolvedSource, opts?: AddOptions): Promise<void | 'unchanged'> {
|
|
22
|
-
const market = plugin.marketplace ?? 'local'; const id = `${plugin.name}@${market}`;
|
|
23
|
-
assertIdentity(market, 'marketplace'); assertIdentity(plugin.name, 'plugin');
|
|
24
|
-
const packageName = nativePackageName(market, plugin.name); const version = plugin.version ?? resolved.sha;
|
|
25
|
-
const managed = join(pluginsDir(), MANAGED); const target = join(managed, nativeSlug(market, plugin.name));
|
|
26
|
-
const link = join(pluginsDir(), 'node_modules', '@plgnz', nativeSlug(market, plugin.name));
|
|
27
|
-
const lockFile = join(pluginsDir(), 'omp-plugins.lock.json'); const registryFile = join(pluginsDir(), 'installed_plugins.json');
|
|
28
|
-
const lock = readDoc(lockFile, { plugins: {}, settings: {} }, 'lockfile'); const lockPlugins = object(lock.plugins, 'lockfile plugins');
|
|
29
|
-
const registry = readDoc(registryFile, { version: 2, plugins: {} }, 'registry'); const registryPlugins = object(registry.plugins, 'registry plugins');
|
|
30
|
-
const rows = userRows(registryPlugins[id]);
|
|
31
|
-
if (rows.length > 0) validateLegacy(rows, id, plugin.name, version, opts?.adoptExisting === true);
|
|
32
|
-
const owner = ownership(target);
|
|
33
|
-
if (owner === null && lstatExists(target)) throw new Error(`OMP managed package slot is unowned; refusing to replace it: ${target}`);
|
|
34
|
-
if (owner !== null && (owner.source !== resolved.sourceUri || owner.pluginId !== id || owner.packageName !== packageName)) throw new Error(`OMP plugin ${id} belongs to another source; refusing to replace it`);
|
|
35
|
-
assertOwnedLink(link, target, owner !== null);
|
|
36
|
-
|
|
37
|
-
const stage = mkdtempSync(join(tmpdir(), '.plgnz-omp-package-'));
|
|
38
|
-
try {
|
|
39
|
-
projectPluginForOmp(plugin.dir, stage, { packageName, version, activeRoot: target, namespace: plugin.name });
|
|
40
|
-
writeFileSync(join(stage, MARKER), JSON.stringify({ source: resolved.sourceUri, pluginId: id, fingerprint: plugin.contentFingerprint ?? '', packageName } satisfies Ownership));
|
|
41
|
-
const unchanged = owner !== null && owner.fingerprint === (plugin.contentFingerprint ?? '') && sameTree(stage, target) && linkPointsTo(link, target) && lockEnabled(lockPlugins[packageName]);
|
|
42
|
-
if (opts?.dryRun) return unchanged ? 'unchanged' : undefined;
|
|
43
|
-
if (unchanged && rows.length === 0) return 'unchanged';
|
|
44
|
-
|
|
45
|
-
mkdirSafe(managed); mkdirSafe(dirname(link));
|
|
46
|
-
const beforeLock = snapshot(lockFile); const beforeRegistry = snapshot(registryFile);
|
|
47
|
-
let active: Move | null = null; let previousLink: Move | null = null; let createdLink = false;
|
|
48
|
-
try {
|
|
49
|
-
if (!unchanged) active = activate(stage, target);
|
|
50
|
-
if (!unchanged) previousLink = moveExisting(link);
|
|
51
|
-
if (!unchanged) { symlinkSync(target, link, 'dir'); createdLink = true; }
|
|
52
|
-
const nextLockPlugins = { ...lockPlugins, [packageName]: { ...entry(lockPlugins[packageName]), version, enabledFeatures: null, enabled: true } };
|
|
53
|
-
const retained = nonUserRows(registryPlugins[id]);
|
|
54
|
-
if (retained.length === 0) delete nextLockPlugins[plugin.name];
|
|
55
|
-
const nextRegistryPlugins = { ...registryPlugins };
|
|
56
|
-
if (retained.length > 0) nextRegistryPlugins[id] = retained; else delete nextRegistryPlugins[id];
|
|
57
|
-
writeDoc(lockFile, { ...lock, plugins: nextLockPlugins });
|
|
58
|
-
if (rows.length > 0) writeDoc(registryFile, { ...registry, plugins: nextRegistryPlugins });
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (createdLink) rmSync(link, { recursive: true, force: true }); previousLink?.rollback(); active?.rollback();
|
|
61
|
-
restore(lockFile, beforeLock); restore(registryFile, beforeRegistry); throw error;
|
|
62
|
-
}
|
|
63
|
-
previousLink?.commit(); active?.commit();
|
|
64
|
-
const retainedPaths = registryInstallPaths(nextRegistryPlugins(registryPlugins, id));
|
|
65
|
-
for (const row of rows) cleanupLegacy(row, plugin.name, retainedPaths);
|
|
66
|
-
return unchanged ? 'unchanged' : undefined;
|
|
67
|
-
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
68
|
-
},
|
|
69
|
-
async pin(plugin: InstalledPlugin, opts?: PinOptions): Promise<PinOutcome> {
|
|
70
|
-
if (plugin.path === undefined || !existsSync(plugin.path)) return { changes: [], refusals: [] };
|
|
71
|
-
return pinPluginMcpFiles(plugin.path, mcpCandidates(), opts);
|
|
72
|
-
},
|
|
73
|
-
async remove(id: string): Promise<void> {
|
|
74
|
-
const owned = findOwned(id); if (owned === null) throw new Error(`OMP plugin ${id} is not plgnz-owned; refusing to remove it`);
|
|
75
|
-
const lockFile = join(pluginsDir(), 'omp-plugins.lock.json'); const lock = readDoc(lockFile, { plugins: {}, settings: {} }, 'lockfile');
|
|
76
|
-
const lockPlugins = object(lock.plugins, 'lockfile plugins'); const link = join(pluginsDir(), 'node_modules', owned.owner.packageName);
|
|
77
|
-
if (!linkPointsTo(link, owned.path)) throw new Error(`OMP native link for ${id} is missing or redirected; refusing to remove it`);
|
|
78
|
-
const before = snapshot(lockFile); let movedRoot: Move | null = null; let movedLink: Move | null = null;
|
|
79
|
-
try { movedRoot = moveExisting(owned.path); movedLink = moveExisting(link); const next = { ...lockPlugins }; delete next[owned.owner.packageName]; writeDoc(lockFile, { ...lock, plugins: next }); }
|
|
80
|
-
catch (error) { movedLink?.rollback(); movedRoot?.rollback(); restore(lockFile, before); throw error; }
|
|
81
|
-
movedLink?.commit(); movedRoot?.commit();
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
function nativePackageName(market: string, plugin: string): string { return `@plgnz/${nativeSlug(market, plugin)}`; }
|
|
86
|
-
function nativeSlug(market: string, plugin: string): string { return `${hex(market)}-${hex(plugin)}`; }
|
|
87
|
-
function hex(value: string): string { return Array.from(new TextEncoder().encode(value), byte => byte.toString(16).padStart(2, '0')).join(''); }
|
|
88
|
-
function lockEnabled(value: unknown): boolean { return isDoc(value) && value.enabled === true; }
|
|
89
|
-
function entry(value: unknown): Doc { return isDoc(value) ? value : {}; }
|
|
90
|
-
|
|
91
|
-
function validateLegacy(rows: Array<Doc & { installPath?: unknown }>, id: string, name: string, version: string, adopt: boolean): void {
|
|
92
|
-
if (!adopt) throw new Error(`OMP marketplace install ${id} exists; pass --adopt-existing to migrate it to the native extension-package lane`);
|
|
93
|
-
const [marketplace] = id.split('@').slice(1); const cache = join(pluginsDir(), 'cache', 'plugins');
|
|
94
|
-
for (const row of rows) {
|
|
95
|
-
if (typeof row.installPath !== 'string' || typeof row.version !== 'string' || row.version !== version || marketplace === undefined) throw new Error(`OMP marketplace install ${id} has no safe native copy`);
|
|
96
|
-
const expected = join(cache, `${marketplace}___${name}___${version}`);
|
|
97
|
-
if (resolve(row.installPath) !== resolve(expected)) throw new Error(`OMP marketplace install ${id} is outside its approved cache slot`);
|
|
98
|
-
assertExistingDirectoryTree(cache, row.installPath, `OMP marketplace install ${id}`);
|
|
99
|
-
const manifest = readDoc(join(row.installPath, 'plugin.json'), {}, 'legacy plugin manifest');
|
|
100
|
-
if (manifest.name !== name || (typeof manifest.version === 'string' && manifest.version !== version)) throw new Error(`OMP marketplace install ${id} identity differs from the selected source`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function cleanupLegacy(row: Doc & { installPath?: unknown }, name: string, retainedPaths: Set<string>): void {
|
|
104
|
-
if (typeof row.installPath !== 'string' || retainedPaths.has(resolve(row.installPath))) return;
|
|
105
|
-
const legacyLink = join(pluginsDir(), 'node_modules', name);
|
|
106
|
-
if (linkPointsTo(legacyLink, row.installPath)) rmSync(legacyLink, { force: true });
|
|
107
|
-
rmSync(row.installPath, { recursive: true, force: true });
|
|
108
|
-
}
|
|
109
|
-
function nextRegistryPlugins(registry: Doc, id: string): Doc { const next = { ...registry }; const retained = nonUserRows(registry[id]); if (retained.length > 0) next[id] = retained; else delete next[id]; return next; }
|
|
110
|
-
function registryInstallPaths(registry: Doc): Set<string> { const paths = new Set<string>(); for (const value of Object.values(registry)) if (Array.isArray(value)) for (const row of value) if (isDoc(row) && typeof row.installPath === 'string') paths.add(resolve(row.installPath)); return paths; }
|
|
111
|
-
function findOwned(id: string): { path: string; owner: Ownership } | null {
|
|
112
|
-
const root = join(pluginsDir(), MANAGED); if (!existsSync(root)) return null;
|
|
113
|
-
for (const name of readdirSync(root)) { const path = join(root, name); const owner = ownership(path); if (owner?.pluginId === id) return { path, owner }; }
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
function assertOwnedLink(path: string, target: string, owned: boolean): void {
|
|
117
|
-
if (!lstatExists(path)) return;
|
|
118
|
-
if (!owned || !linkPointsTo(path, target)) throw new Error(`OMP native package slot is unowned or redirected: ${path}`);
|
|
119
|
-
}
|
|
120
|
-
function lstatExists(path: string): boolean { try { lstatSync(path); return true; } catch { return false; } }
|
|
121
|
-
function linkPointsTo(path: string, target: string): boolean { try { return lstatSync(path).isSymbolicLink() && resolve(dirname(path), readlinkSync(path)) === resolve(target); } catch { return false; } }
|
|
122
|
-
|
|
123
|
-
type Move = { commit(): void; rollback(): void };
|
|
124
|
-
function moveExisting(path: string): Move | null {
|
|
125
|
-
if (!lstatExists(path)) return null;
|
|
126
|
-
const root = mkdtempSync(join(dirname(path), '.plgnz-omp-backup-')); const backup = join(root, 'previous'); renameSync(path, backup);
|
|
127
|
-
return { commit: () => rmSync(root, { recursive: true, force: true }), rollback: () => { rmSync(path, { recursive: true, force: true }); renameSync(backup, path); rmSync(root, { recursive: true, force: true }); } };
|
|
128
|
-
}
|
|
129
|
-
function activate(stage: string, target: string): Move {
|
|
130
|
-
const old = moveExisting(target);
|
|
131
|
-
try {
|
|
132
|
-
if (process.env['OPEN_PLUGIN_TEST_OMP_ACTIVATION_FAILURE'] === 'after-old-move') throw new Error('forced OMP activation failure');
|
|
133
|
-
renameSync(stage, target);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
old?.rollback();
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
return { commit: () => old?.commit(), rollback: () => { rmSync(target, { recursive: true, force: true }); old?.rollback(); } };
|
|
139
|
-
}
|
|
140
|
-
function mkdirSafe(path: string): void {
|
|
141
|
-
const boundary = resolve(pluginsDir()); const target = resolve(path);
|
|
142
|
-
if (target !== boundary && !target.startsWith(`${boundary}/`)) throw new Error(`OMP managed path escapes the plugin store: ${target}`);
|
|
143
|
-
const parentBoundary = dirname(boundary);
|
|
144
|
-
for (let current = target; ; current = dirname(current)) {
|
|
145
|
-
if (lstatExists(current)) { const stat = lstatSync(current); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`OMP managed path is unsafe: ${current}`); }
|
|
146
|
-
if (current === parentBoundary) break;
|
|
147
|
-
}
|
|
148
|
-
mkdirSync(path, { recursive: true }); let current = target;
|
|
149
|
-
while (true) {
|
|
150
|
-
const stat = lstatSync(current);
|
|
151
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`OMP managed path is unsafe: ${current}`);
|
|
152
|
-
if (current === boundary) return;
|
|
153
|
-
current = dirname(current);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function assertExistingDirectoryTree(boundaryPath: string, targetPath: string, label: string): void {
|
|
157
|
-
const boundary = resolve(boundaryPath); const target = resolve(targetPath);
|
|
158
|
-
if (target !== boundary && !target.startsWith(`${boundary}/`)) throw new Error(`${label} escapes its approved cache root`);
|
|
159
|
-
for (let current = target; ; current = dirname(current)) {
|
|
160
|
-
if (!lstatExists(current)) throw new Error(`${label} has no safe native copy`);
|
|
161
|
-
const stat = lstatSync(current); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`${label} contains a symlink or non-directory component: ${current}`);
|
|
162
|
-
if (current === boundary) return;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function sameTree(left: string, right: string): boolean {
|
|
166
|
-
if (!existsSync(right)) return false; const bytes = readFileSync as unknown as (path: string) => Uint8Array;
|
|
167
|
-
const list = (root: string): string[] => { const out: string[] = []; const walk = (dir: string, prefix: string): void => { for (const name of readdirSync(dir).sort()) { if (name === MARKER) continue; const path = join(dir, name); const rel = prefix ? `${prefix}/${name}` : name; const stat = lstatSync(path); if (stat.isSymbolicLink()) throw new Error(`OMP managed plugin contains symlink: ${path}`); if (stat.isDirectory()) walk(path, rel); else if (stat.isFile()) out.push(`${rel}:${Array.from(bytes(path)).join(',')}`); else throw new Error(`OMP managed plugin contains unsupported file: ${path}`); } }; walk(root, ''); return out; };
|
|
168
|
-
return JSON.stringify(list(left)) === JSON.stringify(list(right));
|
|
169
|
-
}
|
|
170
|
-
function ownership(path: string): Ownership | null { const marker = join(path, MARKER); if (!existsSync(marker)) return null; const value = readDoc(marker, {}, 'ownership marker'); if (typeof value.source !== 'string' || typeof value.pluginId !== 'string' || typeof value.fingerprint !== 'string' || typeof value.packageName !== 'string') throw new Error(`invalid plgnz ownership marker: ${marker}`); return value as Ownership; }
|
|
171
|
-
function readDoc(path: string, fallback: Doc, label: string): Doc { if (!existsSync(path)) return fallback; try { const value: unknown = JSON.parse(readFileSync(path, 'utf8')); if (!isDoc(value)) throw new Error('expected object'); return value; } catch (error) { throw new Error(`invalid OMP ${label}: ${path} (${(error as Error).message})`); } }
|
|
172
|
-
function object(value: unknown, label: string): Doc { if (!isDoc(value)) throw new Error(`invalid OMP ${label}`); return value; }
|
|
173
|
-
function isDoc(value: unknown): value is Doc { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
174
|
-
function userRows(value: unknown): Array<Doc & { installPath?: unknown }> { return Array.isArray(value) ? value.filter((row): row is Doc & { installPath?: unknown } => isDoc(row) && row.scope === 'user') : []; }
|
|
175
|
-
function nonUserRows(value: unknown): unknown[] { return Array.isArray(value) ? value.filter(row => !isDoc(row) || row.scope !== 'user') : []; }
|
|
176
|
-
function writeDoc(path: string, value: unknown): void { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, JSON.stringify(value, null, 2)); }
|
|
177
|
-
function snapshot(path: string): string | undefined { return existsSync(path) ? readFileSync(path, 'utf8') : undefined; }
|
|
178
|
-
function restore(path: string, value: string | undefined): void { if (value === undefined) rmSync(path, { force: true }); else writeFileSync(path, value); }
|
|
179
|
-
function assertIdentity(value: string, label: string): void { if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(value)) throw new Error(`unsafe OMP ${label}: ${value}`); }
|
package/src/hosts/omp.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* omp host reader.
|
|
3
|
-
*
|
|
4
|
-
* Reader only — the writer (add/pin/remove) lives in
|
|
5
|
-
* src/hosts/omp-writer.ts so loading a reader never evaluates writer code
|
|
6
|
-
* (AGENTS.md: doctor is read-only by construction).
|
|
7
|
-
*
|
|
8
|
-
* Real store layout (measured 2026-09-16, evidence in docs/hosts/omp.md):
|
|
9
|
-
* ~/.omp/plugins/installed_plugins.json — claude-code-shaped registry:
|
|
10
|
-
* {version:2, plugins:{"<name>@<marketplace>":
|
|
11
|
-
* [{scope, installPath, version, …}]}
|
|
12
|
-
* ~/.omp/plugins/cache/plugins/<marketplace>___<name>___<version>/ — install dirs
|
|
13
|
-
* (flat triple-underscore keys, unlike
|
|
14
|
-
* claude-code's nested cache layout)
|
|
15
|
-
* ~/.omp/plugins/omp-plugins.lock.json — {plugins:{<name>:{version, enabled, …}}}
|
|
16
|
-
* ~/.omp/plugins/node_modules/<name> — symlinks into the cache
|
|
17
|
-
* ~/.omp/marketplaces.json — {marketplaces:[{name, sourceType, sourceUri, catalogPath}]}
|
|
18
|
-
*
|
|
19
|
-
* omp is a native plugin host with its own store (AGENTS.md hosts list; ground
|
|
20
|
-
* truth "Correction", 2026-09-16) — never routed through a repo-root
|
|
21
|
-
* `.mcp.json`, which is the Claude Code project convention, not omp's plugin
|
|
22
|
-
* surface. omp has no measured user-level MCP config (no ~/.omp/mcp.json on
|
|
23
|
-
* this machine), so doctor's MCP surface is the installed plugins'
|
|
24
|
-
* mcp.json/.mcp.json alone; the shadow check therefore has no user side to
|
|
25
|
-
* trip on until such a config is verified to exist.
|
|
26
|
-
*/
|
|
27
|
-
import { existsSync, lstatSync, readdirSync, readlinkSync } from 'node:fs';
|
|
28
|
-
import { dirname, join, resolve } from 'node:path';
|
|
29
|
-
import type { HostReader, InstalledPlugin, McpServerEntry } from '../host';
|
|
30
|
-
import { ompRoot } from '../paths';
|
|
31
|
-
import { collectPluginServers, readJson, type PluginMcpCandidate } from '../mcp';
|
|
32
|
-
|
|
33
|
-
export function pluginsDir(): string {
|
|
34
|
-
return join(ompRoot(), 'plugins');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Where a plugin copy declares MCP servers (spec `mcp.json`, plus the `.mcp.json` twin). */
|
|
38
|
-
export function mcpCandidates(): PluginMcpCandidate[] {
|
|
39
|
-
return [
|
|
40
|
-
{ kind: 'spec', file: '.mcp.json' },
|
|
41
|
-
{ kind: 'spec', file: 'mcp.json' },
|
|
42
|
-
];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** enabled state from omp-plugins.lock.json, keyed by bare plugin name. */
|
|
46
|
-
function lockEnabled(name: string): boolean | undefined {
|
|
47
|
-
const root = readJson(join(pluginsDir(), 'omp-plugins.lock.json'));
|
|
48
|
-
if (root === null) return undefined;
|
|
49
|
-
const plugins = root['plugins'];
|
|
50
|
-
if (typeof plugins !== 'object' || plugins === null || Array.isArray(plugins)) return undefined;
|
|
51
|
-
const entry = (plugins as Record<string, unknown>)[name];
|
|
52
|
-
if (typeof entry !== 'object' || entry === null) return undefined;
|
|
53
|
-
return (entry as Record<string, unknown>)['enabled'] === false ? false : undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type ManagedMarker = {
|
|
57
|
-
pluginId: string;
|
|
58
|
-
packageName: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
function managedMarker(path: string): ManagedMarker | null {
|
|
62
|
-
const value = readJson(join(path, '.plgnz-install.json'));
|
|
63
|
-
if (value === null || typeof value['pluginId'] !== 'string' || typeof value['packageName'] !== 'string') return null;
|
|
64
|
-
return { pluginId: value['pluginId'], packageName: value['packageName'] };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function managedInstalled(): InstalledPlugin[] {
|
|
68
|
-
const managed = join(pluginsDir(), 'plgnz');
|
|
69
|
-
if (!existsSync(managed)) return [];
|
|
70
|
-
const out: InstalledPlugin[] = [];
|
|
71
|
-
for (const entry of readdirSync(managed).sort()) {
|
|
72
|
-
const path = join(managed, entry);
|
|
73
|
-
if (!lstatSync(path).isDirectory()) continue;
|
|
74
|
-
const marker = managedMarker(path);
|
|
75
|
-
const manifest = readJson(join(path, 'package.json'));
|
|
76
|
-
if (marker === null || manifest === null || manifest['name'] !== marker.packageName) continue;
|
|
77
|
-
const link = join(pluginsDir(), 'node_modules', marker.packageName);
|
|
78
|
-
try {
|
|
79
|
-
if (!lstatSync(link).isSymbolicLink() || resolve(dirname(link), readlinkSync(link)) !== resolve(path)) continue;
|
|
80
|
-
} catch {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const at = marker.pluginId.indexOf('@');
|
|
84
|
-
const name = at === -1 ? marker.pluginId : marker.pluginId.slice(0, at);
|
|
85
|
-
const marketplace = at === -1 ? undefined : marker.pluginId.slice(at + 1);
|
|
86
|
-
const plugin: InstalledPlugin = { id: marker.pluginId, name, path };
|
|
87
|
-
if (marketplace !== undefined) plugin.marketplace = marketplace;
|
|
88
|
-
if (typeof manifest['version'] === 'string') plugin.version = manifest['version'];
|
|
89
|
-
if (lockEnabled(marker.packageName) === false) plugin.enabled = false;
|
|
90
|
-
out.push(plugin);
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const omp: HostReader = {
|
|
96
|
-
id: 'omp',
|
|
97
|
-
gui: false,
|
|
98
|
-
|
|
99
|
-
/** omp is present iff its native store is — a stray repo .mcp.json must not conjure the host. */
|
|
100
|
-
detect(): boolean {
|
|
101
|
-
return existsSync(pluginsDir());
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
stores(): string[] {
|
|
105
|
-
return [pluginsDir()];
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
listInstalled(): InstalledPlugin[] {
|
|
109
|
-
const managed = managedInstalled();
|
|
110
|
-
const managedIds = new Set(managed.map(plugin => plugin.id));
|
|
111
|
-
const root = readJson(join(pluginsDir(), 'installed_plugins.json'));
|
|
112
|
-
if (root === null) return managed;
|
|
113
|
-
const plugins = root['plugins'];
|
|
114
|
-
if (typeof plugins !== 'object' || plugins === null || Array.isArray(plugins)) return managed;
|
|
115
|
-
const out: InstalledPlugin[] = [...managed];
|
|
116
|
-
for (const [id, value] of Object.entries(plugins as Record<string, unknown>)) {
|
|
117
|
-
if (managedIds.has(id)) continue;
|
|
118
|
-
const at = id.indexOf('@');
|
|
119
|
-
const name = at === -1 ? id : id.slice(0, at);
|
|
120
|
-
const marketplace = at === -1 ? undefined : id.slice(at + 1);
|
|
121
|
-
if (!Array.isArray(value)) continue;
|
|
122
|
-
for (const raw of value) {
|
|
123
|
-
if (typeof raw !== 'object' || raw === null) continue;
|
|
124
|
-
const rec = raw as Record<string, unknown>;
|
|
125
|
-
const version = typeof rec['version'] === 'string' ? rec['version'] : undefined;
|
|
126
|
-
const recordedPath = typeof rec['installPath'] === 'string' ? rec['installPath'] : undefined;
|
|
127
|
-
// Cache slots are flat: cache/plugins/<marketplace>___<name>___<version>.
|
|
128
|
-
const fallback =
|
|
129
|
-
marketplace !== undefined && version !== undefined
|
|
130
|
-
? join(pluginsDir(), 'cache', 'plugins', `${marketplace}___${name}___${version}`)
|
|
131
|
-
: undefined;
|
|
132
|
-
const path = recordedPath !== undefined && existsSync(recordedPath) ? recordedPath : fallback;
|
|
133
|
-
const plugin: InstalledPlugin = { id, name };
|
|
134
|
-
if (marketplace !== undefined) plugin.marketplace = marketplace;
|
|
135
|
-
if (path !== undefined) plugin.path = path;
|
|
136
|
-
if (version !== undefined) plugin.version = version;
|
|
137
|
-
const enabled = lockEnabled(name);
|
|
138
|
-
if (enabled === false) plugin.enabled = false;
|
|
139
|
-
out.push(plugin);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return out;
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
mcpEntries(): McpServerEntry[] {
|
|
146
|
-
const entries: McpServerEntry[] = [];
|
|
147
|
-
for (const plugin of this.listInstalled()) {
|
|
148
|
-
if (plugin.path === undefined || !existsSync(plugin.path)) continue;
|
|
149
|
-
entries.push(...collectPluginServers(plugin.id, plugin.path, mcpCandidates()));
|
|
150
|
-
}
|
|
151
|
-
return entries;
|
|
152
|
-
},
|
|
153
|
-
};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/** Transactional OpenCode standalone delivery: private package + skill + command projections. */
|
|
2
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import type { AddOptions, HostWriter, InstalledPlugin, PinOptions, PinOutcome } from '../host';
|
|
6
|
-
import type { PluginSource, ResolvedSource } from '../source';
|
|
7
|
-
import { opencode, opencodeCommandsDir, opencodePackagesDir, opencodeRoot, opencodeSkillsDir } from './opencode';
|
|
8
|
-
|
|
9
|
-
const MARKER = '.plgnz-install.json';
|
|
10
|
-
type Owner = { source: string; pluginId: string; fingerprint: string };
|
|
11
|
-
type Move = { target: string; backup?: string };
|
|
12
|
-
type ManualSkill = { relative: string; name: string; description: string; body: string; base: string };
|
|
13
|
-
type SourceCommand = { relative: string; name: string; description: string; body: string; base: string };
|
|
14
|
-
type ProjectedCommand = { relative: string; name: string; description: string; body: string; base: string };
|
|
15
|
-
declare const Bun: { YAML: { parse(input: string): unknown } };
|
|
16
|
-
|
|
17
|
-
export const opencodeWriter: HostWriter = {
|
|
18
|
-
...opencode,
|
|
19
|
-
async add(plugin: PluginSource, resolved: ResolvedSource, opts?: AddOptions): Promise<void | 'unchanged'> {
|
|
20
|
-
const market = plugin.marketplace ?? 'local', id = `${plugin.name}@${market}`, name = packageName(market, plugin.name);
|
|
21
|
-
assertIdentity(market); assertIdentity(plugin.name); assertStore(); assertTree(plugin.dir); assertSupportedRoot(plugin.dir);
|
|
22
|
-
const parent = opts?.dryRun ? tmpdir() : dirname(opencodeRoot());
|
|
23
|
-
if (!opts?.dryRun) mkdirSync(parent, { recursive: true });
|
|
24
|
-
const stage = mkdtempSync(join(parent, '.plgnz-opencode-stage-'));
|
|
25
|
-
try {
|
|
26
|
-
const targets = targetsFor(name, plugin.name); checkTargets(targets, id, resolved.sourceUri);
|
|
27
|
-
const staged = stagePackage(plugin.dir, stage, name, targets.private, { source: resolved.sourceUri, pluginId: id, fingerprint: plugin.contentFingerprint ?? '' });
|
|
28
|
-
const unchanged = allSame(staged, targets);
|
|
29
|
-
if (opts?.dryRun) return unchanged ? 'unchanged' : undefined;
|
|
30
|
-
ensureStore(); if (unchanged) return 'unchanged';
|
|
31
|
-
const moves: Move[] = [];
|
|
32
|
-
try {
|
|
33
|
-
for (const key of keys()) {
|
|
34
|
-
const target = targets[key];
|
|
35
|
-
if (!existsSync(target)) { moves.push({ target }); continue; }
|
|
36
|
-
const backupRoot = mkdtempSync(join(dirname(target), '.plgnz-opencode-backup-'));
|
|
37
|
-
const backup = join(backupRoot, 'previous'); renameSync(target, backup); moves.push({ target, backup });
|
|
38
|
-
}
|
|
39
|
-
for (const key of keys()) renameSync(staged[key], targets[key]);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
for (const move of moves.reverse()) { rmSync(move.target, { recursive: true, force: true }); if (move.backup) renameSync(move.backup, move.target); }
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
for (const move of moves) if (move.backup) rmSync(dirname(move.backup), { recursive: true, force: true });
|
|
45
|
-
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
46
|
-
},
|
|
47
|
-
async remove(id: string): Promise<void> {
|
|
48
|
-
const installed = opencode.listInstalled().find(item => item.id === id);
|
|
49
|
-
if (!installed?.path) throw new Error(`OpenCode has no plgnz-owned package for ${id}; refusing to remove`);
|
|
50
|
-
const name = installed.path.slice(opencodePackagesDir().length + 1); if (!validName(name)) throw new Error(`unsafe OpenCode package path: ${installed.path}`);
|
|
51
|
-
const plugin = installed.id.split('@', 1)[0]; if (!plugin || !validName(plugin)) throw new Error(`unsafe OpenCode plugin identity: ${installed.id}`);
|
|
52
|
-
const targets = targetsFor(name, plugin); assertStore();
|
|
53
|
-
for (const key of keys()) if (existsSync(targets[key])) { assertManagedTree(targets[key]); if (!owned(targets[key], id)) throw new Error(`OpenCode target is not owned by ${id}: ${targets[key]}`); }
|
|
54
|
-
const moves: Move[] = [];
|
|
55
|
-
try { for (const key of keys()) { const target = targets[key]; if (!existsSync(target)) continue; const root = mkdtempSync(join(dirname(target), '.plgnz-opencode-remove-')); const backup = join(root, 'previous'); renameSync(target, backup); moves.push({ target, backup }); } }
|
|
56
|
-
catch (error) { for (const move of moves.reverse()) if (move.backup) renameSync(move.backup, move.target); throw error; }
|
|
57
|
-
for (const move of moves) if (move.backup) rmSync(dirname(move.backup), { recursive: true, force: true });
|
|
58
|
-
},
|
|
59
|
-
async pin(_plugin: InstalledPlugin, _opts?: PinOptions): Promise<PinOutcome> { return { changes: [], refusals: [] }; },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function stagePackage(source: string, stage: string, name: string, activePrivateRoot: string, ownerValue: Owner): Targets {
|
|
63
|
-
const privateRoot = join(stage, 'private', name), skills = join(stage, 'skills', name), commands = join(stage, 'commands', name);
|
|
64
|
-
mkdirSync(skills, { recursive: true }); mkdirSync(commands, { recursive: true });
|
|
65
|
-
cpSync(source, privateRoot, { recursive: true });
|
|
66
|
-
for (const root of [privateRoot, skills, commands]) writeFileSync(join(root, MARKER), JSON.stringify(ownerValue));
|
|
67
|
-
const manuals = manualSkills(source); const commandsToProject: ProjectedCommand[] = [...sourceCommands(source), ...manuals.map(skill => ({ ...skill, relative: skill.name }))]; const commandNames = new Set<string>();
|
|
68
|
-
for (const command of commandsToProject) { if (commandNames.has(command.name)) throw new Error(`command collision: ${command.name}`); commandNames.add(command.name); }
|
|
69
|
-
for (const command of commandsToProject) writeCommand(join(commands, `${command.relative}.md`), command.description, rewriteCommandReferences(command.body, commandNames, ownerValue.pluginId.split('@', 1)[0] ?? ''), join(activePrivateRoot, command.base));
|
|
70
|
-
cpSync(source, skills, { recursive: true });
|
|
71
|
-
for (const skill of manuals) rmSync(join(skills, skill.relative), { force: true });
|
|
72
|
-
for (const root of [privateRoot, skills, commands]) writeFileSync(join(root, MARKER), JSON.stringify(ownerValue));
|
|
73
|
-
return { private: privateRoot, skills, commands };
|
|
74
|
-
}
|
|
75
|
-
function sourceCommands(source: string): SourceCommand[] {
|
|
76
|
-
const root = existsSync(join(source, '.claude', 'commands')) ? join(source, '.claude', 'commands') : join(source, 'commands');
|
|
77
|
-
if (!existsSync(root)) return [];
|
|
78
|
-
const out: SourceCommand[] = [];
|
|
79
|
-
const walk = (dir: string) => { for (const entry of readdirSync(dir).sort()) { const path = join(dir, entry), stat = lstatSync(path); if (stat.isDirectory()) { walk(path); continue; } if (!stat.isFile()) throw new Error(`OpenCode command is not a regular file: ${path}`); if (entry.endsWith('.toml')) throw new Error(`OpenCode cannot preserve TOML command semantics: ${path}`); if (!entry.endsWith('.md')) continue; const parsed = commandMarkdown(readFileSync(path, 'utf8'), path); const relative = path.slice(root.length + 1, -3); out.push({ relative, name: relative, description: parsed.description, body: parsed.body, base: dirname(path).slice(source.length + 1) }); } };
|
|
80
|
-
walk(root);
|
|
81
|
-
return out;
|
|
82
|
-
}
|
|
83
|
-
function manualSkills(source: string): ManualSkill[] {
|
|
84
|
-
const root = join(source, 'skills'); if (!existsSync(root)) return [];
|
|
85
|
-
const out: ManualSkill[] = [];
|
|
86
|
-
const walk = (dir: string) => { for (const entry of readdirSync(dir).sort()) { const path = join(dir, entry), stat = lstatSync(path); if (stat.isDirectory()) walk(path); else if (stat.isFile() && entry === 'SKILL.md') { const parsed = markdown(readFileSync(path, 'utf8'), path); if (parsed.manual) out.push({ relative: path.slice(source.length + 1), name: parsed.name, description: parsed.description, body: parsed.body, base: dirname(path).slice(source.length + 1) }); } } };
|
|
87
|
-
walk(root); return out;
|
|
88
|
-
}
|
|
89
|
-
function writeCommand(path: string, description: string, body: string, privateRoot: string): void { const preservedBody = body.endsWith('\n') ? body : `${body}\n`; mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, `---\ndescription: ${JSON.stringify(description)}\n---\n\n${preservedBody}\nBase directory for this command: ${privateRoot}\nRelative paths in this command are relative to this base directory.\n`); }
|
|
90
|
-
function rewriteCommandReferences(body: string, names: Set<string>, namespace: string): string { return body.replace(/(^|[^A-Za-z0-9_/-])\/([A-Za-z0-9][A-Za-z0-9._/-]*)(?=\s|$|[.,:;!?])/gu, (whole, prefix: string, name: string) => names.has(name) ? `${prefix}/${namespace}/${name}` : whole); }
|
|
91
|
-
function markdown(raw: string, path: string): { name: string; description: string; body: string; manual: boolean } { const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); if (!match) throw new Error(`OpenCode projection needs Markdown frontmatter: ${path}`); let value: unknown; try { value = Bun.YAML.parse(match[1] ?? ''); } catch { throw new Error(`OpenCode projection has invalid YAML: ${path}`); } if (!record(value)) throw new Error(`OpenCode projection frontmatter must be an object: ${path}`); const description = value['description']; if (typeof description !== 'string' || !description.trim()) throw new Error(`OpenCode projection needs description: ${path}`); const name = typeof value['name'] === 'string' ? value['name'] : basenameStem(path); if (!validName(name)) throw new Error(`unsafe OpenCode command or skill name: ${path}`); const userInvocable = policy(value, 'user-invocable', 'user_invocable', path); if (userInvocable === false) throw new Error(`OpenCode cannot preserve user-invocable: false: ${path}`); const disableModelInvocation = policy(value, 'disable-model-invocation', 'disable_model_invocation', path); return { name, description, body: match[2] ?? '', manual: disableModelInvocation === true }; }
|
|
92
|
-
function policy(frontmatter: Record<string, unknown>, hyphen: string, underscore: string, path: string): boolean | undefined { const first = frontmatter[hyphen], second = frontmatter[underscore]; if (first !== undefined && second !== undefined && first !== second) throw new Error(`conflicting OpenCode policy aliases ${hyphen}/${underscore}: ${path}`); const value = first ?? second; if (value !== undefined && typeof value !== 'boolean') throw new Error(`OpenCode policy ${hyphen} must be boolean: ${path}`); return value; }
|
|
93
|
-
function commandMarkdown(raw: string, path: string): { description: string; body: string } { const parsed = markdown(raw, path); const front = Bun.YAML.parse((raw.match(/^---\r?\n([\s\S]*?)\r?\n---/) ?? [])[1] ?? ''); if (!record(front)) throw new Error(`OpenCode command frontmatter must be an object: ${path}`); for (const key of Object.keys(front)) if (key !== 'description') throw new Error(`OpenCode cannot preserve command metadata ${key}: ${path}`); if (/!`[\s\S]*?`/u.test(parsed.body) || /@\{/u.test(parsed.body)) throw new Error(`OpenCode cannot preserve command preprocessing: ${path}`); return parsed; }
|
|
94
|
-
function basenameStem(path: string): string { const value = path.split('/').at(-2); if (!value) throw new Error(`OpenCode skill has no directory name: ${path}`); return value; }
|
|
95
|
-
function packageName(market: string, plugin: string): string { const marketBytes = encode(market), pluginBytes = encode(plugin); return `plgnz-m${marketBytes.length}-${marketBytes}-p${pluginBytes.length}-${pluginBytes}`; }
|
|
96
|
-
function encode(value: string): string { return [...value].map(char => char.codePointAt(0)?.toString(16) ?? '').join('x'); }
|
|
97
|
-
function validName(value: string): boolean { return /^[a-z0-9][a-z0-9._-]*$/iu.test(value); }
|
|
98
|
-
function assertIdentity(value: string): void { if (!validName(value)) throw new Error(`unsafe OpenCode identity: ${value}`); }
|
|
99
|
-
function record(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
100
|
-
type Targets = { private: string; skills: string; commands: string };
|
|
101
|
-
function keys(): Array<keyof Targets> { return ['private', 'skills', 'commands']; }
|
|
102
|
-
function targetsFor(name: string, commandNamespace: string): Targets { return { private: join(opencodePackagesDir(), name), skills: join(opencodeSkillsDir(), name), commands: join(opencodeCommandsDir(), commandNamespace) }; }
|
|
103
|
-
function owner(path: string): Owner | null { const file = join(path, MARKER); if (!existsSync(file)) return null; try { const value: unknown = JSON.parse(readFileSync(file, 'utf8')); if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(); const item = value as Owner; if (typeof item.source !== 'string' || typeof item.pluginId !== 'string' || typeof item.fingerprint !== 'string') throw new Error(); return item; } catch { throw new Error(`invalid OpenCode ownership marker: ${file}`); } }
|
|
104
|
-
function owned(privateTarget: string, id: string, source?: string): boolean { const mark = owner(privateTarget); return mark !== null && mark.pluginId === id && (source === undefined || mark.source === source); }
|
|
105
|
-
function checkTargets(targets: Targets, id: string, source: string): void { for (const key of keys()) if (existsSync(targets[key])) { assertManagedTree(targets[key]); if (!owned(targets[key], id, source)) throw new Error(`OpenCode target is unowned or belongs to another source: ${targets[key]}`); } }
|
|
106
|
-
function allSame(staged: Targets, targets: Targets): boolean { return keys().every(key => sameTree(staged[key], targets[key])); }
|
|
107
|
-
function sameTree(a: string, b: string): boolean { if (!existsSync(a) || !existsSync(b)) return false; const bytes = readFileSync as unknown as (path: string) => Uint8Array; const list = (root: string): string[] => { const out: string[] = []; const walk = (dir: string, prefix: string) => { for (const name of readdirSync(dir).sort()) { if (name === MARKER) continue; const path = join(dir, name), rel = prefix ? `${prefix}/${name}` : name, stat = lstatSync(path); if (stat.isDirectory()) walk(path, rel); else if (stat.isFile()) out.push(`${rel}:${Array.from(bytes(path)).join(',')}`); else throw new Error(`OpenCode package has unsupported entry: ${path}`); } }; walk(root, ''); return out; }; return JSON.stringify(list(a)) === JSON.stringify(list(b)); }
|
|
108
|
-
function assertTree(root: string): void { const stat = lstatSync(root); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`OpenCode plugin root is not a real directory: ${root}`); for (const name of readdirSync(root)) { const path = join(root, name), child = lstatSync(path); if (name === MARKER) throw new Error(`OpenCode plugin reserves ${MARKER}: ${path}`); if (child.isSymbolicLink()) throw new Error(`OpenCode plugin contains symlink: ${path}`); if (child.isDirectory()) assertTree(path); else if (!child.isFile()) throw new Error(`OpenCode plugin contains unsupported entry: ${path}`); } }
|
|
109
|
-
function assertManagedTree(root: string): void { const stat = lstatSync(root); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`OpenCode managed tree is unsafe: ${root}`); for (const name of readdirSync(root)) { const path = join(root, name), child = lstatSync(path); if (child.isSymbolicLink()) throw new Error(`OpenCode managed tree contains symlink: ${path}`); if (child.isDirectory()) assertManagedTree(path); else if (!child.isFile()) throw new Error(`OpenCode managed tree contains unsupported entry: ${path}`); } }
|
|
110
|
-
function assertSupportedRoot(source: string): void { for (const entry of ['agents', 'hooks', '.claude/agents', '.claude/hooks', 'mcp.json', '.mcp.json']) if (existsSync(join(source, entry))) throw new Error(`OpenCode cannot preserve root ${entry} semantics: ${join(source, entry)}`); for (const file of ['plugin.json', '.plugin/plugin.json', '.claude-plugin/plugin.json']) { const path = join(source, file); if (!existsSync(path)) continue; let manifest: unknown; try { manifest = JSON.parse(readFileSync(path, 'utf8')); } catch { throw new Error(`OpenCode plugin manifest is invalid: ${path}`); } if (!record(manifest)) throw new Error(`OpenCode plugin manifest is invalid: ${path}`); for (const field of ['mcpServers', 'hooks', 'agents']) if (manifest[field] !== undefined) throw new Error(`OpenCode cannot preserve plugin manifest ${field}: ${path}`); } }
|
|
111
|
-
function assertStore(): void { for (const path of [opencodeRoot(), join(opencodeRoot(), '.plgnz'), opencodePackagesDir(), opencodeSkillsDir(), opencodeCommandsDir()]) if (existsSync(path) && (!lstatSync(path).isDirectory() || lstatSync(path).isSymbolicLink())) throw new Error(`OpenCode managed path is unsafe: ${path}`); }
|
|
112
|
-
function ensureStore(): void { for (const path of [opencodeRoot(), join(opencodeRoot(), '.plgnz'), opencodePackagesDir(), opencodeSkillsDir(), opencodeCommandsDir()]) { mkdirSync(path, { recursive: true }); assertStore(); } }
|
package/src/hosts/opencode.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/** Read-only owned standalone-package catalog for OpenCode. */
|
|
2
|
-
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import type { HostReader, InstalledPlugin, McpServerEntry } from '../host';
|
|
5
|
-
|
|
6
|
-
const MARKER = '.plgnz-install.json';
|
|
7
|
-
|
|
8
|
-
export function opencodeRoot(): string {
|
|
9
|
-
const explicit = process.env['OPEN_PLUGIN_OPENCODE_ROOT'];
|
|
10
|
-
if (explicit) return resolve(explicit);
|
|
11
|
-
return join(process.env['OPEN_PLUGIN_HOME'] || process.env['HOME'] || '.', '.config', 'opencode');
|
|
12
|
-
}
|
|
13
|
-
export function opencodePackagesDir(): string { return join(opencodeRoot(), '.plgnz', 'packages'); }
|
|
14
|
-
export function opencodeSkillsDir(): string { return join(opencodeRoot(), 'skills'); }
|
|
15
|
-
export function opencodeCommandsDir(): string { return join(opencodeRoot(), 'commands'); }
|
|
16
|
-
|
|
17
|
-
export const opencode: HostReader = {
|
|
18
|
-
id: 'opencode', gui: false,
|
|
19
|
-
detect: () => existsSync(opencodeRoot()),
|
|
20
|
-
stores: () => [opencodePackagesDir(), opencodeSkillsDir(), opencodeCommandsDir()],
|
|
21
|
-
listInstalled(): InstalledPlugin[] {
|
|
22
|
-
const root = opencodePackagesDir();
|
|
23
|
-
if (!existsSync(root)) return [];
|
|
24
|
-
return readdirSync(root).flatMap(name => {
|
|
25
|
-
const path = join(root, name);
|
|
26
|
-
if (!lstatSync(path).isDirectory()) return [];
|
|
27
|
-
const marker = join(path, MARKER);
|
|
28
|
-
if (!existsSync(marker)) return [];
|
|
29
|
-
let value: unknown;
|
|
30
|
-
try { value = JSON.parse(readFileSync(marker, 'utf8')); } catch { throw new Error(`invalid OpenCode ownership marker: ${marker}`); }
|
|
31
|
-
if (!isRecord(value) || typeof value.pluginId !== 'string') throw new Error(`invalid OpenCode ownership marker: ${marker}`);
|
|
32
|
-
const at = value.pluginId.indexOf('@'); const plugin = at < 0 ? value.pluginId : value.pluginId.slice(0, at);
|
|
33
|
-
return [{ id: value.pluginId, name: plugin, ...(at < 0 ? {} : { marketplace: value.pluginId.slice(at + 1) }), path, contentRoots: { package: path, skills: join(opencodeSkillsDir(), name), commands: join(opencodeCommandsDir(), plugin) }, enabled: true }];
|
|
34
|
-
});
|
|
35
|
-
},
|
|
36
|
-
mcpEntries: (): McpServerEntry[] => [],
|
|
37
|
-
};
|
|
38
|
-
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|