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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugnz",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Install, diagnose and update agent plugins (Agent Plugins spec) and MCP configs across Claude Code, Codex, Cursor, Kimi and more",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -9,23 +9,26 @@
|
|
|
9
9
|
"url": "git+https://github.com/archeism/plugnz.git"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
|
-
"plugnz": "
|
|
13
|
-
"plgnz": "
|
|
12
|
+
"plugnz": "dist/plugnz.mjs",
|
|
13
|
+
"plgnz": "dist/plugnz.mjs"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"test": "bun test",
|
|
17
|
-
"check": "tsc --noEmit"
|
|
17
|
+
"check": "tsc --noEmit",
|
|
18
|
+
"build": "bun scripts/build.mjs",
|
|
19
|
+
"prepublishOnly": "bun run build"
|
|
18
20
|
},
|
|
19
21
|
"files": [
|
|
20
|
-
"
|
|
21
|
-
"src",
|
|
22
|
+
"dist",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"smol-toml": "^1.8.0"
|
|
27
|
-
},
|
|
28
26
|
"devDependencies": {
|
|
29
|
-
"typescript": "^7.0.2"
|
|
27
|
+
"typescript": "^7.0.2",
|
|
28
|
+
"smol-toml": "^1.8.0",
|
|
29
|
+
"yaml": "2"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
30
33
|
}
|
|
31
34
|
}
|
package/bin/plgnz.mjs
DELETED
package/bin/plugnz.mjs
DELETED
package/src/cli.ts
DELETED
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI entrypoint. Verbs: add, doctor, pin, update, list, remove, targets
|
|
3
|
-
* (AGENTS.md verbs list).
|
|
4
|
-
*/
|
|
5
|
-
import { runDoctor, formatFinding, type DoctorFinding } from './doctor';
|
|
6
|
-
import { hosts } from './hosts';
|
|
7
|
-
import { cleanupWriters, writers } from './hosts/writers';
|
|
8
|
-
import { resolveSource } from './source';
|
|
9
|
-
import { findRecord, readState } from './state';
|
|
10
|
-
import { writeState } from './state-write';
|
|
11
|
-
import type { InstallRecord } from './state';
|
|
12
|
-
import { runPin } from './pin';
|
|
13
|
-
import { runUpdate } from './update';
|
|
14
|
-
import { fingerprintInstallation } from './fingerprint';
|
|
15
|
-
import type { HostReader, InstallOutcome } from './host';
|
|
16
|
-
import { consumerProfiles, findConsumerProfile, type ConsumerProfile } from './consumer-profiles';
|
|
17
|
-
import { CompatibilityError, requireCompatible } from './compatibility';
|
|
18
|
-
import packageJson from '../package.json' with { type: 'json' };
|
|
19
|
-
|
|
20
|
-
const USAGE = `plugnz — install, diagnose and update agent plugins and MCP configs
|
|
21
|
-
|
|
22
|
-
usage: plugnz <verb> [options]
|
|
23
|
-
|
|
24
|
-
verbs:
|
|
25
|
-
add <source> [--target <host>…] [--adopt-existing] install a plugin into each host's native store
|
|
26
|
-
doctor [--json] dead commands, shadowed entries, stale installs (read-only)
|
|
27
|
-
pin [--target <host>] [--all] rewrite bare commands to absolute paths for GUI hosts
|
|
28
|
-
(default targets: the GUI hosts; --all for every host)
|
|
29
|
-
update [name] [--dry-run] idempotent re-add from state.json; re-materializes
|
|
30
|
-
copy-based hosts and re-applies recorded pins
|
|
31
|
-
list list installed plugins per host
|
|
32
|
-
remove <plugin> remove an installed plugin
|
|
33
|
-
targets [--all] list detected agent hosts; --all includes frozen consumer profiles
|
|
34
|
-
`;
|
|
35
|
-
|
|
36
|
-
/** Flags shared by `pin` and `update`. */
|
|
37
|
-
interface VerbFlags {
|
|
38
|
-
positionals: string[];
|
|
39
|
-
targets: string[];
|
|
40
|
-
plugins: string[];
|
|
41
|
-
all: boolean;
|
|
42
|
-
dryRun: boolean;
|
|
43
|
-
adoptExisting: boolean;
|
|
44
|
-
errors: string[];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseFlags(args: string[]): VerbFlags {
|
|
48
|
-
const flags: VerbFlags = { positionals: [], targets: [], plugins: [], all: false, dryRun: false, adoptExisting: false, errors: [] };
|
|
49
|
-
for (let i = 0; i < args.length; i++) {
|
|
50
|
-
const arg = args[i];
|
|
51
|
-
if (arg === '--target' || arg === '-t') {
|
|
52
|
-
const value = args[i + 1];
|
|
53
|
-
if (value !== undefined && !value.startsWith('-')) {
|
|
54
|
-
flags.targets.push(value);
|
|
55
|
-
i++;
|
|
56
|
-
} else flags.errors.push(`${arg} requires a value`);
|
|
57
|
-
} else if (arg === '--plugin') {
|
|
58
|
-
const value = args[i + 1];
|
|
59
|
-
if (value !== undefined && !value.startsWith('-')) {
|
|
60
|
-
flags.plugins.push(value);
|
|
61
|
-
i++;
|
|
62
|
-
} else flags.errors.push('--plugin requires a value');
|
|
63
|
-
} else if (arg === '--all') {
|
|
64
|
-
flags.all = true;
|
|
65
|
-
} else if (arg === '--dry-run') {
|
|
66
|
-
flags.dryRun = true;
|
|
67
|
-
} else if (arg === '--adopt-existing') {
|
|
68
|
-
flags.adoptExisting = true;
|
|
69
|
-
} else if (arg !== undefined && arg.startsWith('-')) {
|
|
70
|
-
flags.errors.push(`unknown option '${arg}'`);
|
|
71
|
-
} else if (arg !== undefined) {
|
|
72
|
-
flags.positionals.push(arg);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return flags;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function selectPlugins<T extends { name: string }>(plugins: readonly T[], requested: readonly string[]): { selected: T[]; error?: string } {
|
|
79
|
-
const duplicate = requested.find((name, index) => requested.indexOf(name) !== index);
|
|
80
|
-
if (duplicate !== undefined) return { selected: [], error: `duplicate plugin selector '${duplicate}'` };
|
|
81
|
-
const known = new Map(plugins.map((plugin) => [plugin.name, plugin]));
|
|
82
|
-
for (const name of requested) if (!known.has(name)) return { selected: [], error: `unknown plugin '${name}' (available: ${[...known.keys()].join(', ')})` };
|
|
83
|
-
return { selected: requested.length === 0 ? [...plugins] : requested.map((name) => known.get(name)!) };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function rejectDisallowed(flags: VerbFlags, allowed: ReadonlySet<'target' | 'plugin' | 'all' | 'dryRun' | 'adoptExisting'>): string | undefined {
|
|
87
|
-
if (flags.errors.length > 0) return flags.errors.join('; ');
|
|
88
|
-
if (flags.targets.length > 0 && !allowed.has('target')) return '--target is not supported by this verb';
|
|
89
|
-
if (flags.plugins.length > 0 && !allowed.has('plugin')) return '--plugin is only supported by add';
|
|
90
|
-
if (flags.all && !allowed.has('all')) return '--all is not supported by this verb';
|
|
91
|
-
if (flags.dryRun && !allowed.has('dryRun')) return '--dry-run is not supported by this verb';
|
|
92
|
-
if (flags.adoptExisting && !allowed.has('adoptExisting')) return '--adopt-existing is only supported by add';
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Findings print the same way doctor's do: `<host> <mark> <message>`. */
|
|
97
|
-
function printFindings(findings: readonly DoctorFinding[], json: boolean): void {
|
|
98
|
-
if (json) console.log(JSON.stringify(findings, null, 2));
|
|
99
|
-
else for (const f of findings) console.log(formatFinding(f));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function printOutcomes(outcomes: readonly InstallOutcome[], json: boolean): void {
|
|
103
|
-
if (json) console.log(JSON.stringify(outcomes, null, 2));
|
|
104
|
-
else for (const outcome of outcomes) console.log(`${outcome.target}\t${outcome.status}\t${outcome.plugin}${outcome.diagnostic ? `\t${outcome.diagnostic}` : ''}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function select<T extends HostReader>(available: readonly T[], targets: readonly string[]): { selected: T[]; error?: string } {
|
|
108
|
-
const known = new Map(available.map((host) => [host.id, host]));
|
|
109
|
-
const duplicate = targets.find((target, index) => targets.indexOf(target) !== index);
|
|
110
|
-
if (duplicate !== undefined) return { selected: [], error: `duplicate target '${duplicate}'` };
|
|
111
|
-
for (const target of targets) if (!known.has(target)) return { selected: [], error: `unknown target '${target}' (known: ${[...known.keys()].join(', ')})` };
|
|
112
|
-
const selected = targets.length === 0 ? [...available] : targets.map((target) => known.get(target)!);
|
|
113
|
-
const absent = selected.find((host) => !host.detect());
|
|
114
|
-
if (absent !== undefined && targets.length > 0) return { selected: [], error: `requested target '${absent.id}' is not present on this machine` };
|
|
115
|
-
return { selected: selected.filter((host) => host.detect()) };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function selectProfiles(targets: readonly string[]): { selected: ConsumerProfile[]; error?: string } {
|
|
119
|
-
const duplicate = targets.find((target, index) => targets.indexOf(target) !== index);
|
|
120
|
-
if (duplicate !== undefined) return { selected: [], error: `duplicate target '${duplicate}'` };
|
|
121
|
-
const selected = targets.length === 0
|
|
122
|
-
? writers.filter((writer) => writer.detect()).map((writer) => findConsumerProfile(writer.id)!).filter((profile): profile is ConsumerProfile => profile !== undefined)
|
|
123
|
-
: targets.map(findConsumerProfile);
|
|
124
|
-
const unknownIndex = selected.findIndex((profile) => profile === undefined);
|
|
125
|
-
if (unknownIndex !== -1) return { selected: [], error: `unknown target '${targets[unknownIndex]}' (known: ${consumerProfiles.map((profile) => profile.id).join(', ')})` };
|
|
126
|
-
return { selected: selected as ConsumerProfile[] };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function compatibilityOutcomes(profiles: readonly ConsumerProfile[], plugins: readonly string[], action: 'install' | 'update', dryRun: boolean): InstallOutcome[] | undefined {
|
|
130
|
-
const refusals = new Map<string, CompatibilityError>();
|
|
131
|
-
for (const profile of profiles) {
|
|
132
|
-
try { requireCompatible(profile, action); }
|
|
133
|
-
catch (error) { if (error instanceof CompatibilityError) refusals.set(profile.id, error); else throw error; }
|
|
134
|
-
}
|
|
135
|
-
if (refusals.size === 0) return undefined;
|
|
136
|
-
const diagnostic = 'not attempted because another selected target could not be admitted';
|
|
137
|
-
return profiles.flatMap((profile) => plugins.map((plugin) => {
|
|
138
|
-
const refusal = refusals.get(profile.id);
|
|
139
|
-
return {
|
|
140
|
-
plugin,
|
|
141
|
-
target: profile.id,
|
|
142
|
-
status: refusal?.status ?? 'failed',
|
|
143
|
-
action,
|
|
144
|
-
dryRun,
|
|
145
|
-
diagnostic: refusal?.message ?? diagnostic,
|
|
146
|
-
} satisfies InstallOutcome;
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function withoutConsole<T>(fn: () => Promise<T>): Promise<T> {
|
|
151
|
-
const original = console.log;
|
|
152
|
-
console.log = () => {};
|
|
153
|
-
try { return await fn(); } finally { console.log = original; }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function fail(message: string, code: number): never {
|
|
157
|
-
console.error(message);
|
|
158
|
-
process.exit(code);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function main(argv: string[]): Promise<number> {
|
|
162
|
-
const args = argv.filter((a) => a !== '--json');
|
|
163
|
-
const json = argv.length !== args.length;
|
|
164
|
-
const verb = args[0];
|
|
165
|
-
|
|
166
|
-
if (verb === '--version' || verb === '-v' || verb === 'version') {
|
|
167
|
-
if (args.length > 1) fail('plugnz version: unexpected argument', 2);
|
|
168
|
-
console.log(json ? JSON.stringify({ name: 'plugnz', version: packageJson.version }) : packageJson.version);
|
|
169
|
-
return 0;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (verb === undefined || verb === 'help' || verb === '--help' || verb === '-h') {
|
|
173
|
-
console.log(USAGE);
|
|
174
|
-
return verb === undefined ? 2 : 0;
|
|
175
|
-
}
|
|
176
|
-
if (verb === 'doctor') {
|
|
177
|
-
const flags = parseFlags(args.slice(1));
|
|
178
|
-
if (rejectDisallowed(flags, new Set(['target'])) || flags.positionals.length > 0) fail(`plugnz doctor: unexpected argument`, 2);
|
|
179
|
-
const selection = select(hosts, flags.targets);
|
|
180
|
-
if (selection.error) {
|
|
181
|
-
if (json) printOutcomes(flags.targets.map((target) => ({ plugin: '*', target, status: 'failed', dryRun: false, diagnostic: selection.error })), true);
|
|
182
|
-
else console.error(`plugnz doctor: ${selection.error}`);
|
|
183
|
-
return 2;
|
|
184
|
-
}
|
|
185
|
-
const { findings, exitCode } = runDoctor(selection.selected);
|
|
186
|
-
if (json) console.log(JSON.stringify(findings, null, 2));
|
|
187
|
-
else for (const f of findings) console.log(formatFinding(f));
|
|
188
|
-
return exitCode;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (verb === 'targets') {
|
|
192
|
-
const flags = parseFlags(args.slice(1));
|
|
193
|
-
const disallowed = rejectDisallowed(flags, new Set(['all']));
|
|
194
|
-
if (disallowed || flags.positionals.length > 0) fail(`plugnz targets: ${disallowed ?? 'unexpected argument'}`, 2);
|
|
195
|
-
const present = hosts.filter(h => h.detect());
|
|
196
|
-
if (json) {
|
|
197
|
-
console.log(JSON.stringify(flags.all ? consumerProfiles : present.map(h => h.id), null, 2));
|
|
198
|
-
} else {
|
|
199
|
-
for (const h of flags.all ? consumerProfiles : present) console.log(h.id);
|
|
200
|
-
}
|
|
201
|
-
return 0;
|
|
202
|
-
}
|
|
203
|
-
if (verb === 'list') {
|
|
204
|
-
const flags = parseFlags(args.slice(1));
|
|
205
|
-
if (rejectDisallowed(flags, new Set(['target'])) || flags.positionals.length > 0) fail(`plugnz list: unexpected argument`, 2);
|
|
206
|
-
const selection = select(hosts, flags.targets);
|
|
207
|
-
if (selection.error) {
|
|
208
|
-
if (json) printOutcomes(flags.targets.map((target) => ({ plugin: '*', target, status: 'failed', dryRun: false, diagnostic: selection.error })), true);
|
|
209
|
-
else console.error(`plugnz list: ${selection.error}`);
|
|
210
|
-
return 2;
|
|
211
|
-
}
|
|
212
|
-
const state = readState();
|
|
213
|
-
const all: any[] = [];
|
|
214
|
-
for (const h of selection.selected) {
|
|
215
|
-
const installed = h.listInstalled();
|
|
216
|
-
const pending = state.filter((record) => record.host === h.id && record.pending !== undefined)
|
|
217
|
-
.map((record) => ({ id: record.id, action: record.pending }));
|
|
218
|
-
if (json) {
|
|
219
|
-
all.push({ host: h.id, plugins: installed, ...(pending.length > 0 ? { pending } : {}) });
|
|
220
|
-
} else {
|
|
221
|
-
for (const p of installed) {
|
|
222
|
-
console.log(`${h.id}\t${p.id}\t${p.version || p.sha || 'unknown'}`);
|
|
223
|
-
}
|
|
224
|
-
for (const record of pending) console.log(`${h.id}\t${record.id}\tpending:${record.action}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (json) console.log(JSON.stringify(all, null, 2));
|
|
228
|
-
return 0;
|
|
229
|
-
}
|
|
230
|
-
if (verb === 'remove') {
|
|
231
|
-
const flags = parseFlags(args.slice(1));
|
|
232
|
-
const target = flags.positionals[0];
|
|
233
|
-
if (rejectDisallowed(flags, new Set(['target', 'dryRun'])) || !target || flags.positionals.length > 1) fail(`plugnz remove: missing or unexpected plugin id`, 2);
|
|
234
|
-
const selection = select(cleanupWriters, flags.targets);
|
|
235
|
-
if (selection.error) {
|
|
236
|
-
printOutcomes(flags.targets.map((host) => ({ plugin: target, target: host, status: 'failed', dryRun: flags.dryRun, diagnostic: selection.error })), json);
|
|
237
|
-
return 2;
|
|
238
|
-
}
|
|
239
|
-
if (selection.selected.length === 0) {
|
|
240
|
-
printOutcomes([{ plugin: target, target: '*', status: 'failed', action: 'remove', dryRun: flags.dryRun, diagnostic: 'No detected writer targets' }], json);
|
|
241
|
-
return 1;
|
|
242
|
-
}
|
|
243
|
-
let state: InstallRecord[];
|
|
244
|
-
try { state = readState(); }
|
|
245
|
-
catch (error) {
|
|
246
|
-
printOutcomes([{ plugin: target, target: '*', status: 'failed', action: 'remove', dryRun: flags.dryRun, diagnostic: (error as Error).message }], json);
|
|
247
|
-
return 1;
|
|
248
|
-
}
|
|
249
|
-
const outcomes: InstallOutcome[] = [];
|
|
250
|
-
for (const w of selection.selected) {
|
|
251
|
-
const record = findRecord(state, w.id, target);
|
|
252
|
-
const owned = record !== undefined && (record.ownership === 'plgnz' || record.ownership === undefined);
|
|
253
|
-
if (!owned || record?.pending === 'install') {
|
|
254
|
-
outcomes.push({ plugin: target, target: w.id, status: 'failed', action: 'remove', dryRun: flags.dryRun, diagnostic: record?.pending === 'install' ? 'install is pending; refusing removal' : 'no owned install record; refusing removal' });
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (flags.dryRun) {
|
|
258
|
-
outcomes.push({ plugin: target, target: w.id, status: 'installed', action: 'remove', dryRun: true });
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
try {
|
|
262
|
-
const pending = { ...record, pending: 'remove' as const };
|
|
263
|
-
const pendingState = state.map((candidate) => candidate === record ? pending : candidate);
|
|
264
|
-
writeState(pendingState);
|
|
265
|
-
state = pendingState;
|
|
266
|
-
await w.remove(target);
|
|
267
|
-
const finalized = state.filter((candidate) => candidate !== pending);
|
|
268
|
-
writeState(finalized);
|
|
269
|
-
state = finalized;
|
|
270
|
-
outcomes.push({ plugin: target, target: w.id, status: 'installed', action: 'remove', dryRun: false });
|
|
271
|
-
} catch (error) {
|
|
272
|
-
outcomes.push({ plugin: target, target: w.id, status: 'failed', action: 'remove', dryRun: false, diagnostic: (error as Error).message });
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
printOutcomes(outcomes, json);
|
|
276
|
-
return outcomes.some((outcome) => outcome.status === 'failed') ? 1 : 0;
|
|
277
|
-
}
|
|
278
|
-
if (verb === 'add') {
|
|
279
|
-
const flags = parseFlags(args.slice(1));
|
|
280
|
-
const outcomes: InstallOutcome[] = [];
|
|
281
|
-
let activeTarget = '*';
|
|
282
|
-
let activePlugin = '*';
|
|
283
|
-
let expected: Array<{ plugin: string; target: string }> = [];
|
|
284
|
-
const disallowed = rejectDisallowed(flags, new Set(['target', 'plugin', 'dryRun', 'adoptExisting']));
|
|
285
|
-
if (disallowed) {
|
|
286
|
-
if (json) printOutcomes([{ plugin: '*', target: '*', status: 'failed', dryRun: flags.dryRun, diagnostic: disallowed }], true);
|
|
287
|
-
else console.error(`plugnz add: ${disallowed}`);
|
|
288
|
-
return 2;
|
|
289
|
-
}
|
|
290
|
-
if (flags.positionals.length > 1) fail(`plugnz add: unexpected argument: ${flags.positionals[1]}`, 2);
|
|
291
|
-
const sourceArg = flags.positionals[0];
|
|
292
|
-
if (sourceArg === undefined) fail(`plugnz add: missing source`, 2);
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
const profileSelection = selectProfiles(flags.targets);
|
|
296
|
-
if (profileSelection.error) {
|
|
297
|
-
printOutcomes(flags.targets.map((target) => ({ plugin: '*', target, status: 'failed', dryRun: flags.dryRun, diagnostic: profileSelection.error })), json);
|
|
298
|
-
return 2;
|
|
299
|
-
}
|
|
300
|
-
if (profileSelection.selected.some((profile) => profile.scope === 'excluded-standalone')) {
|
|
301
|
-
const incompatible = compatibilityOutcomes(profileSelection.selected, ['*'], 'install', flags.dryRun)!;
|
|
302
|
-
printOutcomes(incompatible, json);
|
|
303
|
-
return 1;
|
|
304
|
-
}
|
|
305
|
-
if (flags.adoptExisting) {
|
|
306
|
-
const incompatible = compatibilityOutcomes(profileSelection.selected, ['*'], 'install', flags.dryRun);
|
|
307
|
-
if (incompatible !== undefined) {
|
|
308
|
-
printOutcomes(incompatible, json);
|
|
309
|
-
return 1;
|
|
310
|
-
}
|
|
311
|
-
const adoptionSelection = flags.targets.length === 0
|
|
312
|
-
? select(writers, [])
|
|
313
|
-
: select(writers, profileSelection.selected.map((profile) => profile.id));
|
|
314
|
-
if (adoptionSelection.error) {
|
|
315
|
-
printOutcomes(profileSelection.selected.map((profile) => ({ plugin: '*', target: profile.id, status: 'failed', dryRun: flags.dryRun, diagnostic: adoptionSelection.error })), json);
|
|
316
|
-
return 2;
|
|
317
|
-
}
|
|
318
|
-
const unsupported = adoptionSelection.selected.find((writer) => writer.supportsAdoption !== true);
|
|
319
|
-
if (unsupported !== undefined) {
|
|
320
|
-
const diagnostic = `target '${unsupported.id}' does not support --adopt-existing`;
|
|
321
|
-
printOutcomes(adoptionSelection.selected.map((writer) => ({ plugin: '*', target: writer.id, status: 'unsupported' as const, action: 'install' as const, dryRun: flags.dryRun, diagnostic })), json);
|
|
322
|
-
return 2;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const resolved = resolveSource(sourceArg);
|
|
326
|
-
const pluginSelection = selectPlugins(resolved.plugins, flags.plugins);
|
|
327
|
-
if (pluginSelection.error) {
|
|
328
|
-
printOutcomes([{ plugin: '*', target: '*', status: 'failed', dryRun: flags.dryRun, diagnostic: pluginSelection.error }], json);
|
|
329
|
-
return 2;
|
|
330
|
-
}
|
|
331
|
-
const incompatible = compatibilityOutcomes(profileSelection.selected, pluginSelection.selected.map((plugin) => plugin.name), 'install', flags.dryRun);
|
|
332
|
-
if (incompatible !== undefined) {
|
|
333
|
-
printOutcomes(incompatible, json);
|
|
334
|
-
return 1;
|
|
335
|
-
}
|
|
336
|
-
const selection = flags.targets.length === 0
|
|
337
|
-
? select(writers, [])
|
|
338
|
-
: select(writers, profileSelection.selected.map((profile) => profile.id));
|
|
339
|
-
if (selection.error) {
|
|
340
|
-
printOutcomes(profileSelection.selected.map((profile) => ({ plugin: '*', target: profile.id, status: 'failed', dryRun: flags.dryRun, diagnostic: selection.error })), json);
|
|
341
|
-
return 2;
|
|
342
|
-
}
|
|
343
|
-
if (selection.selected.length === 0) throw new Error('No detected writer targets');
|
|
344
|
-
expected = selection.selected.flatMap((writer) => pluginSelection.selected.map((plugin) => ({ plugin: plugin.name, target: writer.id })));
|
|
345
|
-
activeTarget = expected[0]?.target ?? '*';
|
|
346
|
-
activePlugin = expected[0]?.plugin ?? '*';
|
|
347
|
-
let state = readState();
|
|
348
|
-
for (const w of selection.selected) {
|
|
349
|
-
activeTarget = w.id;
|
|
350
|
-
for (const plugin of pluginSelection.selected) {
|
|
351
|
-
activePlugin = plugin.name;
|
|
352
|
-
const nativeId = plugin.marketplace ? `${plugin.name}@${plugin.marketplace}` : plugin.name;
|
|
353
|
-
if (flags.dryRun) {
|
|
354
|
-
if (json) await withoutConsole(() => w.add(plugin, resolved, { dryRun: true, adoptExisting: flags.adoptExisting }));
|
|
355
|
-
else await w.add(plugin, resolved, { dryRun: true, adoptExisting: flags.adoptExisting });
|
|
356
|
-
outcomes.push({ plugin: plugin.name, target: w.id, status: 'installed', action: 'install', dryRun: true, nativeId });
|
|
357
|
-
} else {
|
|
358
|
-
const expectedIds = new Set([nativeId, plugin.name, `${plugin.name}@${plugin.marketplace ?? 'local'}`]);
|
|
359
|
-
const idx = state.findIndex(r => r.host === w.id && expectedIds.has(r.id));
|
|
360
|
-
const previous = idx !== -1 ? state[idx] : undefined;
|
|
361
|
-
const rec: InstallRecord = {
|
|
362
|
-
...(previous ?? {
|
|
363
|
-
host: w.id,
|
|
364
|
-
id: nativeId,
|
|
365
|
-
}),
|
|
366
|
-
source: resolved.sourceUri,
|
|
367
|
-
sourceSha: resolved.sha,
|
|
368
|
-
ownership: previous?.ownership ?? 'plgnz',
|
|
369
|
-
pending: 'install',
|
|
370
|
-
};
|
|
371
|
-
const pendingState = idx === -1 ? [...state, rec] : state.map((candidate) => candidate === previous ? rec : candidate);
|
|
372
|
-
writeState(pendingState);
|
|
373
|
-
state = pendingState;
|
|
374
|
-
const writerResult = await w.add(plugin, resolved, { dryRun: false, adoptExisting: flags.adoptExisting });
|
|
375
|
-
const installed = w.listInstalled();
|
|
376
|
-
const expectedMarketplace = plugin.marketplace ?? 'local';
|
|
377
|
-
const match = installed.find(p => expectedIds.has(p.id) && p.name === plugin.name &&
|
|
378
|
-
(p.marketplace === expectedMarketplace || (expectedMarketplace === 'local' && p.marketplace === undefined)) && p.enabled !== false);
|
|
379
|
-
if (match?.path === undefined) throw new Error(`native install readback is missing ${nativeId}`);
|
|
380
|
-
const finalized: InstallRecord = {
|
|
381
|
-
...rec,
|
|
382
|
-
id: match?.id ?? nativeId,
|
|
383
|
-
source: resolved.sourceUri,
|
|
384
|
-
sourceSha: resolved.sha,
|
|
385
|
-
installedAt: new Date().toISOString(),
|
|
386
|
-
sourceDir: plugin.dir,
|
|
387
|
-
installedFingerprint: fingerprintInstallation(match),
|
|
388
|
-
...(plugin.contentFingerprint !== undefined ? { fingerprint: plugin.contentFingerprint } : {}),
|
|
389
|
-
};
|
|
390
|
-
delete finalized.pending;
|
|
391
|
-
const finalizedState = state.map((candidate) => candidate === rec ? finalized : candidate);
|
|
392
|
-
writeState(finalizedState);
|
|
393
|
-
state = finalizedState;
|
|
394
|
-
outcomes.push({ plugin: plugin.name, target: w.id, status: writerResult === 'unchanged' ? 'unchanged' : 'installed', action: 'install', dryRun: false, nativeId: match.id });
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
printOutcomes(outcomes, json);
|
|
399
|
-
} catch (e: any) {
|
|
400
|
-
const reported = new Set(outcomes.map((outcome) => `${outcome.plugin}\u0000${outcome.target}`));
|
|
401
|
-
const failures: InstallOutcome[] = [];
|
|
402
|
-
if (expected.length === 0) {
|
|
403
|
-
failures.push({ plugin: '*', target: e instanceof CompatibilityError ? e.target : activeTarget, status: e instanceof CompatibilityError ? e.status : 'failed', action: 'install', dryRun: flags.dryRun, diagnostic: e.message });
|
|
404
|
-
} else {
|
|
405
|
-
for (const pair of expected) {
|
|
406
|
-
const key = `${pair.plugin}\u0000${pair.target}`;
|
|
407
|
-
if (reported.has(key)) continue;
|
|
408
|
-
failures.push({
|
|
409
|
-
plugin: pair.plugin,
|
|
410
|
-
target: pair.target,
|
|
411
|
-
status: e instanceof CompatibilityError && pair.target === e.target ? e.status : 'failed',
|
|
412
|
-
action: 'install',
|
|
413
|
-
dryRun: flags.dryRun,
|
|
414
|
-
diagnostic: pair.plugin === activePlugin && pair.target === activeTarget ? e.message : 'not attempted after an earlier install failure',
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (json) printOutcomes([...outcomes, ...failures], true);
|
|
419
|
-
else console.error(e.message);
|
|
420
|
-
return 1;
|
|
421
|
-
}
|
|
422
|
-
return 0;
|
|
423
|
-
}
|
|
424
|
-
if (verb === 'pin') {
|
|
425
|
-
const flags = parseFlags(args.slice(1));
|
|
426
|
-
if (rejectDisallowed(flags, new Set(['target', 'all', 'dryRun'])) || flags.positionals.length > 0) fail(`plugnz pin: unexpected argument: ${flags.positionals[0]}`, 2);
|
|
427
|
-
const known = new Set(writers.map((w) => w.id));
|
|
428
|
-
for (const target of flags.targets) {
|
|
429
|
-
if (!known.has(target)) {
|
|
430
|
-
fail(`plugnz pin: unknown target '${target}' (known: ${[...known].join(', ')})`, 2);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
const candidates = flags.targets.length > 0
|
|
434
|
-
? writers.filter((writer) => flags.targets.includes(writer.id))
|
|
435
|
-
: flags.all ? [...writers] : writers.filter((writer) => writer.gui);
|
|
436
|
-
const detected = candidates.filter((writer) => writer.detect());
|
|
437
|
-
if (detected.length === 0) {
|
|
438
|
-
console.error('plugnz pin: No detected writer targets');
|
|
439
|
-
return 1;
|
|
440
|
-
}
|
|
441
|
-
const result = await runPin({ targets: flags.targets, all: flags.all, dryRun: flags.dryRun, writers: detected });
|
|
442
|
-
printFindings(result.findings, json);
|
|
443
|
-
return result.exitCode;
|
|
444
|
-
}
|
|
445
|
-
if (verb === 'update') {
|
|
446
|
-
const flags = parseFlags(args.slice(1));
|
|
447
|
-
if (rejectDisallowed(flags, new Set(['target', 'dryRun'])) || flags.positionals.length > 1) fail(`plugnz update: unexpected argument: ${flags.positionals[1]}`, 2);
|
|
448
|
-
const profileSelection = selectProfiles(flags.targets);
|
|
449
|
-
if (profileSelection.error) {
|
|
450
|
-
printOutcomes(flags.targets.map((target) => ({ plugin: flags.positionals[0] ?? '*', target, status: 'failed', dryRun: flags.dryRun, diagnostic: profileSelection.error })), json);
|
|
451
|
-
return 2;
|
|
452
|
-
}
|
|
453
|
-
const incompatible = compatibilityOutcomes(profileSelection.selected, [flags.positionals[0] ?? '*'], 'update', flags.dryRun);
|
|
454
|
-
if (incompatible !== undefined) {
|
|
455
|
-
printOutcomes(incompatible, json);
|
|
456
|
-
return 1;
|
|
457
|
-
}
|
|
458
|
-
const selection = flags.targets.length === 0
|
|
459
|
-
? select(writers, [])
|
|
460
|
-
: select(writers, profileSelection.selected.map((profile) => profile.id));
|
|
461
|
-
if (selection.error) {
|
|
462
|
-
printOutcomes(profileSelection.selected.map((profile) => ({ plugin: flags.positionals[0] ?? '*', target: profile.id, status: 'failed', dryRun: flags.dryRun, diagnostic: selection.error })), json);
|
|
463
|
-
return 2;
|
|
464
|
-
}
|
|
465
|
-
if (selection.selected.length === 0) {
|
|
466
|
-
printOutcomes([{ plugin: flags.positionals[0] ?? '*', target: '*', status: 'failed', dryRun: flags.dryRun, diagnostic: 'No detected writer targets' }], json);
|
|
467
|
-
return 1;
|
|
468
|
-
}
|
|
469
|
-
try {
|
|
470
|
-
const result = json && flags.dryRun
|
|
471
|
-
? await withoutConsole(() => runUpdate(flags.positionals[0], { dryRun: true, writers: selection.selected }))
|
|
472
|
-
: await runUpdate(flags.positionals[0], { dryRun: flags.dryRun, writers: selection.selected });
|
|
473
|
-
if (json) printOutcomes(result.findings.map((finding) => ({ plugin: flags.positionals[0] ?? '*', target: finding.host, status: finding.status ?? (finding.mark === '✓' ? 'installed' : finding.mark === '!' ? 'unverified' : 'failed'), action: 'update', dryRun: flags.dryRun, diagnostic: finding.message })), true);
|
|
474
|
-
else printFindings(result.findings, false);
|
|
475
|
-
return result.exitCode;
|
|
476
|
-
} catch (error) {
|
|
477
|
-
printOutcomes([{ plugin: flags.positionals[0] ?? '*', target: '*', status: 'failed', action: 'update', dryRun: flags.dryRun, diagnostic: (error as Error).message }], json);
|
|
478
|
-
return 1;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
fail(`plugnz: unknown verb '${verb}'\n\n${USAGE}`, 2);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (process.argv[1] !== undefined && process.argv[1].endsWith('cli.ts')) {
|
|
486
|
-
void main(process.argv.slice(2)).then((code) => {
|
|
487
|
-
process.exitCode = code;
|
|
488
|
-
});
|
|
489
|
-
}
|
package/src/compatibility.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { InstallStatus } from './host';
|
|
2
|
-
import type { ConsumerProfile } from './consumer-profiles';
|
|
3
|
-
|
|
4
|
-
export type CompatibilityStatus = Extract<InstallStatus, 'unsupported' | 'unverified'> | 'supported';
|
|
5
|
-
export type ConsumerCapability = 'install' | 'update' | 'commandProjection' | 'userOnlySkills';
|
|
6
|
-
|
|
7
|
-
/** A target-specific refusal that callers can render as an InstallOutcome. */
|
|
8
|
-
export class CompatibilityError extends Error {
|
|
9
|
-
constructor(
|
|
10
|
-
readonly target: string,
|
|
11
|
-
readonly capability: ConsumerCapability,
|
|
12
|
-
readonly status: Extract<CompatibilityStatus, 'unsupported' | 'unverified'>,
|
|
13
|
-
readonly evidence: string,
|
|
14
|
-
) {
|
|
15
|
-
super(`target '${target}' is ${status} for ${capability}; evidence: ${evidence}`);
|
|
16
|
-
this.name = 'CompatibilityError';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function requireCompatible(profile: ConsumerProfile, capability: ConsumerCapability): void {
|
|
21
|
-
const status = profile.capabilities[capability];
|
|
22
|
-
if (status !== 'supported') throw new CompatibilityError(profile.id, capability, status, profile.evidence);
|
|
23
|
-
}
|
package/src/consumer-profiles.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Native plugin migration inventory plus retired standalone cleanup routes from
|
|
3
|
-
* SPEC.md § Compatibility and lifecycle requirements. Profiles describe
|
|
4
|
-
* evidence; they do not make an adapter active.
|
|
5
|
-
*/
|
|
6
|
-
import type { CompatibilityStatus, ConsumerCapability } from './compatibility';
|
|
7
|
-
|
|
8
|
-
export const targetIds = [
|
|
9
|
-
'claude-code', 'codex', 'omp', 'dcode', 'hermes', 'openclaw', 'grok', 'kimi',
|
|
10
|
-
'zcode-cli', 'zcode-desktop', 'cursor', 'opencode', 'pi', 'gemini-cli',
|
|
11
|
-
'factory', 'grokbot',
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
|
-
export type TargetId = typeof targetIds[number];
|
|
15
|
-
|
|
16
|
-
export interface ConsumerProfile {
|
|
17
|
-
id: TargetId;
|
|
18
|
-
scope: 'native-plugin' | 'excluded-standalone';
|
|
19
|
-
surface: string;
|
|
20
|
-
/** Observed version when known; otherwise evidence has not established one. */
|
|
21
|
-
version: string | 'unverified';
|
|
22
|
-
evidence: string;
|
|
23
|
-
capabilities: Readonly<Record<ConsumerCapability, CompatibilityStatus>>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const active: Record<ConsumerCapability, CompatibilityStatus> = {
|
|
27
|
-
install: 'supported',
|
|
28
|
-
update: 'supported',
|
|
29
|
-
commandProjection: 'unverified',
|
|
30
|
-
userOnlySkills: 'unverified',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const pending = (id: TargetId, surface: string, evidence: string, version: ConsumerProfile['version'] = 'unverified'): ConsumerProfile => ({
|
|
34
|
-
id,
|
|
35
|
-
scope: 'native-plugin',
|
|
36
|
-
surface,
|
|
37
|
-
version,
|
|
38
|
-
evidence,
|
|
39
|
-
capabilities: { install: 'unverified', update: 'unverified', commandProjection: 'unverified', userOnlySkills: 'unverified' },
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
function profile(id: TargetId, surface: string, evidence: string, version: ConsumerProfile['version'], capabilities: ConsumerProfile['capabilities']): ConsumerProfile {
|
|
43
|
-
return { id, scope: 'native-plugin', surface, version, evidence, capabilities };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function excludedStandalone(id: TargetId, surface: string, evidence: string, version: ConsumerProfile['version'] = 'unverified'): ConsumerProfile {
|
|
47
|
-
return {
|
|
48
|
-
id, scope: 'excluded-standalone', surface, version, evidence,
|
|
49
|
-
capabilities: { install: 'unsupported', update: 'unsupported', commandProjection: 'unsupported', userOnlySkills: 'unsupported' },
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Native plugin routes, with legacy standalone profiles retained only for safe read/remove cleanup. */
|
|
54
|
-
export const consumerProfiles: readonly ConsumerProfile[] = [
|
|
55
|
-
profile('claude-code', 'Claude Code plugin loader', 'docs/hosts/claude-code.md', '2.1.275', active),
|
|
56
|
-
profile('codex', 'Codex plugin loader', 'docs/evidence/codex-personal-20260922.json', '0.153.4', { ...active, commandProjection: 'supported', userOnlySkills: 'supported' }),
|
|
57
|
-
profile('omp', 'OMP native npm/link extension-package loader', 'docs/evidence/omp-native-extension-package-20260923.json', '18.1.4', { ...active, commandProjection: 'supported', userOnlySkills: 'supported' }),
|
|
58
|
-
profile('dcode', 'deepagents-code plugin loader', 'docs/evidence/dcode-native-batch2-20260923.md', '0.1.74', { ...active, commandProjection: 'unsupported', userOnlySkills: 'unsupported' }),
|
|
59
|
-
profile('hermes', 'Hermes portable + native directory plugin loaders', 'docs/hosts/hermes.md', 'c0d7294', { ...active, commandProjection: 'supported', userOnlySkills: 'supported' }),
|
|
60
|
-
pending('openclaw', 'OpenClaw plugin route', 'SPEC.md § Compatibility and lifecycle requirements'),
|
|
61
|
-
profile('grok', 'Grok Build native marketplace loader', 'docs/hosts/grok.md (isolated native lifecycle probe, 2026-09-23)', '1.0.41', { ...active, commandProjection: 'supported', userOnlySkills: 'supported' }),
|
|
62
|
-
profile('kimi', 'Kimi Code plugin loader', 'docs/hosts/kimi.md (isolated native probe, 2026-09-22)', '2.0.1', { ...active, commandProjection: 'supported', userOnlySkills: 'supported' }),
|
|
63
|
-
profile('zcode-cli', 'Official Z.ai ZCode CLI plugin loader', 'docs/evidence/zcode-official-cli-872ad960-20260923.json', '0.16.9', { ...active, commandProjection: 'supported', userOnlySkills: 'supported' }),
|
|
64
|
-
pending('zcode-desktop', 'Z.ai ZCode desktop plugin route', 'SPEC.md § Compatibility and lifecycle requirements'),
|
|
65
|
-
profile('cursor', 'Cursor local plugin loader', 'docs/hosts/cursor.md', '2026.09.18-9a7762b', active),
|
|
66
|
-
excludedStandalone('opencode', 'OpenCode standalone skill and command loaders', 'docs/evidence/opencode-native-loader-20260922.json', '1.15.13'),
|
|
67
|
-
excludedStandalone('pi', 'Pi standalone skill loader', 'docs/evidence/pi-native-loader-20260922.json', '0.80.10'),
|
|
68
|
-
pending('gemini-cli', 'Gemini CLI native plugin-extension route', 'SPEC.md § Compatibility and lifecycle requirements (native extension semantics unverified)'),
|
|
69
|
-
excludedStandalone('factory', 'Factory standalone skill route', 'docs/hosts/factory.md'),
|
|
70
|
-
excludedStandalone('grokbot', 'Grok Bot standalone skill route', 'docs/hosts/grokbot.md'),
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
const byId = new Map(consumerProfiles.map((entry) => [entry.id, entry]));
|
|
74
|
-
|
|
75
|
-
export function findConsumerProfile(id: string): ConsumerProfile | undefined {
|
|
76
|
-
return byId.get(id as TargetId);
|
|
77
|
-
}
|