plugnz 0.0.4 → 0.0.5
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/pi-writer.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/** Transactional standalone-skill writer for Pi 0.80.10. */
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { basename, 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 { pi, piRoot, skillsDir } from './pi';
|
|
8
|
-
import { projectPluginForPi } from '../conversion';
|
|
9
|
-
|
|
10
|
-
const MARKER = '.plgnz-install.json';
|
|
11
|
-
type Owner = { source: string; pluginId: string; fingerprint: string };
|
|
12
|
-
type Move = { target: string; backup?: string };
|
|
13
|
-
|
|
14
|
-
export const piWriter: HostWriter = {
|
|
15
|
-
...pi,
|
|
16
|
-
async add(plugin: PluginSource, resolved: ResolvedSource, opts?: AddOptions): Promise<void | 'unchanged'> {
|
|
17
|
-
const market = plugin.marketplace ?? 'local'; const id = `${plugin.name}@${market}`;
|
|
18
|
-
identity(plugin.name, 'plugin'); identity(market, 'marketplace');
|
|
19
|
-
assertPiStore();
|
|
20
|
-
const stageParent = opts?.dryRun ? tmpdir() : dirname(skillsDir());
|
|
21
|
-
if (!opts?.dryRun) mkdirSync(stageParent, { recursive: true });
|
|
22
|
-
const stage = mkdtempSync(join(stageParent, '.plgnz-pi-stage-'));
|
|
23
|
-
try {
|
|
24
|
-
const target = join(skillsDir(), packageName(plugin.name, market));
|
|
25
|
-
const staged = stagePlugin(plugin.dir, stage, plugin.name, market);
|
|
26
|
-
writeFileSync(join(staged, MARKER), JSON.stringify({ source: resolved.sourceUri, pluginId: id, fingerprint: plugin.contentFingerprint ?? '' } satisfies Owner));
|
|
27
|
-
checkTarget(target, id, resolved.sourceUri);
|
|
28
|
-
const prior = ownedFor(id, resolved.sourceUri);
|
|
29
|
-
const unchanged = prior.length === 1 && prior[0] === target && owner(target)?.fingerprint === (plugin.contentFingerprint ?? '') && sameTree(staged, target);
|
|
30
|
-
if (opts?.dryRun) {
|
|
31
|
-
console.log(`[pi] would activate standalone package: ${target}`);
|
|
32
|
-
return unchanged ? 'unchanged' : undefined;
|
|
33
|
-
}
|
|
34
|
-
ensureStore();
|
|
35
|
-
if (unchanged) return 'unchanged';
|
|
36
|
-
const backups: Move[] = [];
|
|
37
|
-
try {
|
|
38
|
-
for (const candidate of [...new Set([target, ...prior])]) {
|
|
39
|
-
if (!existsSync(candidate)) { backups.push({ target: candidate }); continue; }
|
|
40
|
-
const backupRoot = mkdtempSync(join(skillsDir(), '.plgnz-pi-backup-'));
|
|
41
|
-
const backup = join(backupRoot, 'previous'); renameSync(candidate, backup); backups.push({ target: candidate, backup });
|
|
42
|
-
}
|
|
43
|
-
renameSync(staged, target);
|
|
44
|
-
} catch (error) {
|
|
45
|
-
for (const move of backups.reverse()) { rmSync(move.target, { recursive: true, force: true }); if (move.backup !== undefined) renameSync(move.backup, move.target); }
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
for (const move of backups) if (move.backup !== undefined) rmSync(dirname(move.backup), { recursive: true, force: true });
|
|
49
|
-
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
50
|
-
},
|
|
51
|
-
async pin(_plugin: InstalledPlugin, _opts?: PinOptions): Promise<PinOutcome> { return { changes: [], refusals: [] }; },
|
|
52
|
-
async remove(id: string): Promise<void> {
|
|
53
|
-
if (!/^[a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-9._-]*$/iu.test(id)) throw new Error(`unsafe Pi plugin id: ${id}`);
|
|
54
|
-
assertPiStore();
|
|
55
|
-
const targets = ownedFor(id);
|
|
56
|
-
if (targets.length === 0) throw new Error(`Pi has no wholly plgnz-owned standalone skills for ${id}; refusing to remove`);
|
|
57
|
-
const moves: Move[] = [];
|
|
58
|
-
try {
|
|
59
|
-
for (const target of targets) {
|
|
60
|
-
const backupRoot = mkdtempSync(join(skillsDir(), '.plgnz-pi-remove-'));
|
|
61
|
-
const backup = join(backupRoot, 'previous'); renameSync(target, backup); moves.push({ target, backup });
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
for (const move of moves.reverse()) if (move.backup !== undefined) renameSync(move.backup, move.target);
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
67
|
-
for (const move of moves) if (move.backup !== undefined) rmSync(dirname(move.backup), { recursive: true, force: true });
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function stagePlugin(source: string, stage: string, plugin: string, market: string): string {
|
|
72
|
-
assertTree(source);
|
|
73
|
-
const root = join(source, 'skills');
|
|
74
|
-
if (!existsSync(root) || !lstatSync(root).isDirectory()) throw new Error('Pi standalone delivery requires plugin skills/ directories');
|
|
75
|
-
const destination = join(stage, packageName(plugin, market)); projectPluginForPi(source, destination, plugin);
|
|
76
|
-
const skillFiles: string[] = [];
|
|
77
|
-
const walk = (dir: string): void => { for (const entry of readdirSync(dir)) { const path = join(dir, entry); const stat = lstatSync(path); if (stat.isDirectory()) walk(path); else if (entry === 'SKILL.md') skillFiles.push(path); } };
|
|
78
|
-
walk(join(destination, 'skills'));
|
|
79
|
-
for (const skill of skillFiles) {
|
|
80
|
-
const relative = skill.slice(join(destination, 'skills').length + 1).split('/');
|
|
81
|
-
const sourceName = relative.length >= 2 ? relative[relative.length - 2] : undefined;
|
|
82
|
-
if (sourceName === undefined) throw new Error(`Pi skill has no directory name: ${skill}`);
|
|
83
|
-
const name = `${plugin}-${sourceName}`; piSkillName(name);
|
|
84
|
-
if (name.length > 64) throw new Error(`Pi standalone skill name exceeds 64 characters: ${name}`);
|
|
85
|
-
const text = readFileSync(skill, 'utf8');
|
|
86
|
-
if (!/^---\n[\s\S]*?^description\s*:\s*\S[\s\S]*?^---\s*$/m.test(text)) throw new Error(`Pi skill needs frontmatter description: ${skill}`);
|
|
87
|
-
writeFileSync(skill, namespaceSkill(text, name));
|
|
88
|
-
}
|
|
89
|
-
if (skillFiles.length === 0) throw new Error('Pi standalone delivery found no SKILL.md files');
|
|
90
|
-
assertTree(destination); return destination;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function namespaceSkill(text: string, name: string): string {
|
|
94
|
-
const end = text.indexOf('\n---', 3); if (!text.startsWith('---\n') || end === -1) throw new Error('Pi skill frontmatter is malformed');
|
|
95
|
-
const front = text.slice(0, end + 4);
|
|
96
|
-
const next = /^name\s*:/m.test(front) ? front.replace(/^name\s*:.*$/m, `name: ${name}`) : front.slice(0, -4) + `\nname: ${name}\n---`;
|
|
97
|
-
return next + text.slice(end + 4);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function packageName(plugin: string, market: string): string { return `${market}___${plugin}`; }
|
|
101
|
-
function owner(dir: string): Owner | null { const file = join(dir, MARKER); if (!existsSync(file)) return null; try { const x: unknown = JSON.parse(readFileSync(file, 'utf8')); if (typeof x !== 'object' || x === null || Array.isArray(x)) throw new Error('not an object'); const r = x as Record<string, unknown>; if (typeof r.source !== 'string' || typeof r.pluginId !== 'string' || typeof r.fingerprint !== 'string') throw new Error('fields are invalid'); return r as Owner; } catch (error) { throw new Error(`invalid Pi ownership marker: ${file} (${(error as Error).message})`); } }
|
|
102
|
-
function ownedFor(id: string, source?: string): string[] { assertPiStore(); if (!existsSync(skillsDir())) return []; const out: string[] = []; for (const name of readdirSync(skillsDir())) { const path = join(skillsDir(), name); if (!lstatSync(path).isDirectory()) continue; const mark = owner(path); if (mark?.pluginId === id && (source === undefined || mark.source === source)) out.push(path); } return out; }
|
|
103
|
-
function checkTarget(target: string, id: string, source: string): void { assertUnder(skillsDir(), target); if (!existsSync(target)) return; const mark = owner(target); if (mark === null) throw new Error(`Pi standalone skill is unowned; refusing to replace it: ${target}`); if (mark.pluginId !== id || mark.source !== source) throw new Error(`Pi standalone skill belongs to another source; refusing to replace it: ${target}`); }
|
|
104
|
-
function sameTree(stage: string, target: string): boolean { if (!existsSync(target)) 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): void => { for (const name of readdirSync(dir).sort()) { if (name === MARKER) continue; const path = join(dir, name), relative = prefix ? `${prefix}/${name}` : name, stat = lstatSync(path); if (stat.isDirectory()) walk(path, relative); else if (stat.isFile()) out.push(`${relative}:${Array.from(bytes(path)).join(',')}`); else throw new Error(`Pi standalone skill contains unsupported entry: ${path}`); } }; walk(root, ''); return out; }; return JSON.stringify(list(stage)) === JSON.stringify(list(target)); }
|
|
105
|
-
function assertTree(root: string): void { const stat = lstatSync(root); if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Pi plugin root is not a real directory: ${root}`); for (const name of readdirSync(root)) { const path = join(root, name), child = lstatSync(path); if (child.isSymbolicLink()) throw new Error(`Pi plugin contains symlink: ${path}`); if (child.isDirectory()) assertTree(path); else if (!child.isFile()) throw new Error(`Pi plugin contains unsupported entry: ${path}`); } }
|
|
106
|
-
function identity(value: string, label: string): void { if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(value)) throw new Error(`unsafe Pi ${label}: ${value}`); }
|
|
107
|
-
function piSkillName(value: string): void { if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(value)) throw new Error(`Pi standalone skill name is not lowercase-hyphenated: ${value}`); }
|
|
108
|
-
function ensureStore(): void { assertPiStore(); mkdirSync(piRoot(), { recursive: true }); assertRealDirectory(piRoot()); mkdirSync(skillsDir(), { recursive: true }); assertRealDirectory(skillsDir()); }
|
|
109
|
-
function assertPiStore(): void { const root = piRoot(); const home = dirname(dirname(root)); let current = home; for (const part of [basename(dirname(root)), basename(root)]) { current = join(current, part); if (existsSync(current) && lstatSync(current).isSymbolicLink()) throw new Error(`Pi managed path component is a symlink: ${current}`); } if (existsSync(root)) assertRealDirectory(root); if (existsSync(skillsDir())) { assertUnder(root, skillsDir()); assertRealDirectory(skillsDir()); } }
|
|
110
|
-
function assertRealDirectory(path: string): void { const stat = lstatSync(path); if (stat.isSymbolicLink()) throw new Error(`Pi managed path component is a symlink: ${path}`); if (!stat.isDirectory()) throw new Error(`Pi managed path component is not a directory: ${path}`); }
|
|
111
|
-
function assertUnder(root: string, path: string): void { const base = resolve(root), candidate = resolve(path); if (candidate !== base && !candidate.startsWith(`${base}/`)) throw new Error(`Pi path escapes standalone skills store: ${path}`); let current = base; for (const part of candidate.slice(base.length).split('/').filter(Boolean)) { current = join(current, part); if (existsSync(current) && lstatSync(current).isSymbolicLink()) throw new Error(`Pi managed path component is a symlink: ${current}`); } }
|
package/src/hosts/pi.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/** Read-only Pi standalone-skill catalog (Pi 0.80.10; docs/hosts/pi.md). */
|
|
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 skillsDir(): string { return join(piRoot(), 'skills'); }
|
|
9
|
-
|
|
10
|
-
/** Pi root: host override, isolated open-plugin home, then the user's home. */
|
|
11
|
-
export function piRoot(): string {
|
|
12
|
-
const explicit = process.env['OPEN_PLUGIN_PI_ROOT'];
|
|
13
|
-
if (explicit && explicit.length > 0) return resolve(explicit);
|
|
14
|
-
const home = process.env['OPEN_PLUGIN_HOME'] || process.env['HOME'] || '.';
|
|
15
|
-
return join(home, '.pi', 'agent');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function owner(dir: string): { pluginId: string } | null {
|
|
19
|
-
const file = join(dir, MARKER);
|
|
20
|
-
if (!existsSync(file)) return null;
|
|
21
|
-
try {
|
|
22
|
-
const value: unknown = JSON.parse(readFileSync(file, 'utf8'));
|
|
23
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`invalid Pi ownership marker: ${file}`);
|
|
24
|
-
const pluginId = (value as Record<string, unknown>).pluginId;
|
|
25
|
-
if (typeof pluginId !== 'string') throw new Error(`invalid Pi ownership marker: ${file}`);
|
|
26
|
-
return { pluginId };
|
|
27
|
-
} catch (error) { if (error instanceof Error && error.message.startsWith('invalid Pi ownership marker:')) throw error; throw new Error(`invalid Pi ownership marker: ${file}`); }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const pi: HostReader = {
|
|
31
|
-
id: 'pi', gui: false,
|
|
32
|
-
detect: () => existsSync(piRoot()),
|
|
33
|
-
stores: () => [skillsDir()],
|
|
34
|
-
listInstalled(): InstalledPlugin[] {
|
|
35
|
-
if (!existsSync(skillsDir())) return [];
|
|
36
|
-
const result: InstalledPlugin[] = [];
|
|
37
|
-
for (const entry of readdirSync(skillsDir())) {
|
|
38
|
-
const dir = join(skillsDir(), entry);
|
|
39
|
-
if (!lstatSync(dir).isDirectory()) continue;
|
|
40
|
-
const marker = owner(dir);
|
|
41
|
-
if (marker === null) continue; // User-owned standalone skills are never claimed.
|
|
42
|
-
const at = marker.pluginId.indexOf('@');
|
|
43
|
-
result.push({ id: marker.pluginId, name: at === -1 ? marker.pluginId : marker.pluginId.slice(0, at), ...(at === -1 ? {} : { marketplace: marker.pluginId.slice(at + 1) }), path: dir, enabled: true });
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
},
|
|
47
|
-
mcpEntries: (): McpServerEntry[] => [],
|
|
48
|
-
};
|
package/src/hosts/writers.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The writer side of the host registry — `add`/`pin`/`update`/`remove`
|
|
3
|
-
* consume this list; doctor never does (see src/hosts/index.ts for why the
|
|
4
|
-
* two lists live in separate modules).
|
|
5
|
-
*/
|
|
6
|
-
import type { HostWriter } from '../host';
|
|
7
|
-
import { claudeCodeWriter } from './claude-code-writer';
|
|
8
|
-
import { codexWriter } from './codex-writer';
|
|
9
|
-
import { kimiWriter } from './kimi-writer';
|
|
10
|
-
import { cursorWriter } from './cursor-writer';
|
|
11
|
-
import { ompWriter } from './omp-writer';
|
|
12
|
-
import { piWriter } from './pi-writer';
|
|
13
|
-
import { dcodeWriter } from './dcode-writer';
|
|
14
|
-
import { opencodeWriter } from './opencode-writer';
|
|
15
|
-
import { grokWriter } from './grok-writer';
|
|
16
|
-
import { zcodeCliWriter } from './zcode-cli-writer';
|
|
17
|
-
import { hermesWriter } from './hermes-writer';
|
|
18
|
-
|
|
19
|
-
/** Active mutation routes: native plugin stores only. */
|
|
20
|
-
export const writers: HostWriter[] = [claudeCodeWriter, codexWriter, kimiWriter, cursorWriter, ompWriter, hermesWriter, dcodeWriter, grokWriter, zcodeCliWriter];
|
|
21
|
-
|
|
22
|
-
/** Legacy standalone writers are retained solely to remove plgnz-owned installs safely. */
|
|
23
|
-
export const cleanupWriters: HostWriter[] = [...writers, piWriter, opencodeWriter];
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/** Official ZCode CLI marketplace writer. */
|
|
2
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join, relative, 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 { fingerprintTree } from '../fingerprint';
|
|
8
|
-
import { assertZcodeNativeRegistryReadable, readZcodeEnabledPluginIds, readZcodeNativeRecords, readZcodeOwnership, runOfficialZcode, zcodeCli, zcodeMarketplaceRoot, zcodeResourceRoot, zcodeSafeInstallRoot } from './zcode-cli';
|
|
9
|
-
import { normalizeCommandTree } from '../conversion';
|
|
10
|
-
import { zcodeCliRoot } from '../paths';
|
|
11
|
-
|
|
12
|
-
declare const Bun: any;
|
|
13
|
-
|
|
14
|
-
const MARKER = '.plgnz-install.json';
|
|
15
|
-
type Ownership = { owner: 'plgnz'; schema: 1; logicalId: string; nativeId: string; fingerprint: string; source: string; resourcePath: string };
|
|
16
|
-
|
|
17
|
-
export const zcodeCliWriter: HostWriter = {
|
|
18
|
-
...zcodeCli,
|
|
19
|
-
supportsAdoption: false,
|
|
20
|
-
async add(plugin: PluginSource, resolved: ResolvedSource, opts?: AddOptions): Promise<void | 'unchanged'> {
|
|
21
|
-
if (opts?.adoptExisting) throw new Error('Official ZCode adoption is not implemented; refusing an unowned native install');
|
|
22
|
-
assertName(plugin.name, 'plugin name');
|
|
23
|
-
const logicalId = plugin.marketplace === undefined ? plugin.name : `${plugin.name}@${plugin.marketplace}`;
|
|
24
|
-
const fingerprint = requireFingerprint(plugin);
|
|
25
|
-
const nativeVersion = versionFor(plugin, fingerprint);
|
|
26
|
-
const market = ownedMarketplace(logicalId);
|
|
27
|
-
const nativeId = `${plugin.name}@${market}`;
|
|
28
|
-
assertZcodeNativeRegistryReadable();
|
|
29
|
-
const prior = nativeById(nativeId);
|
|
30
|
-
if (prior !== undefined) provePrior(prior, logicalId, nativeId, resolved.sourceUri);
|
|
31
|
-
const stageParent = join(zcodeCliRoot(), '.plgnz-zcode-stage');
|
|
32
|
-
mkdirSync(opts?.dryRun ? tmpdir() : stageParent, { recursive: true });
|
|
33
|
-
const stage = mkdtempSync(join(opts?.dryRun ? tmpdir() : stageParent, 'candidate-'));
|
|
34
|
-
const stagedMarket = join(stage, market); const stagedResources = join(stage, 'resources');
|
|
35
|
-
const marketRoot = join(zcodeMarketplaceRoot(), market); const resourceRoot = join(zcodeResourceRoot(), market, fingerprint);
|
|
36
|
-
try {
|
|
37
|
-
stagePackage(plugin, stagedMarket, stagedResources, resourceRoot, logicalId, nativeId, nativeVersion, fingerprint, resolved.sourceUri);
|
|
38
|
-
rejectCommandCollisions(stagedMarket, nativeId, prior?.installPath);
|
|
39
|
-
assertReplaceableOwnedRoots(marketRoot, resourceRoot, plugin.name, prior, logicalId, nativeId, resolved.sourceUri);
|
|
40
|
-
if (prior !== undefined && prior.version === nativeVersion && readZcodeEnabledPluginIds().get(nativeId) === true && sameCandidate(prior, stagedMarket, stagedResources, resourceRoot, logicalId, nativeId, resolved.sourceUri, fingerprint)) return 'unchanged';
|
|
41
|
-
if (opts?.dryRun) return;
|
|
42
|
-
const marketBackup = moveAside(marketRoot);
|
|
43
|
-
const resourceBackup = moveAside(resourceRoot);
|
|
44
|
-
try {
|
|
45
|
-
mkdirSync(dirname(marketRoot), { recursive: true }); mkdirSync(dirname(resourceRoot), { recursive: true });
|
|
46
|
-
renameSync(stagedMarket, marketRoot);
|
|
47
|
-
renameSync(stagedResources, resourceRoot);
|
|
48
|
-
const expectedResourceFingerprint = fingerprintTree(resourceRoot);
|
|
49
|
-
if (prior === undefined) {
|
|
50
|
-
runOfficialZcode(['plugins', 'marketplace', 'add', marketRoot]);
|
|
51
|
-
runOfficialZcode(['plugins', 'install', nativeId]);
|
|
52
|
-
} else {
|
|
53
|
-
runOfficialZcode(['plugins', 'marketplace', 'update', market]);
|
|
54
|
-
runOfficialZcode(['plugins', 'update', nativeId]);
|
|
55
|
-
}
|
|
56
|
-
proveActive(nativeId, logicalId, resolved.sourceUri, fingerprint, nativeVersion, join(marketRoot, 'plugins', plugin.name), resourceRoot, expectedResourceFingerprint);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
// The old cache remains native-selected on documented update failure.
|
|
59
|
-
// Restore the old marketplace source before trying a compensating refresh.
|
|
60
|
-
rmSync(marketRoot, { recursive: true, force: true }); marketBackup.rollback();
|
|
61
|
-
rmSync(resourceRoot, { recursive: true, force: true }); resourceBackup.rollback();
|
|
62
|
-
if (prior !== undefined) restorePrior(nativeId, market, prior);
|
|
63
|
-
else removeCandidate(nativeId, logicalId, resolved.sourceUri, fingerprint);
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
marketBackup.commit(); resourceBackup.commit();
|
|
67
|
-
} finally { rmSync(stage, { recursive: true, force: true }); }
|
|
68
|
-
},
|
|
69
|
-
async remove(id: string): Promise<void> {
|
|
70
|
-
assertZcodeNativeRegistryReadable();
|
|
71
|
-
const native = nativeForLogical(id);
|
|
72
|
-
if (native === undefined) throw new Error(`Official ZCode plugin ${id} is not a proven plgnz-owned install`);
|
|
73
|
-
runOfficialZcode(['plugins', 'uninstall', native.id, '--force']);
|
|
74
|
-
if (nativeById(native.id) !== undefined) throw new Error(`Official ZCode did not remove ${id}`);
|
|
75
|
-
},
|
|
76
|
-
async pin(_plugin: InstalledPlugin, _opts?: PinOptions): Promise<PinOutcome> { return { changes: [], refusals: [] }; },
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/** Stages a namespaced command projection without invoking the native CLI. */
|
|
80
|
-
export function projectZcodePlugin(source: string, destination: string, resourceCopy: string, resourceLinkRoot: string, logicalId: string, nativeId: string, nativeVersion: string, fingerprint: string, sourceUri: string): void {
|
|
81
|
-
const input = resolve(source); const target = resolve(destination); const ownedResources = resolve(resourceCopy); const links = resolve(resourceLinkRoot);
|
|
82
|
-
if (!existsSync(input) || !statSync(input).isDirectory()) throw new Error(`ZCode plugin source is not a directory: ${input}`);
|
|
83
|
-
if (target.startsWith(`${input}/`) || ownedResources.startsWith(`${input}/`)) throw new Error('ZCode projected outputs must not be inside the source');
|
|
84
|
-
assertNoSymlinks(input); cpSync(input, target, { recursive: true });
|
|
85
|
-
validateRootManifest(target);
|
|
86
|
-
projectCommands(target, sourceName(target));
|
|
87
|
-
projectUserOnlySkills(target, ownedResources, links, sourceName(target));
|
|
88
|
-
const manifest: Record<string, unknown> = { name: sourceName(target), version: nativeVersion };
|
|
89
|
-
const rootManifest = readJson(join(target, 'plugin.json'));
|
|
90
|
-
for (const key of ['description', 'author', 'license']) if (typeof rootManifest?.[key] === 'string') manifest[key] = rootManifest[key];
|
|
91
|
-
if (hasSkill(target)) manifest['skills'] = 'skills';
|
|
92
|
-
if (hasCommand(target)) manifest['commands'] = 'commands';
|
|
93
|
-
mkdirSync(join(target, '.zcode-plugin'), { recursive: true });
|
|
94
|
-
writeFileSync(join(target, '.zcode-plugin', 'plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
95
|
-
writeFileSync(join(target, MARKER), `${JSON.stringify({ owner: 'plgnz', schema: 1, logicalId, nativeId, fingerprint, source: sourceUri, resourcePath: resourceLinkRoot } satisfies Ownership, null, 2)}\n`);
|
|
96
|
-
assertNoSymlinks(target); assertNoSymlinks(ownedResources);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function stagePackage(plugin: PluginSource, market: string, resources: string, resourceLinkRoot: string, logicalId: string, nativeId: string, nativeVersion: string, fingerprint: string, source: string): void {
|
|
100
|
-
const packageRoot = join(market, 'plugins', plugin.name);
|
|
101
|
-
mkdirSync(dirname(packageRoot), { recursive: true }); mkdirSync(resources, { recursive: true });
|
|
102
|
-
projectZcodePlugin(plugin.dir, packageRoot, resources, resourceLinkRoot, logicalId, nativeId, nativeVersion, fingerprint, source);
|
|
103
|
-
writeFileSync(join(market, 'marketplace.json'), `${JSON.stringify({ name: nativeId.slice(nativeId.indexOf('@') + 1), plugins: [{ name: plugin.name, source: `./plugins/${plugin.name}` }] }, null, 2)}\n`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function projectCommands(root: string, pluginName: string): void {
|
|
107
|
-
const source = join(root, 'commands'); if (!existsSync(source)) return;
|
|
108
|
-
if (!statSync(source).isDirectory()) throw new Error(`ZCode commands path is not a directory: ${source}`);
|
|
109
|
-
const temp = join(root, '.plgnz-zcode-commands'); renameSync(source, temp);
|
|
110
|
-
try {
|
|
111
|
-
normalizeCommandTree(temp);
|
|
112
|
-
copyCommandTree(temp, join(root, 'commands', pluginName));
|
|
113
|
-
} finally { rmSync(temp, { recursive: true, force: true }); }
|
|
114
|
-
}
|
|
115
|
-
function copyCommandTree(source: string, dest: string): void {
|
|
116
|
-
for (const entry of readdirSync(source)) {
|
|
117
|
-
const from = join(source, entry); const to = join(dest, entry); const stat = lstatSync(from);
|
|
118
|
-
if (stat.isSymbolicLink()) throw new Error(`ZCode command source contains symlink: ${from}`);
|
|
119
|
-
if (stat.isDirectory()) { copyCommandTree(from, to); continue; }
|
|
120
|
-
if (!stat.isFile() || !entry.endsWith('.md')) throw new Error(`ZCode command source must be Markdown: ${from}`);
|
|
121
|
-
const text = readFileSync(from, 'utf8'); validateCommand(text, from); mkdirSync(dirname(to), { recursive: true }); writeFileSync(to, text);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function projectUserOnlySkills(root: string, resources: string, resourceLinks: string, pluginName: string): void {
|
|
125
|
-
const skills = join(root, 'skills'); if (!existsSync(skills)) return;
|
|
126
|
-
if (!statSync(skills).isDirectory()) throw new Error(`ZCode skills path is not a directory: ${skills}`);
|
|
127
|
-
for (const entry of readdirSync(skills)) {
|
|
128
|
-
const dir = join(skills, entry); if (!statSync(dir).isDirectory()) throw new Error(`ZCode skill is not a directory: ${dir}`);
|
|
129
|
-
const skill = join(dir, 'SKILL.md'); if (!existsSync(skill)) continue;
|
|
130
|
-
const raw = readFileSync(skill, 'utf8'); const fm = frontmatter(raw, skill);
|
|
131
|
-
const hidden = fm.values['disable-model-invocation'] ?? fm.values['disable_model_invocation'];
|
|
132
|
-
const invocable = fm.values['user-invocable'] ?? fm.values['user_invocable'];
|
|
133
|
-
if (hidden !== undefined && typeof hidden !== 'boolean') throw new Error(`ZCode disable-model-invocation must be boolean: ${skill}`);
|
|
134
|
-
if (invocable !== undefined && typeof invocable !== 'boolean') throw new Error(`ZCode user-invocable must be boolean: ${skill}`);
|
|
135
|
-
if (hidden !== true) continue;
|
|
136
|
-
if (invocable === false) throw new Error(`ZCode cannot represent user-invocable: false as a command: ${skill}`);
|
|
137
|
-
const name = typeof fm.values['name'] === 'string' ? fm.values['name'] : entry; assertName(name, 'user-only skill name');
|
|
138
|
-
const command = join(root, 'commands', pluginName, `${name}.md`);
|
|
139
|
-
if (existsSync(command)) throw new Error(`ZCode command collision for user-only skill ${name}`);
|
|
140
|
-
for (const key of Object.keys(fm.values)) if (!['name', 'description', 'disable-model-invocation', 'disable_model_invocation', 'user-invocable', 'user_invocable', 'argument-hint', 'allowed-tools', 'disable-noninteractive', 'model', 'skills'].includes(key)) throw new Error(`ZCode unsupported user-only skill metadata ${key}: ${skill}`);
|
|
141
|
-
const resource = join(resources, 'skills', entry); const linkResource = join(resourceLinks, 'skills', entry); copyResources(dir, resource);
|
|
142
|
-
const commandMeta: string[] = [`description: ${JSON.stringify(typeof fm.values['description'] === 'string' ? fm.values['description'] : name)}`];
|
|
143
|
-
for (const key of ['argument-hint', 'allowed-tools', 'disable-noninteractive', 'model', 'skills']) {
|
|
144
|
-
const value = fm.values[key]; if (value === undefined) continue;
|
|
145
|
-
commandMeta.push(`${key}: ${key === 'argument-hint' ? argumentHint(value, skill) : JSON.stringify(value)}`);
|
|
146
|
-
}
|
|
147
|
-
const body = raw.slice(fm.end); validateCommand(`---\n${commandMeta.join('\n')}\n---\n${body}`, skill);
|
|
148
|
-
mkdirSync(dirname(command), { recursive: true });
|
|
149
|
-
writeFileSync(command, `---\n${commandMeta.join('\n')}\n---\n${rewriteLinks(body, dir, linkResource, skill)}`);
|
|
150
|
-
rmSync(dir, { recursive: true, force: true });
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function copyResources(source: string, destination: string): void {
|
|
154
|
-
mkdirSync(destination, { recursive: true });
|
|
155
|
-
for (const name of readdirSync(source)) {
|
|
156
|
-
if (name === 'SKILL.md') continue;
|
|
157
|
-
const from = join(source, name); const stat = lstatSync(from); if (stat.isSymbolicLink()) throw new Error(`ZCode resource contains symlink: ${from}`);
|
|
158
|
-
cpSync(from, join(destination, name), { recursive: true });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function rewriteLinks(body: string, source: string, resources: string, label: string): string {
|
|
162
|
-
if (/\]\s*\[[^\]]+\]/u.test(body)) throw new Error(`ZCode unsupported reference-style Markdown link: ${label}`);
|
|
163
|
-
// Inline code spans are not links; mask them before rewriting so example
|
|
164
|
-
// syntax like `[title](link)` inside backticks stays literal (CommonMark).
|
|
165
|
-
const spans: string[] = [];
|
|
166
|
-
const masked = body.replace(/(`+)([\s\S]*?)\1/gu, (all) => { spans.push(all); return `\u0000${spans.length - 1}\u0000`; });
|
|
167
|
-
const rewritten = masked.replace(/(!?\[[^\]]*\])\(([^)\s]+)(\s+[^)]*)?\)/gu, (all, text: string, target: string, suffix: string | undefined) => {
|
|
168
|
-
if (/^(?:https?:|mailto:|#)/iu.test(target)) return all;
|
|
169
|
-
if (target.startsWith('/') || target.includes('\\')) throw new Error(`ZCode unsupported absolute resource reference: ${label}`);
|
|
170
|
-
const absolute = resolve(source, target); const rel = relative(source, absolute);
|
|
171
|
-
if (rel === '' || rel.startsWith('..') || !existsSync(absolute) || !statSync(absolute).isFile()) throw new Error(`ZCode resource reference escapes or is missing: ${label} (${target})`);
|
|
172
|
-
return `${text}(${join(resources, rel)}${suffix ?? ''})`;
|
|
173
|
-
});
|
|
174
|
-
return rewritten.replace(/\u0000(\d+)\u0000/gu, (_, index: string) => spans[Number(index)] ?? '');
|
|
175
|
-
}
|
|
176
|
-
function validateCommand(raw: string, file: string): void {
|
|
177
|
-
const fm = frontmatter(raw, file);
|
|
178
|
-
for (const key of Object.keys(fm.values)) if (!['description', 'argument-hint', 'disable-noninteractive', 'model', 'skills'].includes(key)) throw new Error(`ZCode unsupported command metadata ${key}: ${file}`);
|
|
179
|
-
if (/(?:^|\n)\s*!|!`/u.test(raw.slice(fm.end))) throw new Error(`ZCode shell command expansion is unsupported: ${file}`);
|
|
180
|
-
}
|
|
181
|
-
function validateRootManifest(root: string): void {
|
|
182
|
-
const manifest = readJson(join(root, 'plugin.json'));
|
|
183
|
-
if (manifest === null || typeof manifest['name'] !== 'string') throw new Error(`ZCode projection requires plugin.json name: ${root}`);
|
|
184
|
-
for (const key of Object.keys(manifest)) if (['hooks', 'mcpServers', 'agents', 'agent', 'executables'].includes(key)) throw new Error(`ZCode root plugin semantic is unsupported: ${key}`);
|
|
185
|
-
}
|
|
186
|
-
function sourceName(root: string): string { const name = readJson(join(root, 'plugin.json'))?.['name']; if (typeof name !== 'string') throw new Error(`ZCode projection requires plugin.json name: ${root}`); return name; }
|
|
187
|
-
function readJson(file: string): Record<string, unknown> | null { try { const value: unknown = JSON.parse(readFileSync(file, 'utf8')); return isObject(value) ? value : null; } catch { return null; } }
|
|
188
|
-
function frontmatter(raw: string, file: string): { values: Record<string, unknown>; end: number } { const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u.exec(raw); if (match === null) throw new Error(`ZCode command or user-only skill needs YAML frontmatter: ${file}`); let value: unknown; try { value = Bun.YAML.parse(match[1] ?? ''); } catch { throw new Error(`ZCode invalid YAML frontmatter: ${file}`); } if (!isObject(value)) throw new Error(`ZCode YAML frontmatter must be a mapping: ${file}`); return { values: value, end: match[0].length }; }
|
|
189
|
-
function hasSkill(root: string): boolean { const dir = join(root, 'skills'); return existsSync(dir) && readdirSync(dir).some(name => existsSync(join(dir, name, 'SKILL.md'))); }
|
|
190
|
-
function hasCommand(root: string): boolean { const dir = join(root, 'commands'); return existsSync(dir) && commandNames(dir).length > 0; }
|
|
191
|
-
function commandNames(dir: string, prefix = ''): string[] { if (!existsSync(dir)) return []; const out: string[] = []; for (const name of readdirSync(dir)) { const path = join(dir, name); if (statSync(path).isDirectory()) out.push(...commandNames(path, prefix === '' ? name : `${prefix}:${name}`)); else if (name.endsWith('.md')) out.push(`${prefix === '' ? '' : `${prefix}:`}${name.slice(0, -3)}`); } return out; }
|
|
192
|
-
function rejectCommandCollisions(stagedMarket: string, nativeId: string, ownPrior?: string): void {
|
|
193
|
-
const candidate = commandNames(join(stagedMarket, 'plugins'));
|
|
194
|
-
const own = ownPrior === undefined ? undefined : resolve(ownPrior);
|
|
195
|
-
const enabled = readZcodeEnabledPluginIds();
|
|
196
|
-
for (const native of readZcodeNativeRecords()) {
|
|
197
|
-
if (enabled.get(native.id) !== true) continue;
|
|
198
|
-
if (own !== undefined && resolve(native.installPath) === own) continue;
|
|
199
|
-
const root = zcodeSafeInstallRoot(native.installPath); if (root === undefined) throw new Error(`Official ZCode native root is unsafe: ${native.installPath}`);
|
|
200
|
-
const overlap = commandNames(join(root, 'commands')).find(name => candidate.includes(name));
|
|
201
|
-
if (overlap !== undefined) throw new Error(`ZCode command ${overlap} collides with enabled native plugin ${native.id}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
function nativeById(id: string) { const rows = readZcodeNativeRecords().filter(row => row.id === id); if (rows.length > 1) throw new Error(`Official ZCode native id is ambiguous: ${id}`); return rows[0]; }
|
|
205
|
-
function nativeForLogical(logicalId: string) {
|
|
206
|
-
const rows = readZcodeNativeRecords().filter(row => zcodeSafeInstallRoot(row.installPath) !== undefined && readZcodeOwnership(row.installPath)?.logicalId === logicalId && readZcodeOwnership(row.installPath)?.nativeId === row.id);
|
|
207
|
-
if (rows.length > 1) throw new Error(`Official ZCode logical install is ambiguous: ${logicalId}`);
|
|
208
|
-
if (rows[0]?.scope !== undefined && rows[0].scope !== 'user') throw new Error(`Official ZCode workspace-scoped install is unsupported: ${logicalId}`);
|
|
209
|
-
return rows[0];
|
|
210
|
-
}
|
|
211
|
-
function sameCandidate(prior: NonNullable<ReturnType<typeof nativeById>>, market: string, resources: string, resourceRoot: string, logical: string, native: string, source: string, fingerprint: string): boolean {
|
|
212
|
-
const marker = readZcodeOwnership(prior.installPath);
|
|
213
|
-
if (marker?.logicalId !== logical || marker.nativeId !== native || marker.source !== source || marker.fingerprint !== fingerprint || marker.resourcePath !== resourceRoot || !existsSync(resourceRoot)) return false;
|
|
214
|
-
try { return fingerprintTree(join(market, 'plugins', prior.name)) === fingerprintTree(prior.installPath) && fingerprintTree(resources) === fingerprintTree(resourceRoot); }
|
|
215
|
-
catch { return false; }
|
|
216
|
-
}
|
|
217
|
-
function assertReplaceableOwnedRoots(market: string, resources: string, name: string, prior: ReturnType<typeof nativeById>, logical: string, native: string, source: string): void {
|
|
218
|
-
if (!existsSync(market) && !existsSync(resources)) return;
|
|
219
|
-
if (prior === undefined) throw new Error(`Official ZCode owned path already exists without a proven native install: ${existsSync(market) ? market : resources}`);
|
|
220
|
-
const priorRoot = zcodeSafeInstallRoot(prior.installPath); const marker = priorRoot === undefined ? null : readZcodeOwnership(priorRoot);
|
|
221
|
-
if (marker?.logicalId !== logical || marker.nativeId !== native || marker.source !== source) throw new Error(`Official ZCode existing paths are not proven owned: ${native}`);
|
|
222
|
-
if (existsSync(market)) {
|
|
223
|
-
assertNoSymlinks(market); const staged = readZcodeOwnership(join(market, 'plugins', name));
|
|
224
|
-
if (staged?.logicalId !== logical || staged.nativeId !== native || staged.source !== source) throw new Error(`Official ZCode marketplace root is not proven owned: ${market}`);
|
|
225
|
-
}
|
|
226
|
-
if (existsSync(resources) && marker.resourcePath !== resources) throw new Error(`Official ZCode resource root is not proven owned: ${resources}`);
|
|
227
|
-
}
|
|
228
|
-
function provePrior(prior: ReturnType<typeof nativeById>, logical: string, native: string, source: string): void { if (prior === undefined) return; const root = zcodeSafeInstallRoot(prior.installPath); const marker = root === undefined ? null : readZcodeOwnership(root); if (prior.scope !== 'user' || marker?.logicalId !== logical || marker.nativeId !== native || marker.source !== source) throw new Error(`Official ZCode ${native} is not a proven user-scoped plgnz-owned install`); }
|
|
229
|
-
function proveActive(native: string, logical: string, source: string, fingerprint: string, version: string, expectedPackage: string, expectedResources: string, expectedResourceFingerprint: string): void { const row = nativeById(native); const root = row === undefined ? undefined : zcodeSafeInstallRoot(row.installPath); const marker = root === undefined ? null : readZcodeOwnership(root); if (row === undefined || root === undefined || row.version !== version || readZcodeEnabledPluginIds().get(native) !== true || marker?.logicalId !== logical || marker.nativeId !== native || marker.source !== source || marker.fingerprint !== fingerprint || marker.resourcePath !== expectedResources || fingerprintTree(root) !== fingerprintTree(expectedPackage) || fingerprintTree(expectedResources) !== expectedResourceFingerprint) throw new Error(`Official ZCode native readback did not activate ${native}`); }
|
|
230
|
-
function restorePrior(native: string, market: string, prior: NonNullable<ReturnType<typeof nativeById>>): void { runOfficialZcode(['plugins', 'marketplace', 'update', market]); runOfficialZcode(['plugins', 'update', native]); const restored = nativeById(native); if (restored?.version !== prior.version || readZcodeEnabledPluginIds().get(native) !== true) throw new Error(`Official ZCode could not restore prior ${native}`); }
|
|
231
|
-
function removeCandidate(native: string, logical: string, source: string, fingerprint: string): void { const row = nativeById(native); const marker = row === undefined ? null : readZcodeOwnership(row.installPath); if (row !== undefined && marker?.logicalId === logical && marker.source === source && marker.fingerprint === fingerprint) runOfficialZcode(['plugins', 'uninstall', native, '--force']); }
|
|
232
|
-
function versionFor(plugin: PluginSource, fingerprint: string): string { const canonical = plugin.version ?? '0.0.0'; if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(canonical)) throw new Error(`ZCode needs canonical semver before plgnz metadata: ${canonical}`); return `${canonical}+plgnz.${fingerprint.slice(0, 16).toLowerCase()}`; }
|
|
233
|
-
function ownedMarketplace(logical: string): string { const h = new Bun.CryptoHasher('sha256'); h.update(logical); return `plgnz-${h.digest('hex').slice(0, 16)}`; }
|
|
234
|
-
function requireFingerprint(plugin: PluginSource): string { const fp = plugin.contentFingerprint ?? fingerprintTree(plugin.dir); if (!/^[0-9a-f]{16,}$/iu.test(fp)) throw new Error('ZCode requires a content fingerprint'); return fp.toLowerCase(); }
|
|
235
|
-
function moveAside(path: string): { rollback(): void; commit(): void } { if (!existsSync(path)) return { rollback: () => {}, commit: () => {} }; const backup = `${path}.plgnz-backup-${Date.now()}`; renameSync(path, backup); return { rollback: () => { if (!existsSync(path)) renameSync(backup, path); }, commit: () => rmSync(backup, { recursive: true, force: true }) }; }
|
|
236
|
-
function assertName(value: string, label: string): void { if (!/^[a-z0-9][a-z0-9._-]*$/iu.test(value)) throw new Error(`ZCode unsafe ${label}: ${value}`); }
|
|
237
|
-
function argumentHint(value: unknown, file: string): string { if (typeof value === 'string') return JSON.stringify(value); if (Array.isArray(value) && value.every(item => typeof item === 'string')) return `[${value.join(' ')}]`; throw new Error(`ZCode argument-hint must be text or text list: ${file}`); }
|
|
238
|
-
function assertNoSymlinks(root: string): void { const visit = (path: string): void => { const stat = lstatSync(path); if (stat.isSymbolicLink()) throw new Error(`ZCode source contains symlink: ${path}`); if (stat.isDirectory()) for (const child of readdirSync(path)) visit(join(path, child)); }; visit(root); }
|
|
239
|
-
function isObject(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
package/src/hosts/zcode-cli.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Official ZCode CLI reader.
|
|
3
|
-
*
|
|
4
|
-
* `doctor` must be read-only, so this module reads ZCode's installed registry
|
|
5
|
-
* directly. It never invokes `plugins list`: that command initializes bundled
|
|
6
|
-
* plugins in an otherwise empty store (evidence 2026-09-23).
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
9
|
-
import { dirname, join, resolve } from 'node:path';
|
|
10
|
-
import type { HostReader, InstalledPlugin, McpServerEntry } from '../host';
|
|
11
|
-
import { homeRoot, zcodeCliConfigRoot, zcodeCliRoot, zcodeStorageRoot } from '../paths';
|
|
12
|
-
|
|
13
|
-
declare const Bun: any;
|
|
14
|
-
declare const TextDecoder: any;
|
|
15
|
-
|
|
16
|
-
type NativeRecord = { id: string; name: string; marketplace: string; version: string; installPath: string; scope: 'user' | 'workspace' };
|
|
17
|
-
type Ownership = { owner: 'plgnz'; schema: 1; logicalId: string; nativeId: string; fingerprint: string; source: string; resourcePath?: string };
|
|
18
|
-
|
|
19
|
-
export function zcodeRegistryFile(): string { return join(zcodeCliRoot(), 'plugins', 'installed_plugins.json'); }
|
|
20
|
-
export function zcodeMarketplaceRoot(): string { return join(zcodeCliRoot(), 'plgnz-marketplaces'); }
|
|
21
|
-
export function zcodeResourceRoot(): string { return join(zcodeCliRoot(), 'plgnz-resources'); }
|
|
22
|
-
export function zcodeCliBinary(): string | undefined { return process.env['OPEN_PLUGIN_ZCODE_CLI_BIN']; }
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Candidate official CLI executables, most authoritative first. An explicit
|
|
26
|
-
* `OPEN_PLUGIN_ZCODE_CLI_BIN` override is exclusive — candidates stop at it so
|
|
27
|
-
* isolated roots and tests never reach a real desktop bundle. Without an
|
|
28
|
-
* override the desktop app's bundled official CLI is probed: it reports the
|
|
29
|
-
* same official identity (`doctor --json` → zcode/zcode-cli, bare-semver
|
|
30
|
-
* `--version`, executable node shebang) and shares the `~/.zcode/cli` store
|
|
31
|
-
* (evidence 2026-10-07). An isolated `OPEN_PLUGIN_HOME` never falls through to
|
|
32
|
-
* a system-wide bundle.
|
|
33
|
-
*/
|
|
34
|
-
export function zcodeCliBinaryCandidates(env: Record<string, string | undefined> = process.env): string[] {
|
|
35
|
-
const override = env['OPEN_PLUGIN_ZCODE_CLI_BIN'];
|
|
36
|
-
if (override !== undefined && override.length > 0) return [override];
|
|
37
|
-
const homeBundle = join(homeRoot(), 'Applications', 'ZCode.app', 'Contents', 'Resources', 'glm', 'zcode.cjs');
|
|
38
|
-
const systemBundle = '/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs';
|
|
39
|
-
const isolatedHome = process.env['OPEN_PLUGIN_HOME'];
|
|
40
|
-
if (process.platform !== 'darwin') return [];
|
|
41
|
-
if (isolatedHome !== undefined && isolatedHome.length > 0) return [homeBundle];
|
|
42
|
-
return [homeBundle, systemBundle].filter((path, index, all) => all.indexOf(path) === index);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Official CLI gets its storage parent through this supported test override. */
|
|
46
|
-
export function zcodeCliEnv(): Record<string, string | undefined> {
|
|
47
|
-
// Never inherit a caller's real HOME/storage into tests or an explicit root.
|
|
48
|
-
return { ...process.env, HOME: homeRoot(), ZCODE_STORAGE_DIR: zcodeStorageRoot() };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function readZcodeNativeRecords(): NativeRecord[] {
|
|
52
|
-
const file = zcodeRegistryFile();
|
|
53
|
-
if (!existsSync(file)) return [];
|
|
54
|
-
let value: unknown;
|
|
55
|
-
try { value = JSON.parse(readFileSync(file, 'utf8')); } catch { return []; }
|
|
56
|
-
if (!isObject(value) || value['version'] !== 1 || !Array.isArray(value['plugins'])) return [];
|
|
57
|
-
const out: NativeRecord[] = [];
|
|
58
|
-
for (const row of value['plugins']) {
|
|
59
|
-
if (!isObject(row)) continue;
|
|
60
|
-
const id = text(row['id']); const name = text(row['name']); const marketplace = text(row['marketplace']);
|
|
61
|
-
const version = text(row['version']); const installPath = text(row['installPath']);
|
|
62
|
-
const scope = row['scope'];
|
|
63
|
-
if (id === undefined || name === undefined || marketplace === undefined || version === undefined || installPath === undefined || (scope !== 'user' && scope !== 'workspace')) throw new Error(`Official ZCode registry has an unsupported plugin record: ${file}`);
|
|
64
|
-
out.push({ id, name, marketplace, version, installPath, scope });
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Writers fail closed rather than mistaking a corrupt native registry for an empty store. */
|
|
70
|
-
export function assertZcodeNativeRegistryReadable(): void {
|
|
71
|
-
const file = zcodeRegistryFile();
|
|
72
|
-
if (!existsSync(file)) return;
|
|
73
|
-
let value: unknown;
|
|
74
|
-
try { value = JSON.parse(readFileSync(file, 'utf8')); } catch { throw new Error(`Official ZCode registry is invalid: ${file}`); }
|
|
75
|
-
if (!isObject(value) || value['version'] !== 1 || !Array.isArray(value['plugins']) || value['plugins'].some(row => !isObject(row) || (row['scope'] !== 'user' && row['scope'] !== 'workspace'))) throw new Error(`Official ZCode registry is unsupported: ${file}`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function readZcodeOwnership(root: string): Ownership | null {
|
|
79
|
-
const file = join(root, '.plgnz-install.json');
|
|
80
|
-
try {
|
|
81
|
-
if (lstatSync(file).isSymbolicLink()) return null;
|
|
82
|
-
const value: unknown = JSON.parse(readFileSync(file, 'utf8'));
|
|
83
|
-
if (!isObject(value) || value['owner'] !== 'plgnz' || value['schema'] !== 1) return null;
|
|
84
|
-
const logicalId = text(value['logicalId']); const nativeId = text(value['nativeId']); const fingerprint = text(value['fingerprint']); const source = text(value['source']);
|
|
85
|
-
const resourcePath = text(value['resourcePath']);
|
|
86
|
-
return logicalId === undefined || nativeId === undefined || fingerprint === undefined || source === undefined ? null : { owner: 'plgnz', schema: 1, logicalId, nativeId, fingerprint, source, ...(resourcePath === undefined ? {} : { resourcePath }) };
|
|
87
|
-
} catch { return null; }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Detection requires an official terminal CLI binary and official doctor shape. */
|
|
91
|
-
function isOfficialZcodeCliBinary(binary: string): boolean {
|
|
92
|
-
if (!existsSync(binary)) return false;
|
|
93
|
-
try {
|
|
94
|
-
const versionResult = Bun.spawnSync([binary, '--version'], { stdout: 'pipe', stderr: 'pipe', env: zcodeCliEnv(), timeout: 10_000 });
|
|
95
|
-
if (versionResult.exitCode !== 0 || !(versionResult.stdout instanceof Uint8Array)) return false;
|
|
96
|
-
const version = new TextDecoder().decode(versionResult.stdout).trim();
|
|
97
|
-
if (/zcode-app-cli/iu.test(version) || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(version)) return false;
|
|
98
|
-
const result = Bun.spawnSync([binary, 'doctor', '--json'], { stdout: 'pipe', stderr: 'pipe', env: zcodeCliEnv(), timeout: 10_000 });
|
|
99
|
-
if (result.exitCode !== 0 || !(result.stdout instanceof Uint8Array)) return false;
|
|
100
|
-
const stdout = new TextDecoder().decode(result.stdout);
|
|
101
|
-
if (/zcode-app-cli/iu.test(stdout)) return false;
|
|
102
|
-
const value: unknown = JSON.parse(stdout);
|
|
103
|
-
if (!isObject(value) || !isObject(value['cli'])) return false;
|
|
104
|
-
return value['cli']['name'] === 'zcode' && value['cli']['processName'] === 'zcode-cli';
|
|
105
|
-
} catch { return false; }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The first candidate that exists and passes the official-identity gate. */
|
|
109
|
-
export function resolveOfficialZcodeCli(): string | undefined {
|
|
110
|
-
return zcodeCliBinaryCandidates().find((binary) => isOfficialZcodeCliBinary(binary));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function isOfficialZcodeCli(): boolean {
|
|
114
|
-
return resolveOfficialZcodeCli() !== undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function runOfficialZcode(args: string[]): string {
|
|
118
|
-
const binary = resolveOfficialZcodeCli();
|
|
119
|
-
if (binary === undefined) throw new Error('Official ZCode CLI required: set OPEN_PLUGIN_ZCODE_CLI_BIN to a binary whose doctor --json reports zcode/zcode-cli, or install the ZCode desktop app (its bundled CLI is probed automatically on darwin)');
|
|
120
|
-
const result = Bun.spawnSync([binary, ...args], { stdout: 'pipe', stderr: 'pipe', env: zcodeCliEnv(), timeout: 30_000 });
|
|
121
|
-
const stdout = result.stdout instanceof Uint8Array ? new TextDecoder().decode(result.stdout) : '';
|
|
122
|
-
const stderr = result.stderr instanceof Uint8Array ? new TextDecoder().decode(result.stderr) : '';
|
|
123
|
-
if (result.exitCode !== 0) throw new Error(`Official ZCode CLI ${args.join(' ')} failed: ${(stderr || stdout).trim()}`);
|
|
124
|
-
return stdout;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export const zcodeCli: HostReader = {
|
|
128
|
-
id: 'zcode-cli',
|
|
129
|
-
gui: false,
|
|
130
|
-
detect: isOfficialZcodeCli,
|
|
131
|
-
stores: () => [join(zcodeCliRoot(), 'plugins', 'cache'), zcodeMarketplaceRoot(), zcodeResourceRoot()],
|
|
132
|
-
listInstalled(): InstalledPlugin[] {
|
|
133
|
-
const enabled = readZcodeEnabledPluginIds();
|
|
134
|
-
const seen = new Set<string>();
|
|
135
|
-
return readZcodeNativeRecords().map((native) => {
|
|
136
|
-
const root = zcodeSafeInstallRoot(native.installPath);
|
|
137
|
-
const marker = root === undefined ? null : readZcodeOwnership(root);
|
|
138
|
-
const owned = marker !== null && marker.nativeId === native.id;
|
|
139
|
-
const id = owned ? marker.logicalId : native.id;
|
|
140
|
-
if (seen.has(id)) throw new Error(`Official ZCode installed identity is ambiguous: ${id}`);
|
|
141
|
-
seen.add(id);
|
|
142
|
-
const at = id.indexOf('@');
|
|
143
|
-
return {
|
|
144
|
-
id,
|
|
145
|
-
name: owned ? native.name : at < 0 ? id : id.slice(0, at),
|
|
146
|
-
...(owned ? { marketplace: marker.logicalId.includes('@') ? marker.logicalId.slice(marker.logicalId.indexOf('@') + 1) : undefined } : { marketplace: native.marketplace }),
|
|
147
|
-
...(root === undefined ? {} : { path: root, contentRoots: contentRoots(root, marker) }),
|
|
148
|
-
version: native.version,
|
|
149
|
-
enabled: enabled.get(native.id) === true,
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
},
|
|
153
|
-
mcpEntries: (): McpServerEntry[] => [],
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
export function zcodeSafeInstallRoot(path: string): string | undefined {
|
|
157
|
-
try {
|
|
158
|
-
const root = canonical(path); const cache = canonical(join(zcodeCliRoot(), 'plugins', 'cache'));
|
|
159
|
-
if (!root.startsWith(`${cache}/`) || lstatSync(root).isSymbolicLink()) return undefined;
|
|
160
|
-
return root;
|
|
161
|
-
} catch { return undefined; }
|
|
162
|
-
}
|
|
163
|
-
function canonical(path: string): string {
|
|
164
|
-
return realpathSync(path);
|
|
165
|
-
}
|
|
166
|
-
function contentRoots(native: string, marker: Ownership | null): Record<string, string> {
|
|
167
|
-
const roots: Record<string, string> = { native };
|
|
168
|
-
if (marker?.resourcePath !== undefined) {
|
|
169
|
-
try {
|
|
170
|
-
const resource = canonical(marker.resourcePath); const owned = canonical(zcodeResourceRoot());
|
|
171
|
-
if (resource.startsWith(`${owned}/`)) roots['resources'] = resource;
|
|
172
|
-
} catch { /* missing resources are reported by doctor through native fingerprint drift */ }
|
|
173
|
-
}
|
|
174
|
-
return roots;
|
|
175
|
-
}
|
|
176
|
-
export function readZcodeEnabledPluginIds(): Map<string, boolean> {
|
|
177
|
-
const result = new Map<string, boolean>(); let globallyEnabled = true;
|
|
178
|
-
for (const config of effectiveConfigPaths()) {
|
|
179
|
-
let value: unknown; try { value = JSON.parse(readFileSync(config, 'utf8')); } catch { continue; }
|
|
180
|
-
if (!isObject(value) || !isObject(value['plugins'])) continue;
|
|
181
|
-
const plugins = value['plugins'];
|
|
182
|
-
if (typeof plugins['enabled'] === 'boolean') globallyEnabled = plugins['enabled'];
|
|
183
|
-
if (isObject(plugins['enabledPlugins'])) for (const [id, enabled] of Object.entries(plugins['enabledPlugins'])) if (typeof enabled === 'boolean') result.set(id, enabled);
|
|
184
|
-
}
|
|
185
|
-
if (!globallyEnabled) for (const id of result.keys()) result.set(id, false);
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
function effectiveConfigPaths(): string[] {
|
|
189
|
-
const paths = [join(zcodeCliConfigRoot(), 'config.json')]; const dirs: string[] = []; let current = resolve(process.cwd()); let foundWorktree = false;
|
|
190
|
-
while (true) { dirs.push(current); if (existsSync(join(current, '.git'))) { foundWorktree = true; break; } const parent = dirname(current); if (parent === current) break; current = parent; }
|
|
191
|
-
for (const dir of (foundWorktree ? dirs.reverse() : [dirs[0]!])) paths.push(join(dir, 'zcode.json'), join(dir, '.zcode', 'config.json'));
|
|
192
|
-
return paths.filter(existsSync);
|
|
193
|
-
}
|
|
194
|
-
function text(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined; }
|
|
195
|
-
function isObject(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|