@aviaratech/ai-delivery 0.3.22 → 0.3.23
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/README.md +32 -6
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -1
- package/dist/delivery/common.d.ts +4 -1
- package/dist/delivery/common.js +49 -6
- package/dist/delivery/common.js.map +1 -1
- package/dist/delivery/stage.d.ts +6 -0
- package/dist/delivery/stage.js +11 -2
- package/dist/delivery/stage.js.map +1 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pluginInstaller.d.ts +51 -0
- package/dist/pluginInstaller.js +774 -0
- package/dist/pluginInstaller.js.map +1 -0
- package/dist/pluginInstaller.test.d.ts +1 -0
- package/dist/pluginInstaller.test.js +543 -0
- package/dist/pluginInstaller.test.js.map +1 -0
- package/dist/pluginPackaging.test.d.ts +1 -0
- package/dist/pluginPackaging.test.js +116 -0
- package/dist/pluginPackaging.test.js.map +1 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +71 -10
- package/dist/setup.js.map +1 -1
- package/dist/setup.test.js +584 -199
- package/dist/setup.test.js.map +1 -1
- package/dist/verification.d.ts +61 -0
- package/dist/verification.js +168 -6
- package/dist/verification.js.map +1 -1
- package/package.json +4 -4
- package/plugins/ai-delivery/.claude-plugin/plugin.json +6 -1
- package/plugins/ai-delivery/LICENSE +21 -0
- package/plugins/ai-delivery/README.md +4 -2
- package/plugins/ai-delivery/dist/mcp-launcher.js +19 -8
- package/plugins/ai-delivery/mcp.json +10 -0
- package/plugins/ai-delivery/plugin.json +10 -0
- package/plugins/ai-delivery/runtime/dist/THIRD-PARTY-NOTICES.md +1010 -0
- package/plugins/ai-delivery/runtime/dist/cli.js +285 -0
- package/plugins/ai-delivery/runtime/package.json +13 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, closeSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { gunzipSync } from 'node:zlib';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ensurePrivateDirectoryDurably, writePrivateJsonFileAtomically } from './utils/atomicJson.js';
|
|
11
|
+
import { withLock } from './utils/lockfile.js';
|
|
12
|
+
import { confirmOwnedCleanup, processSnapshot, retainedWorktreeTransitionProducerDigest, runStageCommand, terminateOwnedProcesses, } from './verification.js';
|
|
13
|
+
const Version = z
|
|
14
|
+
.string()
|
|
15
|
+
.max(128)
|
|
16
|
+
.regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u);
|
|
17
|
+
const InputSchema = z.strictObject({
|
|
18
|
+
action: z.enum(['install', 'doctor', 'update', 'rollback', 'remove']),
|
|
19
|
+
host: z.enum(['codex', 'claude-code']),
|
|
20
|
+
scope: z.enum(['user', 'project', 'local']),
|
|
21
|
+
version: Version.optional(),
|
|
22
|
+
dryRun: z.boolean().default(false),
|
|
23
|
+
repoRoot: z.string().min(1),
|
|
24
|
+
});
|
|
25
|
+
const ProcessIdentity = z.strictObject({ pid: z.number().int().positive(), birth: z.string().min(1) });
|
|
26
|
+
const Pending = z.strictObject({
|
|
27
|
+
action: InputSchema.shape.action,
|
|
28
|
+
targetVersion: Version.nullable(),
|
|
29
|
+
owner: ProcessIdentity,
|
|
30
|
+
producerDigest: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
|
|
31
|
+
staging: z
|
|
32
|
+
.strictObject({
|
|
33
|
+
path: z.string(),
|
|
34
|
+
device: z.number().int().nonnegative().optional(),
|
|
35
|
+
inode: z.number().int().nonnegative().optional(),
|
|
36
|
+
})
|
|
37
|
+
.optional(),
|
|
38
|
+
command: z.discriminatedUnion('phase', [
|
|
39
|
+
z.strictObject({ phase: z.literal('idle') }),
|
|
40
|
+
z.strictObject({ phase: z.literal('starting') }),
|
|
41
|
+
z.strictObject({
|
|
42
|
+
phase: z.literal('running'),
|
|
43
|
+
process: ProcessIdentity,
|
|
44
|
+
tracked: z.array(ProcessIdentity.extend({ pgid: z.number().int().positive() })).max(256),
|
|
45
|
+
}),
|
|
46
|
+
]),
|
|
47
|
+
});
|
|
48
|
+
const StateSchema = z.strictObject({
|
|
49
|
+
owner: z.literal('@aviaratech/ai-delivery.plugin@1'),
|
|
50
|
+
host: InputSchema.shape.host,
|
|
51
|
+
scope: InputSchema.shape.scope,
|
|
52
|
+
projectRoot: z.string().nullable(),
|
|
53
|
+
nativeHome: z.string().min(1),
|
|
54
|
+
marketplace: z.string(),
|
|
55
|
+
currentVersion: Version.nullable(),
|
|
56
|
+
previousVersion: Version.nullable(),
|
|
57
|
+
installedPath: z.string().nullable(),
|
|
58
|
+
nativeVersion: z.string().optional(),
|
|
59
|
+
pending: Pending.optional(),
|
|
60
|
+
});
|
|
61
|
+
const Unit = z.strictObject({
|
|
62
|
+
owner: z.literal('@aviaratech/ai-delivery.plugin-version@1'),
|
|
63
|
+
version: Version,
|
|
64
|
+
archiveSha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
65
|
+
files: z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/u)),
|
|
66
|
+
});
|
|
67
|
+
const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
68
|
+
function assertCanonical(path) {
|
|
69
|
+
let cursor = resolve(path);
|
|
70
|
+
while (true) {
|
|
71
|
+
if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink())
|
|
72
|
+
throw new Error('Managed plugin paths must not use symlinks.');
|
|
73
|
+
const parent = dirname(cursor);
|
|
74
|
+
if (parent === cursor)
|
|
75
|
+
break;
|
|
76
|
+
cursor = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function targetFor(input) {
|
|
80
|
+
const projectRoot = realpathSync(resolve(input.repoRoot));
|
|
81
|
+
const nativeProfile = resolve(projectRoot, (input.host === 'codex' ? process.env.CODEX_HOME : process.env.CLAUDE_CONFIG_DIR) ??
|
|
82
|
+
join(homedir(), input.host === 'codex' ? '.codex' : '.claude'));
|
|
83
|
+
let ancestor = nativeProfile;
|
|
84
|
+
const missing = [];
|
|
85
|
+
while (!existsSync(ancestor)) {
|
|
86
|
+
missing.unshift(basename(ancestor));
|
|
87
|
+
ancestor = dirname(ancestor);
|
|
88
|
+
}
|
|
89
|
+
const nativeHome = join(realpathSync(ancestor), ...missing);
|
|
90
|
+
const scopeKey = input.scope === 'user'
|
|
91
|
+
? 'user'
|
|
92
|
+
: `${input.scope}-${createHash('sha256').update(projectRoot).digest('hex').slice(0, 16)}`;
|
|
93
|
+
const directory = join(homedir(), '.cache', 'ai-delivery', 'plugins', input.host, scopeKey);
|
|
94
|
+
assertCanonical(directory);
|
|
95
|
+
if (existsSync(directory) && (!lstatSync(directory).isDirectory() || (lstatSync(directory).mode & 0o077) !== 0))
|
|
96
|
+
throw new Error('Managed plugin directory is not private.');
|
|
97
|
+
return {
|
|
98
|
+
directory,
|
|
99
|
+
projectRoot,
|
|
100
|
+
nativeHome,
|
|
101
|
+
marketplace: `ai-delivery-${scopeKey}`,
|
|
102
|
+
catalog: join(directory, 'marketplace'),
|
|
103
|
+
statePath: join(directory, 'state.json'),
|
|
104
|
+
tool: input.host === 'codex' ? 'codex' : 'claude',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function readState(input, target) {
|
|
108
|
+
if (!existsSync(target.statePath))
|
|
109
|
+
return undefined;
|
|
110
|
+
const metadata = lstatSync(target.statePath);
|
|
111
|
+
if (!metadata.isFile() || metadata.nlink !== 1 || (metadata.mode & 0o077) !== 0 || metadata.size > 65536)
|
|
112
|
+
throw new Error('Managed plugin state is unsafe.');
|
|
113
|
+
const state = StateSchema.parse(JSON.parse(readFileSync(target.statePath, 'utf8')));
|
|
114
|
+
if (state.nativeHome !== target.nativeHome)
|
|
115
|
+
throw new Error('Managed plugin state belongs to a different native profile.');
|
|
116
|
+
if (state.host !== input.host ||
|
|
117
|
+
state.scope !== input.scope ||
|
|
118
|
+
state.marketplace !== target.marketplace ||
|
|
119
|
+
state.projectRoot !== (input.scope === 'user' ? null : target.projectRoot))
|
|
120
|
+
throw new Error('Managed plugin state belongs to a different target.');
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
function identity(pid) {
|
|
124
|
+
const member = processSnapshot().get(pid);
|
|
125
|
+
return member && !member.status.startsWith('Z') ? member.identity : undefined;
|
|
126
|
+
}
|
|
127
|
+
async function recover(pending) {
|
|
128
|
+
if (identity(pending.owner.pid) === pending.owner.birth)
|
|
129
|
+
throw new Error('A live plugin installer still owns this target.');
|
|
130
|
+
if (pending.command.phase === 'starting')
|
|
131
|
+
throw new Error('Interrupted installer command ownership is unknown; preserve its state before recovery.');
|
|
132
|
+
if (pending.command.phase !== 'running')
|
|
133
|
+
return;
|
|
134
|
+
const owned = {
|
|
135
|
+
rootPid: pending.command.process.pid,
|
|
136
|
+
rootIdentity: pending.command.process.birth,
|
|
137
|
+
sampled: true,
|
|
138
|
+
tracked: new Map(pending.command.tracked.map(({ pid, birth, pgid }) => [
|
|
139
|
+
pid,
|
|
140
|
+
{ pid, identity: birth, pgid, ppid: 0, rssBytes: 0, status: '' },
|
|
141
|
+
])),
|
|
142
|
+
};
|
|
143
|
+
terminateOwnedProcesses(owned);
|
|
144
|
+
await confirmOwnedCleanup(owned);
|
|
145
|
+
}
|
|
146
|
+
async function command(binary, args, cwd, signal, publish) {
|
|
147
|
+
const output = await runStageCommand(cwd, [binary, ...args], signal, undefined, { maxAggregateRssBytes: 1024 ** 3, minFreeDiskBytes: 256 * 1024 ** 2, maxNewOutputBytes: 512 * 1024 ** 2 }, undefined, undefined, {
|
|
148
|
+
starting: () => publish?.({ phase: 'starting' }),
|
|
149
|
+
observed: (owned) => publish?.({
|
|
150
|
+
phase: 'running',
|
|
151
|
+
process: { pid: owned.rootPid, birth: owned.rootIdentity },
|
|
152
|
+
tracked: [...owned.tracked.values()].map(({ pid, identity: birth, pgid }) => ({ pid, birth, pgid })),
|
|
153
|
+
}),
|
|
154
|
+
idle: () => publish?.({ phase: 'idle' }),
|
|
155
|
+
assertQuiescent: () => { },
|
|
156
|
+
}, {
|
|
157
|
+
environment: { ...process.env, DISABLE_AUTOUPDATER: '1' },
|
|
158
|
+
cwd,
|
|
159
|
+
maxCapturedOutputBytes: 1024 * 1024,
|
|
160
|
+
stdoutOnly: true,
|
|
161
|
+
failedOutput: (bytes) => ` Native command output: ${bytes.toString('utf8').slice(-2000)}`,
|
|
162
|
+
});
|
|
163
|
+
return output.toString('utf8');
|
|
164
|
+
}
|
|
165
|
+
function sync(path) {
|
|
166
|
+
const fd = openSync(path, 'r');
|
|
167
|
+
try {
|
|
168
|
+
fsyncSync(fd);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
closeSync(fd);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function pluginRoot(target, version) {
|
|
175
|
+
return join(target.catalog, 'versions', version, 'plugins', 'ai-delivery');
|
|
176
|
+
}
|
|
177
|
+
function unitDirectory(target, version) {
|
|
178
|
+
return join(target.catalog, 'versions', version);
|
|
179
|
+
}
|
|
180
|
+
function tree(root) {
|
|
181
|
+
const files = {};
|
|
182
|
+
let entries = 0;
|
|
183
|
+
const visit = (directory, prefix, depth) => {
|
|
184
|
+
if (depth > 16)
|
|
185
|
+
throw new Error('Plugin tree exceeds the depth bound.');
|
|
186
|
+
for (const name of readdirSync(directory).sort()) {
|
|
187
|
+
if (++entries > 4096)
|
|
188
|
+
throw new Error('Plugin tree exceeds the entry bound.');
|
|
189
|
+
const path = join(directory, name), relative = `${prefix}${name}`, metadata = lstatSync(path);
|
|
190
|
+
if (metadata.isSymbolicLink() || (!metadata.isDirectory() && (!metadata.isFile() || metadata.nlink !== 1)))
|
|
191
|
+
throw new Error('Plugin tree contains links or special files.');
|
|
192
|
+
if (metadata.isDirectory())
|
|
193
|
+
visit(path, `${relative}/`, depth + 1);
|
|
194
|
+
else {
|
|
195
|
+
if (metadata.size > 32 * 1024 ** 2)
|
|
196
|
+
throw new Error('Plugin file exceeds the size bound.');
|
|
197
|
+
files[relative] = sha(readFileSync(path));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
visit(root, '', 0);
|
|
202
|
+
return files;
|
|
203
|
+
}
|
|
204
|
+
function verifiedUnit(target, version) {
|
|
205
|
+
const directory = unitDirectory(target, version);
|
|
206
|
+
assertCanonical(directory);
|
|
207
|
+
const path = join(directory, 'version.json');
|
|
208
|
+
if (!existsSync(path))
|
|
209
|
+
throw new Error(`No completed immutable plugin unit for ${version}.`);
|
|
210
|
+
const metadata = lstatSync(path), archive = join(directory, 'archive.tgz'), archiveMetadata = lstatSync(archive);
|
|
211
|
+
if (!metadata.isFile() ||
|
|
212
|
+
metadata.nlink !== 1 ||
|
|
213
|
+
metadata.size > 1024 * 1024 ||
|
|
214
|
+
!archiveMetadata.isFile() ||
|
|
215
|
+
archiveMetadata.nlink !== 1 ||
|
|
216
|
+
archiveMetadata.size > 32 * 1024 ** 2)
|
|
217
|
+
throw new Error('Immutable plugin unit metadata is unsafe.');
|
|
218
|
+
const unit = Unit.parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
219
|
+
if (unit.version !== version ||
|
|
220
|
+
JSON.stringify(tree(pluginRoot(target, version))) !== JSON.stringify(unit.files) ||
|
|
221
|
+
sha(readFileSync(archive)) !== unit.archiveSha256)
|
|
222
|
+
throw new Error('Immutable plugin unit integrity changed.');
|
|
223
|
+
return unit;
|
|
224
|
+
}
|
|
225
|
+
function unpack(bytes) {
|
|
226
|
+
if (bytes.length > 32 * 1024 ** 2)
|
|
227
|
+
throw new Error('Plugin archive exceeds 32 MiB.');
|
|
228
|
+
const tar = gunzipSync(bytes, { maxOutputLength: 128 * 1024 ** 2 });
|
|
229
|
+
const files = new Map();
|
|
230
|
+
for (let offset = 0; offset + 512 <= tar.length;) {
|
|
231
|
+
const header = tar.subarray(offset, offset + 512);
|
|
232
|
+
if (header.every((byte) => byte === 0))
|
|
233
|
+
break;
|
|
234
|
+
const text = (start, length) => header
|
|
235
|
+
.subarray(start, start + length)
|
|
236
|
+
.toString()
|
|
237
|
+
.replace(/\0.*$/su, '')
|
|
238
|
+
.trim();
|
|
239
|
+
const octal = (start, length) => {
|
|
240
|
+
const value = text(start, length);
|
|
241
|
+
if (!/^[0-7]+$/u.test(value))
|
|
242
|
+
throw new Error('Unsupported tar numeric header.');
|
|
243
|
+
return Number.parseInt(value, 8);
|
|
244
|
+
};
|
|
245
|
+
const checksum = [...header].reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
|
|
246
|
+
if (checksum !== octal(148, 8))
|
|
247
|
+
throw new Error('Plugin archive header checksum mismatch.');
|
|
248
|
+
const name = `${text(345, 155) ? `${text(345, 155)}/` : ''}${text(0, 100)}`, size = octal(124, 12), kind = text(156, 1);
|
|
249
|
+
if (!/^package\/[A-Za-z0-9_./-]+$/u.test(name) ||
|
|
250
|
+
name.split('/').some((part) => part === '..' || part === '.') ||
|
|
251
|
+
name.split('/').length > 16 ||
|
|
252
|
+
!['', '0', '5'].includes(kind) ||
|
|
253
|
+
offset + 512 + size > tar.length ||
|
|
254
|
+
files.has(name) ||
|
|
255
|
+
files.size >= 4096)
|
|
256
|
+
throw new Error('Unsafe or unsupported plugin archive member.');
|
|
257
|
+
if (kind !== '5')
|
|
258
|
+
files.set(name, tar.subarray(offset + 512, offset + 512 + size));
|
|
259
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
260
|
+
}
|
|
261
|
+
return files;
|
|
262
|
+
}
|
|
263
|
+
async function stageVersion(target, version, signal, publish, staging) {
|
|
264
|
+
const unit = unitDirectory(target, version);
|
|
265
|
+
if (existsSync(unit)) {
|
|
266
|
+
verifiedUnit(target, version);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const parent = dirname(unit);
|
|
270
|
+
ensurePrivateDirectoryDurably(parent);
|
|
271
|
+
const temporary = join(parent, `.stage-${randomUUID()}`);
|
|
272
|
+
staging({ path: temporary });
|
|
273
|
+
mkdirSync(temporary, { mode: 0o700 });
|
|
274
|
+
const allocation = lstatSync(temporary);
|
|
275
|
+
sync(parent);
|
|
276
|
+
staging({ path: temporary, device: allocation.dev, inode: allocation.ino });
|
|
277
|
+
let quiescent = true;
|
|
278
|
+
let failed = false;
|
|
279
|
+
let failure;
|
|
280
|
+
const unitPublish = (value) => {
|
|
281
|
+
quiescent = value.phase === 'idle';
|
|
282
|
+
publish(value);
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
const packed = JSON.parse(await command('npm', [
|
|
286
|
+
'pack',
|
|
287
|
+
`@aviaratech/ai-delivery@${version}`,
|
|
288
|
+
'--ignore-scripts',
|
|
289
|
+
'--json',
|
|
290
|
+
'--logs-max',
|
|
291
|
+
'0',
|
|
292
|
+
'--cache',
|
|
293
|
+
join(target.directory, 'npm-cache'),
|
|
294
|
+
'--pack-destination',
|
|
295
|
+
temporary,
|
|
296
|
+
], temporary, signal, unitPublish));
|
|
297
|
+
const filename = packed[0]?.filename;
|
|
298
|
+
if (typeof filename !== 'string' || !/^aviaratech-ai-delivery-[A-Za-z0-9.+_-]+\.tgz$/u.test(filename))
|
|
299
|
+
throw new Error('npm returned an unsafe archive path.');
|
|
300
|
+
const bytes = readFileSync(join(temporary, filename)), files = unpack(bytes);
|
|
301
|
+
const packageInfo = JSON.parse(files.get('package/package.json')?.toString() ?? 'null');
|
|
302
|
+
if (packageInfo?.name !== '@aviaratech/ai-delivery' || packageInfo.version !== version)
|
|
303
|
+
throw new Error('Archive package identity does not match the explicit version.');
|
|
304
|
+
const prefix = 'package/plugins/ai-delivery/';
|
|
305
|
+
const destination = join(temporary, 'plugins', 'ai-delivery');
|
|
306
|
+
for (const [name, content] of files)
|
|
307
|
+
if (name.startsWith(prefix)) {
|
|
308
|
+
const path = join(destination, name.slice(prefix.length));
|
|
309
|
+
ensurePrivateDirectoryDurably(dirname(path));
|
|
310
|
+
writeFileSync(path, content, { mode: 0o600, flag: 'wx' });
|
|
311
|
+
sync(path);
|
|
312
|
+
}
|
|
313
|
+
const portable = JSON.parse(readFileSync(join(destination, 'plugin.json'), 'utf8'));
|
|
314
|
+
const native = JSON.parse(readFileSync(join(destination, '.claude-plugin/plugin.json'), 'utf8'));
|
|
315
|
+
const runtime = JSON.parse(readFileSync(join(destination, 'runtime/package.json'), 'utf8'));
|
|
316
|
+
if (portable.name !== 'ai-delivery' ||
|
|
317
|
+
portable.version !== version ||
|
|
318
|
+
native.name !== portable.name ||
|
|
319
|
+
native.version !== version ||
|
|
320
|
+
native.packageVersion !== version ||
|
|
321
|
+
native.deliveryCapabilityVersion !== 2 ||
|
|
322
|
+
runtime.name !== packageInfo.name ||
|
|
323
|
+
runtime.version !== version)
|
|
324
|
+
throw new Error('Archive does not contain a coherent self-contained native plugin.');
|
|
325
|
+
if ((await command(process.execPath, [join(destination, 'runtime/dist/cli.js'), '--version'], temporary, signal, unitPublish)).trim() !== version)
|
|
326
|
+
throw new Error('Built native CLI version does not match its archive.');
|
|
327
|
+
renameSync(join(temporary, filename), join(temporary, 'archive.tgz'));
|
|
328
|
+
sync(join(temporary, 'archive.tgz'));
|
|
329
|
+
writePrivateJsonFileAtomically(join(temporary, 'version.json'), {
|
|
330
|
+
owner: '@aviaratech/ai-delivery.plugin-version@1',
|
|
331
|
+
version,
|
|
332
|
+
archiveSha256: sha(bytes),
|
|
333
|
+
files: tree(destination),
|
|
334
|
+
});
|
|
335
|
+
renameSync(temporary, unit);
|
|
336
|
+
sync(parent);
|
|
337
|
+
staging(undefined);
|
|
338
|
+
verifiedUnit(target, version);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
failed = true;
|
|
342
|
+
failure = error;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
if (quiescent && existsSync(temporary)) {
|
|
346
|
+
const current = lstatSync(temporary);
|
|
347
|
+
if (current.dev !== allocation.dev ||
|
|
348
|
+
current.ino !== allocation.ino ||
|
|
349
|
+
!current.isDirectory() ||
|
|
350
|
+
current.isSymbolicLink())
|
|
351
|
+
throw new Error('Owned staging directory identity changed.');
|
|
352
|
+
rmSync(temporary, { recursive: true });
|
|
353
|
+
sync(parent);
|
|
354
|
+
}
|
|
355
|
+
if (quiescent)
|
|
356
|
+
staging(undefined);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
failure = failed
|
|
360
|
+
? new AggregateError([failure, error], 'Plugin preparation failed and staging cleanup also failed.')
|
|
361
|
+
: error;
|
|
362
|
+
failed = true;
|
|
363
|
+
}
|
|
364
|
+
if (failed)
|
|
365
|
+
throw failure;
|
|
366
|
+
}
|
|
367
|
+
function catalog(target, version) {
|
|
368
|
+
verifiedUnit(target, version);
|
|
369
|
+
const source = `./versions/${version}/plugins/ai-delivery`;
|
|
370
|
+
writePrivateJsonFileAtomically(join(target.catalog, '.agents/plugins/marketplace.json'), {
|
|
371
|
+
name: target.marketplace,
|
|
372
|
+
interface: { displayName: 'ai-delivery' },
|
|
373
|
+
plugins: [
|
|
374
|
+
{
|
|
375
|
+
name: 'ai-delivery',
|
|
376
|
+
source: { source: 'local', path: source },
|
|
377
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
378
|
+
category: 'Productivity',
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
});
|
|
382
|
+
writePrivateJsonFileAtomically(join(target.catalog, '.claude-plugin/marketplace.json'), {
|
|
383
|
+
name: target.marketplace,
|
|
384
|
+
owner: { name: 'AviaraTech' },
|
|
385
|
+
plugins: [{ name: 'ai-delivery', source, version }],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async function inventory(input, target, signal, publish) {
|
|
389
|
+
const value = JSON.parse(await command(target.tool, ['plugin', 'list', ...(input.host === 'codex' ? ['--available'] : []), '--json'], target.projectRoot, signal, publish));
|
|
390
|
+
if (input.host === 'claude-code')
|
|
391
|
+
return z.array(z.record(z.string(), z.unknown())).parse(value);
|
|
392
|
+
const all = z
|
|
393
|
+
.object({
|
|
394
|
+
installed: z.array(z.record(z.string(), z.unknown())),
|
|
395
|
+
available: z.array(z.record(z.string(), z.unknown())),
|
|
396
|
+
})
|
|
397
|
+
.parse(value);
|
|
398
|
+
return [
|
|
399
|
+
...all.installed.map((entry) => ({ ...entry, installed: true })),
|
|
400
|
+
...all.available.map((entry) => ({ ...entry, installed: false })),
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
function registration(input, target, entries) {
|
|
404
|
+
if (input.host === 'claude-code') {
|
|
405
|
+
const path = join(target.nativeHome, 'plugins/known_marketplaces.json');
|
|
406
|
+
if (!existsSync(path))
|
|
407
|
+
return false;
|
|
408
|
+
if (!lstatSync(path).isFile() || lstatSync(path).size > 1024 * 1024)
|
|
409
|
+
throw new Error('Native marketplace registry is unsafe.');
|
|
410
|
+
const markets = z.record(z.string(), z.unknown()).parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
411
|
+
if (markets[target.marketplace] === undefined)
|
|
412
|
+
return false;
|
|
413
|
+
const selected = z
|
|
414
|
+
.object({ source: z.object({ source: z.literal('directory'), path: z.string() }), installLocation: z.string() })
|
|
415
|
+
.parse(markets[target.marketplace]);
|
|
416
|
+
if (resolve(selected.source.path) !== target.catalog || resolve(selected.installLocation) !== target.catalog)
|
|
417
|
+
throw new Error('Native marketplace name belongs to an unmanaged source.');
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
const selected = entries.filter((entry) => entry.marketplaceName === target.marketplace);
|
|
421
|
+
for (const entry of selected) {
|
|
422
|
+
const source = z.object({ sourceType: z.literal('local'), source: z.string() }).parse(entry.marketplaceSource);
|
|
423
|
+
if (resolve(source.source) !== target.catalog)
|
|
424
|
+
throw new Error('Native marketplace name belongs to an unmanaged source.');
|
|
425
|
+
}
|
|
426
|
+
return selected.length > 0;
|
|
427
|
+
}
|
|
428
|
+
function nativeEntry(input, target, entries) {
|
|
429
|
+
const matches = entries.filter((entry) => (entry.pluginId ?? entry.id) === `ai-delivery@${target.marketplace}` &&
|
|
430
|
+
(input.host === 'codex' ? entry.installed === true : entry.scope === input.scope) &&
|
|
431
|
+
(input.scope === 'user' || entry.projectPath === target.projectRoot));
|
|
432
|
+
if (matches.length > 1)
|
|
433
|
+
throw new Error('Native plugin readback is ambiguous.');
|
|
434
|
+
const entry = matches[0];
|
|
435
|
+
if (!entry)
|
|
436
|
+
return undefined;
|
|
437
|
+
const sourceRoot = input.host === 'codex'
|
|
438
|
+
? z.object({ source: z.literal('local'), path: z.string() }).parse(entry.source).path
|
|
439
|
+
: undefined;
|
|
440
|
+
return {
|
|
441
|
+
version: Version.parse(entry.version),
|
|
442
|
+
enabled: z.boolean().parse(entry.enabled),
|
|
443
|
+
...(typeof entry.installPath === 'string' ? { installedPath: entry.installPath } : {}),
|
|
444
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
445
|
+
...(entry.mcpServers ? { mcpServers: z.record(z.string(), z.unknown()).parse(entry.mcpServers) } : {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function claudeCatalogSelection(target) {
|
|
449
|
+
const path = join(target.catalog, '.claude-plugin/marketplace.json');
|
|
450
|
+
assertCanonical(path);
|
|
451
|
+
if (!lstatSync(path).isFile() || lstatSync(path).size > 1024 * 1024)
|
|
452
|
+
throw new Error('Managed Claude catalog is unsafe.');
|
|
453
|
+
const catalog = z
|
|
454
|
+
.object({
|
|
455
|
+
name: z.literal(target.marketplace),
|
|
456
|
+
plugins: z.array(z.object({ name: z.string(), version: Version, source: z.string() })),
|
|
457
|
+
})
|
|
458
|
+
.parse(JSON.parse(readFileSync(path, 'utf8')));
|
|
459
|
+
const entries = catalog.plugins.filter((entry) => entry.name === 'ai-delivery');
|
|
460
|
+
if (entries.length !== 1)
|
|
461
|
+
throw new Error('Managed Claude catalog selection is ambiguous.');
|
|
462
|
+
const entry = entries[0];
|
|
463
|
+
if (!entry.source.startsWith('./') || resolve(target.catalog, entry.source) !== pluginRoot(target, entry.version))
|
|
464
|
+
throw new Error('Claude catalog source differs from the immutable managed unit.');
|
|
465
|
+
return entry;
|
|
466
|
+
}
|
|
467
|
+
function selectedRoot(input, target, entry, cachePath, sourceVersion = entry.version) {
|
|
468
|
+
const unit = verifiedUnit(target, input.host === 'claude-code' ? sourceVersion : entry.version), source = pluginRoot(target, input.host === 'claude-code' ? sourceVersion : entry.version);
|
|
469
|
+
if (input.host === 'claude-code' && claudeCatalogSelection(target).version !== sourceVersion)
|
|
470
|
+
throw new Error('Claude catalog version differs from its native selection.');
|
|
471
|
+
if (input.host === 'codex' && resolve(entry.sourceRoot) !== source)
|
|
472
|
+
throw new Error('Native plugin source differs from the managed version.');
|
|
473
|
+
const root = input.host === 'codex' ? cachePath : source;
|
|
474
|
+
if (!root)
|
|
475
|
+
throw new Error('Native cache path is missing.');
|
|
476
|
+
if (input.host === 'codex' &&
|
|
477
|
+
resolve(root) !== join(target.nativeHome, 'plugins/cache', target.marketplace, 'ai-delivery', entry.version))
|
|
478
|
+
throw new Error('Native cache path is outside the managed native selection.');
|
|
479
|
+
assertCanonical(root);
|
|
480
|
+
if (JSON.stringify(tree(root)) !== JSON.stringify(unit.files))
|
|
481
|
+
throw new Error('Selected native plugin bytes differ from the verified archive.');
|
|
482
|
+
if (input.host === 'claude-code') {
|
|
483
|
+
const server = z
|
|
484
|
+
.object({ command: z.literal('node'), args: z.tuple([z.literal('${CLAUDE_PLUGIN_ROOT}/dist/mcp-launcher.js')]) })
|
|
485
|
+
.parse(entry.mcpServers?.['ai-delivery']);
|
|
486
|
+
if (!server)
|
|
487
|
+
throw new Error('Native MCP binding is missing.');
|
|
488
|
+
}
|
|
489
|
+
return root;
|
|
490
|
+
}
|
|
491
|
+
async function startup(root, version, cwd) {
|
|
492
|
+
const transport = new StdioClientTransport({
|
|
493
|
+
command: process.execPath,
|
|
494
|
+
args: [join(root, 'dist/mcp-launcher.js')],
|
|
495
|
+
cwd,
|
|
496
|
+
env: { PATH: process.env.PATH ?? '', NODE_OPTIONS: '--max-old-space-size=1024' },
|
|
497
|
+
stderr: 'pipe',
|
|
498
|
+
maxBufferSize: 1024 * 1024,
|
|
499
|
+
});
|
|
500
|
+
const client = new Client({ name: 'ai-delivery-plugin-doctor', version: '1.0.0' });
|
|
501
|
+
try {
|
|
502
|
+
await client.connect(transport, { timeout: 30000 });
|
|
503
|
+
if (client.getServerVersion()?.version !== version)
|
|
504
|
+
throw new Error('MCP server version differs from the selected archive.');
|
|
505
|
+
await client.listTools({}, { timeout: 30000 });
|
|
506
|
+
return { configuration: 'native_manifest_present', startup: 'ready', version };
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
return {
|
|
510
|
+
configuration: 'native_manifest_present',
|
|
511
|
+
startup: 'failed',
|
|
512
|
+
error: (error instanceof Error ? error.message : String(error)).slice(0, 2000),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
await client.close();
|
|
517
|
+
await transport.close();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
export async function managePlugin(value) {
|
|
521
|
+
const input = InputSchema.parse(value);
|
|
522
|
+
if (input.host === 'codex' && input.scope !== 'user')
|
|
523
|
+
throw new Error('Codex currently supports user plugin installation; select --scope user.');
|
|
524
|
+
if ((input.action === 'install' || input.action === 'update') && input.version === undefined)
|
|
525
|
+
throw new Error('An explicit --version is required for install and update.');
|
|
526
|
+
if (input.version !== undefined && input.action !== 'install' && input.action !== 'update')
|
|
527
|
+
throw new Error('--version is supported only for install and update; rollback selects the recorded previous version.');
|
|
528
|
+
if (!['darwin', 'linux'].includes(process.platform))
|
|
529
|
+
throw new Error('Plugin management requires the supported POSIX runtime environment.');
|
|
530
|
+
const target = targetFor(input);
|
|
531
|
+
let state = readState(input, target);
|
|
532
|
+
const result = {
|
|
533
|
+
action: input.action,
|
|
534
|
+
host: input.host,
|
|
535
|
+
scope: input.scope,
|
|
536
|
+
version: input.action === 'remove'
|
|
537
|
+
? null
|
|
538
|
+
: (input.version ?? (input.action === 'rollback' ? state?.previousVersion : state?.currentVersion) ?? null),
|
|
539
|
+
changed: false,
|
|
540
|
+
dryRun: input.dryRun,
|
|
541
|
+
installed: false,
|
|
542
|
+
marketplace: target.marketplace,
|
|
543
|
+
managedDirectory: target.directory,
|
|
544
|
+
repositoryAdmission: 'explicit_stage_and_admit_required',
|
|
545
|
+
};
|
|
546
|
+
if (input.dryRun)
|
|
547
|
+
return result;
|
|
548
|
+
if (input.action === 'doctor') {
|
|
549
|
+
if (!state)
|
|
550
|
+
return result;
|
|
551
|
+
result.recoveryRequired = state.pending !== undefined;
|
|
552
|
+
if (state.pending || !state.currentVersion)
|
|
553
|
+
return result;
|
|
554
|
+
verifiedUnit(target, state.currentVersion);
|
|
555
|
+
const entries = await inventory(input, target, new AbortController().signal);
|
|
556
|
+
const registered = registration(input, target, entries), entry = nativeEntry(input, target, entries);
|
|
557
|
+
result.installed = entry?.version === state.currentVersion;
|
|
558
|
+
result.enabled = entry?.enabled ?? false;
|
|
559
|
+
if (state.installedPath)
|
|
560
|
+
result.nativeCachePath = state.installedPath;
|
|
561
|
+
if (state.nativeVersion)
|
|
562
|
+
result.nativeVersion = state.nativeVersion;
|
|
563
|
+
if (!registered || !result.installed) {
|
|
564
|
+
result.mcp = { configuration: 'missing', startup: 'not_checked' };
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
result.nativePluginRoot = selectedRoot(input, target, entry, state.installedPath);
|
|
569
|
+
result.sourceIntegrity = true;
|
|
570
|
+
result.skills = readdirSync(join(result.nativePluginRoot, 'skills')).sort();
|
|
571
|
+
result.mcp = entry.enabled
|
|
572
|
+
? await startup(result.nativePluginRoot, state.currentVersion, target.projectRoot)
|
|
573
|
+
: { configuration: 'disabled', startup: 'not_checked' };
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
result.sourceIntegrity = false;
|
|
577
|
+
result.mcp = {
|
|
578
|
+
configuration: 'unverified',
|
|
579
|
+
startup: 'not_checked',
|
|
580
|
+
error: (error instanceof Error ? error.message : String(error)).slice(0, 2000),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
if (!state && input.action === 'remove')
|
|
586
|
+
return result;
|
|
587
|
+
if (!state && existsSync(target.directory) && readdirSync(target.directory).length > 0)
|
|
588
|
+
throw new Error('Managed plugin directory contains unrecognized content.');
|
|
589
|
+
ensurePrivateDirectoryDurably(target.directory);
|
|
590
|
+
return withLock(target.statePath, {
|
|
591
|
+
projectRoot: target.directory,
|
|
592
|
+
timeout: 200,
|
|
593
|
+
onReleaseError: (error) => {
|
|
594
|
+
throw error;
|
|
595
|
+
},
|
|
596
|
+
operation: async () => {
|
|
597
|
+
state = readState(input, target);
|
|
598
|
+
const resuming = state?.pending !== undefined;
|
|
599
|
+
if (state?.pending) {
|
|
600
|
+
await recover(state.pending);
|
|
601
|
+
if (state.pending.producerDigest !==
|
|
602
|
+
retainedWorktreeTransitionProducerDigest(dirname(fileURLToPath(import.meta.url))))
|
|
603
|
+
throw new Error('Interrupted plugin operation belongs to a different controller; resume with its original version.');
|
|
604
|
+
const allocation = state.pending.staging;
|
|
605
|
+
if (allocation && existsSync(allocation.path)) {
|
|
606
|
+
const parent = dirname(unitDirectory(target, state.pending.targetVersion));
|
|
607
|
+
if (dirname(allocation.path) !== parent ||
|
|
608
|
+
!/^\.stage-[a-f0-9-]{36}$/u.test(allocation.path.slice(parent.length + 1)))
|
|
609
|
+
throw new Error('Recorded staging path is outside this version target.');
|
|
610
|
+
assertCanonical(allocation.path);
|
|
611
|
+
const current = lstatSync(allocation.path);
|
|
612
|
+
if (!current.isDirectory() ||
|
|
613
|
+
(allocation.device === undefined
|
|
614
|
+
? readdirSync(allocation.path).length !== 0
|
|
615
|
+
: current.dev !== allocation.device || current.ino !== allocation.inode))
|
|
616
|
+
throw new Error('Interrupted staging directory ownership is unresolved.');
|
|
617
|
+
rmSync(allocation.path, { recursive: true });
|
|
618
|
+
sync(parent);
|
|
619
|
+
}
|
|
620
|
+
if (state.pending.action !== input.action || state.pending.targetVersion !== result.version)
|
|
621
|
+
throw new Error('Resume the interrupted original plugin command before selecting a different operation.');
|
|
622
|
+
}
|
|
623
|
+
if ((input.action === 'update' || input.action === 'rollback') &&
|
|
624
|
+
(!state || (input.action === 'update' && !state.currentVersion)))
|
|
625
|
+
throw new Error('Install a managed plugin before updating or rolling it back.');
|
|
626
|
+
const version = input.action === 'remove' ? null : result.version;
|
|
627
|
+
if (version === null && input.action !== 'remove')
|
|
628
|
+
throw new Error('No previous managed plugin version is available.');
|
|
629
|
+
const prior = state?.currentVersion ?? null;
|
|
630
|
+
state ??= {
|
|
631
|
+
owner: '@aviaratech/ai-delivery.plugin@1',
|
|
632
|
+
host: input.host,
|
|
633
|
+
scope: input.scope,
|
|
634
|
+
projectRoot: input.scope === 'user' ? null : target.projectRoot,
|
|
635
|
+
nativeHome: target.nativeHome,
|
|
636
|
+
marketplace: target.marketplace,
|
|
637
|
+
currentVersion: null,
|
|
638
|
+
previousVersion: null,
|
|
639
|
+
installedPath: null,
|
|
640
|
+
};
|
|
641
|
+
const birth = identity(process.pid);
|
|
642
|
+
if (!birth)
|
|
643
|
+
throw new Error('Installer owner identity is unknown.');
|
|
644
|
+
state.pending = {
|
|
645
|
+
action: input.action,
|
|
646
|
+
targetVersion: version,
|
|
647
|
+
owner: { pid: process.pid, birth },
|
|
648
|
+
producerDigest: retainedWorktreeTransitionProducerDigest(dirname(fileURLToPath(import.meta.url))),
|
|
649
|
+
command: { phase: 'idle' },
|
|
650
|
+
};
|
|
651
|
+
writePrivateJsonFileAtomically(target.statePath, state);
|
|
652
|
+
const controller = new AbortController();
|
|
653
|
+
const cancel = () => controller.abort();
|
|
654
|
+
process.once('SIGINT', cancel);
|
|
655
|
+
process.once('SIGTERM', cancel);
|
|
656
|
+
const publish = (command) => {
|
|
657
|
+
state.pending.command = command;
|
|
658
|
+
writePrivateJsonFileAtomically(target.statePath, state);
|
|
659
|
+
};
|
|
660
|
+
const native = (args) => command(target.tool, args, target.projectRoot, controller.signal, publish);
|
|
661
|
+
const selector = `ai-delivery@${target.marketplace}`;
|
|
662
|
+
const removeArgs = input.host === 'codex'
|
|
663
|
+
? ['plugin', 'remove', selector, '--json']
|
|
664
|
+
: ['plugin', 'uninstall', selector, '--scope', input.scope, '--keep-data'];
|
|
665
|
+
const installArgs = input.host === 'codex'
|
|
666
|
+
? ['plugin', 'add', selector, '--json']
|
|
667
|
+
: ['plugin', 'install', selector, '--scope', input.scope];
|
|
668
|
+
let selectionStarted = false;
|
|
669
|
+
try {
|
|
670
|
+
state.nativeVersion = (await native(['--version'])).trim();
|
|
671
|
+
if (input.host === 'claude-code' && !(await native(['plugin', 'uninstall', '--help'])).includes('--keep-data'))
|
|
672
|
+
throw new Error('This Claude Code version cannot preserve plugin data; upgrade the native tool first.');
|
|
673
|
+
process.stderr.write(`ai-delivery.plugin ${JSON.stringify({ action: input.action, host: input.host, scope: input.scope, stage: version ? 'prepare_version' : 'native_remove', version })}\n`);
|
|
674
|
+
const before = await inventory(input, target, controller.signal, publish);
|
|
675
|
+
const registered = registration(input, target, before), current = nativeEntry(input, target, before);
|
|
676
|
+
if (current && !prior && !(resuming && current.version === version))
|
|
677
|
+
throw new Error('Existing native plugin is not owned by managed state.');
|
|
678
|
+
let recoverySourceVersion = current?.version;
|
|
679
|
+
if (current && resuming && input.host === 'claude-code') {
|
|
680
|
+
recoverySourceVersion = claudeCatalogSelection(target).version;
|
|
681
|
+
if (recoverySourceVersion !== prior && recoverySourceVersion !== version)
|
|
682
|
+
throw new Error('Interrupted native selection has an incompatible catalog version.');
|
|
683
|
+
}
|
|
684
|
+
if (current)
|
|
685
|
+
selectedRoot(input, target, current, current.version === prior
|
|
686
|
+
? state.installedPath
|
|
687
|
+
: join(target.nativeHome, 'plugins/cache', target.marketplace, 'ai-delivery', current.version), recoverySourceVersion);
|
|
688
|
+
if (version) {
|
|
689
|
+
await stageVersion(target, version, controller.signal, publish, (allocation) => {
|
|
690
|
+
if (allocation)
|
|
691
|
+
state.pending.staging = allocation;
|
|
692
|
+
else
|
|
693
|
+
delete state.pending.staging;
|
|
694
|
+
writePrivateJsonFileAtomically(target.statePath, state);
|
|
695
|
+
});
|
|
696
|
+
selectionStarted = true;
|
|
697
|
+
catalog(target, version);
|
|
698
|
+
if (!registered)
|
|
699
|
+
await native([
|
|
700
|
+
'plugin',
|
|
701
|
+
'marketplace',
|
|
702
|
+
'add',
|
|
703
|
+
target.catalog,
|
|
704
|
+
...(input.host === 'codex' ? ['--json'] : ['--scope', input.scope]),
|
|
705
|
+
]);
|
|
706
|
+
}
|
|
707
|
+
if (current && (current.version !== version || input.action === 'remove')) {
|
|
708
|
+
selectionStarted = true;
|
|
709
|
+
await native(removeArgs);
|
|
710
|
+
}
|
|
711
|
+
let installedPath = state.installedPath;
|
|
712
|
+
if (version) {
|
|
713
|
+
const installed = await native(installArgs);
|
|
714
|
+
if (input.host === 'codex')
|
|
715
|
+
installedPath = z.object({ installedPath: z.string() }).parse(JSON.parse(installed)).installedPath;
|
|
716
|
+
}
|
|
717
|
+
const after = await inventory(input, target, controller.signal, publish);
|
|
718
|
+
registration(input, target, after);
|
|
719
|
+
const readback = nativeEntry(input, target, after);
|
|
720
|
+
if (version ? readback?.version !== version : readback !== undefined)
|
|
721
|
+
throw new Error('Native installation readback does not match the selected version.');
|
|
722
|
+
if (version) {
|
|
723
|
+
selectedRoot(input, target, readback, readback?.installedPath ?? installedPath);
|
|
724
|
+
if (!readback.enabled)
|
|
725
|
+
throw new Error('Native plugin selection is disabled.');
|
|
726
|
+
}
|
|
727
|
+
state.currentVersion = version;
|
|
728
|
+
if (prior !== version)
|
|
729
|
+
state.previousVersion = prior;
|
|
730
|
+
state.installedPath = version ? (readback?.installedPath ?? installedPath) : null;
|
|
731
|
+
delete state.pending;
|
|
732
|
+
writePrivateJsonFileAtomically(target.statePath, state);
|
|
733
|
+
result.version = version;
|
|
734
|
+
result.changed = prior !== version;
|
|
735
|
+
result.installed = version !== null;
|
|
736
|
+
result.restartRequired = true;
|
|
737
|
+
if (version)
|
|
738
|
+
result.nativePluginRoot =
|
|
739
|
+
input.host === 'codex' ? (state.installedPath ?? pluginRoot(target, version)) : pluginRoot(target, version);
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
if (!resuming &&
|
|
744
|
+
!controller.signal.aborted &&
|
|
745
|
+
!selectionStarted &&
|
|
746
|
+
state.pending?.command.phase === 'idle' &&
|
|
747
|
+
state.pending.staging === undefined) {
|
|
748
|
+
delete state.pending;
|
|
749
|
+
try {
|
|
750
|
+
writePrivateJsonFileAtomically(target.statePath, state);
|
|
751
|
+
}
|
|
752
|
+
catch (recoveryError) {
|
|
753
|
+
throw new AggregateError([error, recoveryError], 'Plugin preparation failed and pending-state cleanup also failed.');
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Preserve recovery after selection and restore the prior catalog when possible.
|
|
757
|
+
if (selectionStarted && prior) {
|
|
758
|
+
try {
|
|
759
|
+
catalog(target, prior);
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
/* Keep the original failure and pending intent. */
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
throw error;
|
|
766
|
+
}
|
|
767
|
+
finally {
|
|
768
|
+
process.removeListener('SIGINT', cancel);
|
|
769
|
+
process.removeListener('SIGTERM', cancel);
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
//# sourceMappingURL=pluginInstaller.js.map
|