relay-dsh-plugin-manager 0.1.0-rc.1

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/lib/index.js ADDED
@@ -0,0 +1,1621 @@
1
+ import { t as fail } from "./errors-BGx4K2uq.js";
2
+ import "@deepseek-ai/cordis-plugin-loader";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { defineTool } from "@deepseek-ai/dsh-tools";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { parse, parseDocument } from "yaml";
9
+ import { homedir, tmpdir } from "node:os";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { spawn } from "node:child_process";
12
+ //#region src/conversation.ts
13
+ function jsonValue(value) {
14
+ return JSON.parse(JSON.stringify(value));
15
+ }
16
+ function renderJson(_args, value) {
17
+ return [{
18
+ type: "text",
19
+ text: JSON.stringify(value, null, 2)
20
+ }];
21
+ }
22
+ function confirmationCursor(execution) {
23
+ const session = execution.agent?.session;
24
+ if (session === void 0) return null;
25
+ let userMessageSeq = -1;
26
+ for (const event of session.events) if (event.type === "user/message") userMessageSeq = event.seq;
27
+ return {
28
+ sessionId: String(session.id),
29
+ userMessageSeq
30
+ };
31
+ }
32
+ function registerConversationSurface(ctx, manager) {
33
+ const confirmations = /* @__PURE__ */ new Map();
34
+ ctx.tools.register(defineTool({
35
+ name: "plugin_discover",
36
+ description: "Read-only DSH plugin discovery. List installed plugins, search registered sources, inspect one npm/GitHub source, or query plugin/operation status. This tool never changes the profile.",
37
+ parameters: {
38
+ action: {
39
+ type: "string",
40
+ enum: [
41
+ "list",
42
+ "search",
43
+ "inspect",
44
+ "status"
45
+ ],
46
+ required: true,
47
+ description: "The read-only operation."
48
+ },
49
+ query: {
50
+ type: "string",
51
+ description: "Natural-language search query."
52
+ },
53
+ target: {
54
+ type: "string",
55
+ description: "npm package, GitHub source, or installed package name."
56
+ },
57
+ operationId: {
58
+ type: "string",
59
+ description: "Operation id returned by plugin_manage."
60
+ },
61
+ maxResults: {
62
+ type: "integer",
63
+ description: "Maximum merged search results."
64
+ }
65
+ },
66
+ output: {
67
+ schema: { type: "json" },
68
+ render: renderJson
69
+ },
70
+ timeoutMs: 35e3,
71
+ isConcurrencySafe: () => true,
72
+ execute: async (args, execution) => jsonValue(await manager.discover(args, execution.signal))
73
+ }));
74
+ ctx.tools.register(defineTool({
75
+ name: "plugin_manage",
76
+ description: "Plan and run DSH plugin mutations. ALWAYS call action=plan first and show its impact. NEVER treat the request that produced a plan as confirmation. Only call action=execute with its confirmationToken after a later, explicit user confirmation. Install sources are npm or GitHub; search providers do not define installers.",
77
+ parameters: {
78
+ action: {
79
+ type: "string",
80
+ enum: [
81
+ "plan",
82
+ "execute",
83
+ "status",
84
+ "cancel"
85
+ ],
86
+ required: true,
87
+ description: "Lifecycle stage. Mutations require plan followed by execute."
88
+ },
89
+ operation: {
90
+ type: "string",
91
+ enum: [
92
+ "install",
93
+ "remove",
94
+ "update",
95
+ "enable",
96
+ "disable",
97
+ "restart"
98
+ ],
99
+ description: "Mutation to plan."
100
+ },
101
+ target: {
102
+ type: "string",
103
+ description: "Installed package name, or install source when source is omitted."
104
+ },
105
+ source: {
106
+ type: "string",
107
+ description: "npm package/version or canonical GitHub repository/ref."
108
+ },
109
+ confirmationToken: {
110
+ type: "string",
111
+ description: "One-use token from a prior plan."
112
+ },
113
+ operationId: {
114
+ type: "string",
115
+ description: "Tracked operation id."
116
+ }
117
+ },
118
+ output: {
119
+ schema: { type: "json" },
120
+ render: renderJson
121
+ },
122
+ execute: async (args, execution) => {
123
+ if (args.action === "plan") {
124
+ if (args.operation === void 0) fail("INVALID_ACTION", "Planning requires an operation.");
125
+ const plan = await manager.plan({
126
+ operation: args.operation,
127
+ ...args.target === void 0 ? {} : { target: args.target },
128
+ ...args.source === void 0 ? {} : { source: args.source }
129
+ }, execution.signal);
130
+ const cursor = confirmationCursor(execution);
131
+ if (cursor === null) fail("CONFIRMATION_REQUIRED", "Planning requires a DSH Agent session.");
132
+ const now = Date.now();
133
+ for (const [token, binding] of confirmations) if (binding.expiresAt <= now) confirmations.delete(token);
134
+ confirmations.set(plan.confirmationToken, {
135
+ ...cursor,
136
+ expiresAt: Date.parse(plan.expiresAt)
137
+ });
138
+ return jsonValue(plan);
139
+ }
140
+ if (args.action === "execute") {
141
+ if (args.confirmationToken === void 0) fail("CONFIRMATION_REQUIRED", "Execution requires a confirmation token.");
142
+ const binding = confirmations.get(args.confirmationToken);
143
+ const cursor = confirmationCursor(execution);
144
+ if (binding === void 0 || cursor === null || cursor.sessionId !== binding.sessionId) fail("CONFIRMATION_REQUIRED", "Confirmation token is not bound to this DSH conversation.");
145
+ if (cursor.userMessageSeq <= binding.userMessageSeq) fail("CONFIRMATION_REQUIRED", "Wait for a later explicit user confirmation before execution.");
146
+ confirmations.delete(args.confirmationToken);
147
+ return jsonValue(manager.execute(args.confirmationToken));
148
+ }
149
+ if (args.operationId === void 0) fail("OPERATION_NOT_FOUND", `${args.action} requires an operation id.`);
150
+ return jsonValue(args.action === "cancel" ? manager.cancel(args.operationId) : manager.operation(args.operationId));
151
+ }
152
+ }));
153
+ ctx.commands.register({
154
+ name: "plugins",
155
+ description: "manage DSH plugins through this conversation",
156
+ input: { hint: "<request>" },
157
+ handler: ({ agent, rawInput }) => {
158
+ const request = rawInput.trim() === "" ? "List the installed DSH plugins and summarize their status." : rawInput.trim();
159
+ agent.steer(createUserMessage({
160
+ content: [{
161
+ type: "text",
162
+ text: request
163
+ }],
164
+ source: { kind: "user" }
165
+ }));
166
+ return {
167
+ kind: "success",
168
+ text: "Plugin request submitted to this conversation."
169
+ };
170
+ }
171
+ });
172
+ }
173
+ //#endregion
174
+ //#region src/hot-runtime.ts
175
+ function parseSimpleHotPatch(text) {
176
+ let value;
177
+ try {
178
+ value = parse(text);
179
+ } catch {
180
+ return null;
181
+ }
182
+ if (!Array.isArray(value) || value.length === 0) return null;
183
+ const rows = [];
184
+ for (const patch of value) {
185
+ if (typeof patch !== "object" || patch === null || Array.isArray(patch)) return null;
186
+ if (Object.keys(patch).length !== 1 || !Array.isArray(patch.insert)) return null;
187
+ for (const raw of patch.insert) {
188
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
189
+ const entry = raw;
190
+ if (Object.keys(entry).some((key) => key !== "id" && key !== "name")) return null;
191
+ if (typeof entry.id !== "string" || entry.id === "" || typeof entry.name !== "string" || entry.name === "") return null;
192
+ rows.push({
193
+ id: entry.id,
194
+ name: entry.name
195
+ });
196
+ }
197
+ }
198
+ return rows.length === 0 ? null : rows;
199
+ }
200
+ var HotRuntime = class {
201
+ handles = /* @__PURE__ */ new Map();
202
+ sequence = 0;
203
+ includeClass;
204
+ ctx;
205
+ profileDir;
206
+ timeoutMs;
207
+ loadInclude;
208
+ constructor(ctx, profileDir, timeoutMs = 1e4, loadInclude) {
209
+ this.ctx = ctx;
210
+ this.profileDir = profileDir;
211
+ this.timeoutMs = timeoutMs;
212
+ this.loadInclude = loadInclude;
213
+ this.clean();
214
+ }
215
+ hotDir() {
216
+ return join(this.profileDir, ".relay-plugin-manager");
217
+ }
218
+ clean() {
219
+ let files;
220
+ try {
221
+ files = readdirSync(this.hotDir());
222
+ } catch {
223
+ return;
224
+ }
225
+ for (const file of files) if (/^hot-\d+\.yml$/u.test(file)) rmSync(join(this.hotDir(), file), { force: true });
226
+ }
227
+ async include() {
228
+ if (this.includeClass !== void 0) return this.includeClass;
229
+ if (this.loadInclude !== void 0) {
230
+ this.includeClass = await this.loadInclude();
231
+ return this.includeClass;
232
+ }
233
+ try {
234
+ const module = await import("@deepseek-ai/cordis-plugin-include");
235
+ const Include = module.Include ?? module.default;
236
+ if (Include === void 0) throw new Error("missing Include export");
237
+ this.includeClass = class RuntimeHotInclude extends Include {
238
+ write() {}
239
+ };
240
+ } catch {
241
+ this.includeClass = null;
242
+ }
243
+ return this.includeClass;
244
+ }
245
+ async activate(surface) {
246
+ if (this.handles.has(surface.packageName)) return {
247
+ active: true,
248
+ restartRequired: false,
249
+ reason: null
250
+ };
251
+ const Include = await this.include();
252
+ if (Include === null) return {
253
+ active: false,
254
+ restartRequired: true,
255
+ reason: "DSH Include runtime is unavailable."
256
+ };
257
+ let rows = null;
258
+ if (surface.bundlePatch !== null) {
259
+ try {
260
+ rows = parseSimpleHotPatch(readFileSync(join(this.profileDir, "node_modules", surface.packageName, surface.bundlePatch), "utf8"));
261
+ } catch {
262
+ rows = null;
263
+ }
264
+ if (rows === null) return {
265
+ active: false,
266
+ restartRequired: true,
267
+ reason: "Bundle patch is not a plain insert-only patch."
268
+ };
269
+ } else if (surface.client) rows = [{
270
+ id: `client-${surface.packageName.replace(/[^A-Za-z0-9_.-]/gu, "-")}`,
271
+ name: surface.packageName
272
+ }];
273
+ else return {
274
+ active: false,
275
+ restartRequired: true,
276
+ reason: "Package has no hot-activatable DSH surface."
277
+ };
278
+ mkdirSync(this.hotDir(), {
279
+ recursive: true,
280
+ mode: 448
281
+ });
282
+ const file = join(this.hotDir(), `hot-${String(++this.sequence)}.yml`);
283
+ writeFileSync(file, rows.map((row) => ["- id: " + JSON.stringify(`rpm-${row.id}`), " name: " + JSON.stringify(row.name)].join("\n")).join("\n") + "\n", { mode: 384 });
284
+ let handle;
285
+ let timeout;
286
+ try {
287
+ handle = this.ctx.plugin(Include, { path: pathToFileURL(file).href });
288
+ await Promise.race([handle.await(), new Promise((_resolve, reject) => {
289
+ timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("hot activation timed out")), this.timeoutMs);
290
+ })]);
291
+ this.handles.set(surface.packageName, handle);
292
+ return {
293
+ active: true,
294
+ restartRequired: false,
295
+ reason: null
296
+ };
297
+ } catch (error) {
298
+ try {
299
+ await handle?.dispose();
300
+ } catch {}
301
+ return {
302
+ active: false,
303
+ restartRequired: true,
304
+ reason: error instanceof Error ? error.message : String(error)
305
+ };
306
+ } finally {
307
+ if (timeout !== void 0) clearTimeout(timeout);
308
+ }
309
+ }
310
+ async deactivate(packageName) {
311
+ const handle = this.handles.get(packageName);
312
+ if (handle === void 0) return false;
313
+ this.handles.delete(packageName);
314
+ try {
315
+ await handle.dispose();
316
+ return true;
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+ isActive(packageName) {
322
+ return this.handles.has(packageName);
323
+ }
324
+ };
325
+ //#endregion
326
+ //#region src/source.ts
327
+ const EXACT_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
328
+ const NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
329
+ const FULL_COMMIT = /^[a-f0-9]{40}$/iu;
330
+ const GITHUB_PART = /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,98}[A-Za-z0-9])?$/u;
331
+ const UNSAFE_TOKEN = /[\u0000-\u0020\u007f;&|`$<>]/u;
332
+ function safeToken(value) {
333
+ const source = String(value ?? "").trim();
334
+ if (source === "" || source.startsWith("-") || UNSAFE_TOKEN.test(source)) fail("INVALID_SOURCE", "Plugin source must be one safe npm or GitHub token.");
335
+ return source;
336
+ }
337
+ function parseNpmSpec(value, requireExact = false) {
338
+ const spec = safeToken(value);
339
+ let packageName = spec;
340
+ let version;
341
+ const separator = spec.lastIndexOf("@");
342
+ const scopedBoundary = spec.startsWith("@") ? spec.indexOf("/") : -1;
343
+ if (separator > Math.max(0, scopedBoundary)) {
344
+ packageName = spec.slice(0, separator);
345
+ version = spec.slice(separator + 1);
346
+ }
347
+ if (!NPM_NAME.test(packageName)) fail("INVALID_NPM_SPEC", "Plugin source is not a valid npm package name.");
348
+ if (version !== void 0 && !EXACT_SEMVER.test(version)) fail("INVALID_NPM_VERSION", "npm plugin versions must be exact semantic versions.");
349
+ if (requireExact && version === void 0) fail("IMMUTABLE_SOURCE_REQUIRED", "Installation requires an exact npm version.");
350
+ return {
351
+ kind: "npm",
352
+ package: packageName,
353
+ ...version === void 0 ? {} : { version }
354
+ };
355
+ }
356
+ function validGithubPart(value) {
357
+ return GITHUB_PART.test(value) && value !== "." && value !== ".." && !value.endsWith(".git");
358
+ }
359
+ function parseGithubSpec(value, requireCommit = false) {
360
+ const spec = safeToken(value);
361
+ let owner;
362
+ let repo;
363
+ let ref;
364
+ if (spec.startsWith("github:")) {
365
+ const match = /^github:([^/]+)\/([^#]+?)(?:#(.+))?$/u.exec(spec);
366
+ if (match === null) fail("INVALID_GITHUB_SPEC", "GitHub source must use github:owner/repo[#ref].");
367
+ owner = match[1];
368
+ repo = match[2];
369
+ ref = match[3];
370
+ } else if (spec.startsWith("https://github.com/")) {
371
+ let url;
372
+ try {
373
+ url = new URL(spec);
374
+ } catch {
375
+ fail("INVALID_GITHUB_SPEC", "GitHub URL is invalid.");
376
+ }
377
+ if (url.protocol !== "https:" || url.hostname !== "github.com" || url.search !== "") fail("INVALID_GITHUB_SPEC", "Only canonical HTTPS github.com repository URLs are supported.");
378
+ const parts = url.pathname.split("/").filter(Boolean);
379
+ owner = parts[0];
380
+ repo = parts[1]?.replace(/\.git$/u, "");
381
+ if (parts.length > 2) {
382
+ if (parts[2] !== "tree" && parts[2] !== "commit" || parts.length < 4) fail("INVALID_GITHUB_SPEC", "GitHub URL must identify a repository, tree, or commit.");
383
+ ref = decodeURIComponent(parts.slice(3).join("/"));
384
+ } else if (url.hash !== "") fail("INVALID_GITHUB_SPEC", "Use a tree/commit URL or github:owner/repo#ref for GitHub refs.");
385
+ } else if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(spec)) fail("INVALID_GITHUB_SPEC", "Only canonical HTTPS github.com repository URLs are supported.");
386
+ else return null;
387
+ if (!validGithubPart(owner ?? "") || !validGithubPart(repo ?? "")) fail("INVALID_GITHUB_SPEC", "GitHub owner or repository name is invalid.");
388
+ if (ref !== void 0 && (ref === "" || ref.length > 200 || UNSAFE_TOKEN.test(ref))) fail("INVALID_GITHUB_REF", "GitHub ref is invalid.");
389
+ if (requireCommit && !FULL_COMMIT.test(ref ?? "")) fail("IMMUTABLE_SOURCE_REQUIRED", "Installation requires a full GitHub commit.");
390
+ return {
391
+ kind: "github",
392
+ owner,
393
+ repo,
394
+ ...ref === void 0 ? {} : { ref }
395
+ };
396
+ }
397
+ function parsePluginSource(value) {
398
+ if (typeof value === "string") return parseGithubSpec(value) ?? parseNpmSpec(value);
399
+ if (value.kind === "npm") return parseNpmSpec(`${value.package}${value.version === void 0 ? "" : `@${value.version}`}`);
400
+ return parseGithubSpec(`github:${value.owner}/${value.repo}${value.ref === void 0 ? "" : `#${value.ref}`}`);
401
+ }
402
+ function renderSource(source) {
403
+ if (source.kind === "npm") return `${source.package}${source.version === void 0 ? "" : `@${source.version}`}`;
404
+ return `github:${source.owner}/${source.repo}${source.ref === void 0 ? "" : `#${source.ref}`}`;
405
+ }
406
+ function manifestDsh(value) {
407
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {
408
+ bundlePatch: null,
409
+ client: false
410
+ };
411
+ const dsh = value.dsh;
412
+ if (typeof dsh !== "object" || dsh === null || Array.isArray(dsh)) return {
413
+ bundlePatch: null,
414
+ client: false
415
+ };
416
+ const bundle = dsh.bundle;
417
+ const patch = typeof bundle === "object" && bundle !== null && !Array.isArray(bundle) ? bundle.patch : void 0;
418
+ return {
419
+ bundlePatch: typeof patch === "string" && patch.trim() !== "" ? patch : null,
420
+ client: dsh.client !== void 0
421
+ };
422
+ }
423
+ function validatePluginManifest(manifest, expectedName) {
424
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) fail("INVALID_PLUGIN_MANIFEST", "Plugin package manifest must be an object.");
425
+ const packageName = String(manifest.name ?? "");
426
+ if (!NPM_NAME.test(packageName)) fail("INVALID_PLUGIN_MANIFEST", "Plugin manifest has no valid package name.");
427
+ if (expectedName !== void 0 && packageName !== expectedName) fail("PACKAGE_NAME_MISMATCH", "Resolved package name does not match the requested npm package.");
428
+ const surface = manifestDsh(manifest);
429
+ if (surface.bundlePatch === null && !surface.client) fail("NOT_DSH_PLUGIN", `${packageName} declares neither dsh.bundle.patch nor dsh.client.`);
430
+ return {
431
+ packageName,
432
+ ...surface
433
+ };
434
+ }
435
+ async function fetchJson(url, options, headers = {}) {
436
+ const fetchImpl = options.fetch ?? globalThis.fetch;
437
+ let response;
438
+ try {
439
+ response = await fetchImpl(url, {
440
+ headers: {
441
+ accept: "application/json",
442
+ ...headers
443
+ },
444
+ redirect: "follow",
445
+ signal: options.signal
446
+ });
447
+ } catch (error) {
448
+ fail("NETWORK_ERROR", `Could not reach plugin source: ${error instanceof Error ? error.message : String(error)}`);
449
+ }
450
+ if (!response.ok) fail("SOURCE_HTTP_ERROR", `Plugin source returned HTTP ${response.status}.`, {
451
+ url,
452
+ status: response.status
453
+ });
454
+ try {
455
+ return await response.json();
456
+ } catch {
457
+ fail("INVALID_SOURCE_METADATA", "Plugin source returned invalid JSON metadata.", { url });
458
+ }
459
+ }
460
+ function repositoryIdentity(value) {
461
+ const raw = typeof value === "string" ? value : typeof value === "object" && value !== null ? value.url : void 0;
462
+ if (typeof raw !== "string" || raw.trim() === "") return null;
463
+ const normalized = raw.trim().replace(/^git\+/u, "").replace(/^git@github\.com:/u, "https://github.com/").replace(/^github:/u, "https://github.com/").replace(/\.git(?:#.*)?$/u, "").replace(/\/$/u, "");
464
+ const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/iu.exec(normalized);
465
+ return match === null ? normalized.toLowerCase() : `github.com/${match[1].toLowerCase()}/${match[2].toLowerCase()}`;
466
+ }
467
+ function npmMetadataUrl(name, version) {
468
+ return `https://registry.npmjs.org/${encodeURIComponent(name).replace(/^%40/u, "@")}/${encodeURIComponent(version ?? "latest")}`;
469
+ }
470
+ async function inspectNpm(source, options = {}) {
471
+ const manifest = await fetchJson(npmMetadataUrl(source.package, source.version), options);
472
+ const plugin = validatePluginManifest(manifest, source.package);
473
+ const version = String(manifest.version ?? "");
474
+ if (!EXACT_SEMVER.test(version)) fail("INVALID_NPM_VERSION", "Registry metadata has no exact semantic version.");
475
+ const integrity = manifest.dist?.integrity;
476
+ if (typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/=]+$/u.test(integrity)) fail("NPM_INTEGRITY_MISSING", "Registry metadata has no SHA-512 package integrity.");
477
+ const exact = {
478
+ kind: "npm",
479
+ package: plugin.packageName,
480
+ version
481
+ };
482
+ return {
483
+ source: exact,
484
+ sourceType: "npm",
485
+ requestedSpec: renderSource(source),
486
+ installSpec: renderSource(exact),
487
+ packageName: plugin.packageName,
488
+ version,
489
+ integrity,
490
+ repository: repositoryIdentity(manifest.repository),
491
+ description: typeof manifest.description === "string" ? manifest.description : null,
492
+ bundlePatch: plugin.bundlePatch,
493
+ client: plugin.client
494
+ };
495
+ }
496
+ function githubHeaders(env = process.env) {
497
+ const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
498
+ return {
499
+ "user-agent": "relay-dsh-plugin-manager",
500
+ "x-github-api-version": "2022-11-28",
501
+ ...token === void 0 || token === "" ? {} : { authorization: `Bearer ${token}` }
502
+ };
503
+ }
504
+ async function inspectGithub(source, options = {}) {
505
+ const headers = githubHeaders(options.env);
506
+ let ref = source.ref;
507
+ if (ref === void 0) {
508
+ const repository = await fetchJson(`https://api.github.com/repos/${source.owner}/${source.repo}`, options, headers);
509
+ ref = typeof repository.default_branch === "string" ? repository.default_branch : void 0;
510
+ if (ref === void 0 || ref === "") fail("INVALID_SOURCE_METADATA", "GitHub repository has no default branch.");
511
+ }
512
+ const commit = await fetchJson(`https://api.github.com/repos/${source.owner}/${source.repo}/commits/${encodeURIComponent(ref)}`, options, headers);
513
+ const sha = String(commit.sha ?? "").toLowerCase();
514
+ if (!FULL_COMMIT.test(sha)) fail("INVALID_SOURCE_METADATA", "GitHub did not resolve the source to a full commit.");
515
+ const manifest = await fetchJson(`https://raw.githubusercontent.com/${source.owner}/${source.repo}/${sha}/package.json`, options);
516
+ const plugin = validatePluginManifest(manifest);
517
+ const exact = {
518
+ kind: "github",
519
+ owner: source.owner,
520
+ repo: source.repo,
521
+ ref: sha
522
+ };
523
+ return {
524
+ source: exact,
525
+ sourceType: "github",
526
+ requestedSpec: renderSource(source),
527
+ installSpec: renderSource(exact),
528
+ packageName: plugin.packageName,
529
+ commit: sha,
530
+ repository: `github.com/${source.owner.toLowerCase()}/${source.repo.toLowerCase()}`,
531
+ description: typeof manifest.description === "string" ? manifest.description : null,
532
+ bundlePatch: plugin.bundlePatch,
533
+ client: plugin.client
534
+ };
535
+ }
536
+ async function inspectPluginSource(value, options = {}) {
537
+ const source = parsePluginSource(value);
538
+ return source.kind === "npm" ? inspectNpm(source, options) : inspectGithub(source, options);
539
+ }
540
+ function inspectionIdentity(inspection) {
541
+ return inspection.repository ?? `${inspection.sourceType}:${inspection.packageName.toLowerCase()}`;
542
+ }
543
+ //#endregion
544
+ //#region src/search.ts
545
+ function searchQuery(value) {
546
+ const query = value.trim();
547
+ if (query === "" || query.length > 120 || /[\u0000-\u001f\u007f]/u.test(query)) fail("INVALID_SEARCH_QUERY", "Search query must contain 1 to 120 printable characters.");
548
+ return query;
549
+ }
550
+ function abortReason(signal) {
551
+ return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("search cancelled");
552
+ }
553
+ async function searchProvider(provider, query, maxResults, parent, timeoutMs) {
554
+ const controller = new AbortController();
555
+ const onAbort = () => controller.abort(parent?.reason);
556
+ if (parent?.aborted === true) throw abortReason(parent);
557
+ parent?.addEventListener("abort", onAbort, { once: true });
558
+ const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`provider timed out after ${timeoutMs}ms`)), timeoutMs);
559
+ try {
560
+ const result = await provider.search({
561
+ query,
562
+ maxResults,
563
+ signal: controller.signal
564
+ });
565
+ if (!Array.isArray(result)) throw new TypeError("provider result must be an array");
566
+ return result.slice(0, maxResults);
567
+ } finally {
568
+ clearTimeout(timeout);
569
+ parent?.removeEventListener("abort", onAbort);
570
+ }
571
+ }
572
+ function candidateSources(provider, rows) {
573
+ const output = [];
574
+ const ranked = [...rows].sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.id.localeCompare(right.id));
575
+ for (const [rank, candidate] of ranked.entries()) {
576
+ if (typeof candidate.id !== "string" || candidate.id.trim() === "" || !Array.isArray(candidate.sources)) continue;
577
+ for (const raw of candidate.sources.slice(0, 3)) try {
578
+ output.push({
579
+ source: parsePluginSource(raw),
580
+ provider,
581
+ evidence: [...candidate.evidence ?? []].filter((value) => typeof value === "string").slice(0, 5),
582
+ rank
583
+ });
584
+ } catch {}
585
+ }
586
+ return output;
587
+ }
588
+ async function searchPlugins(runtime, rawQuery, options = {}) {
589
+ const query = searchQuery(rawQuery);
590
+ const maxResults = Math.max(1, Math.min(20, options.maxResults ?? 6));
591
+ const timeoutMs = Math.max(100, options.providerTimeoutMs ?? 1e4);
592
+ const providers = runtime.entries();
593
+ const settled = await Promise.allSettled(providers.map(async (provider) => ({
594
+ provider: provider.id,
595
+ rows: await searchProvider(provider, query, maxResults, options.signal, timeoutMs)
596
+ })));
597
+ if (options.signal?.aborted === true) throw abortReason(options.signal);
598
+ const providerErrors = [];
599
+ const discovered = [];
600
+ for (let index = 0; index < settled.length; index += 1) {
601
+ const result = settled[index];
602
+ const provider = providers[index].id;
603
+ if (result.status === "rejected") {
604
+ providerErrors.push({
605
+ provider,
606
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
607
+ });
608
+ continue;
609
+ }
610
+ discovered.push(...candidateSources(result.value.provider, result.value.rows));
611
+ }
612
+ const inspect = options.inspect ?? inspectPluginSource;
613
+ const inspected = await Promise.all(discovered.map(async (item) => {
614
+ try {
615
+ return {
616
+ ok: true,
617
+ item,
618
+ inspection: await inspect(item.source, options)
619
+ };
620
+ } catch {
621
+ return { ok: false };
622
+ }
623
+ }));
624
+ const projects = /* @__PURE__ */ new Map();
625
+ let rejectedCandidates = 0;
626
+ for (const result of inspected) {
627
+ if (!result.ok) {
628
+ rejectedCandidates += 1;
629
+ continue;
630
+ }
631
+ const identity = inspectionIdentity(result.inspection);
632
+ const existing = projects.get(identity) ?? {
633
+ identity,
634
+ packageName: result.inspection.packageName,
635
+ description: result.inspection.description,
636
+ repository: result.inspection.repository,
637
+ sources: [],
638
+ recommendedSource: result.inspection.installSpec,
639
+ rank: result.item.rank
640
+ };
641
+ const sameSource = existing.sources.find((source) => source.inspection.installSpec === result.inspection.installSpec);
642
+ if (sameSource === void 0) existing.sources.push({
643
+ inspection: result.inspection,
644
+ providers: [result.item.provider],
645
+ evidence: [...result.item.evidence]
646
+ });
647
+ else {
648
+ if (!sameSource.providers.includes(result.item.provider)) sameSource.providers.push(result.item.provider);
649
+ for (const evidence of result.item.evidence) if (!sameSource.evidence.includes(evidence)) sameSource.evidence.push(evidence);
650
+ }
651
+ existing.rank = Math.min(existing.rank, result.item.rank);
652
+ existing.recommendedSource = existing.sources.find((source) => source.inspection.sourceType === "npm")?.inspection.installSpec ?? existing.sources[0].inspection.installSpec;
653
+ projects.set(identity, existing);
654
+ }
655
+ return {
656
+ query,
657
+ candidates: [...projects.values()].sort((left, right) => left.rank - right.rank || left.packageName.localeCompare(right.packageName)).slice(0, maxResults).map(({ rank: _rank, ...candidate }) => candidate),
658
+ providerErrors,
659
+ rejectedCandidates
660
+ };
661
+ }
662
+ const STATE_VERSION = 1;
663
+ const STATE_DIR = ".relay-plugin-manager";
664
+ function readJsonObject(file, missing) {
665
+ try {
666
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
667
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return missing;
668
+ return parsed;
669
+ } catch (error) {
670
+ if (error.code === "ENOENT") return missing;
671
+ fail("PROFILE_READ_FAILED", `Could not read ${file}: ${error instanceof Error ? error.message : String(error)}`);
672
+ }
673
+ }
674
+ function atomicWrite(file, text) {
675
+ try {
676
+ mkdirSync(dirname(file), {
677
+ recursive: true,
678
+ mode: 448
679
+ });
680
+ const temporary = `${file}.tmp-${process.pid}-${Date.now()}`;
681
+ writeFileSync(temporary, text, { mode: 384 });
682
+ renameSync(temporary, file);
683
+ } catch (error) {
684
+ fail("PROFILE_WRITE_FAILED", `Could not write ${file}: ${error instanceof Error ? error.message : String(error)}`);
685
+ }
686
+ }
687
+ function writeProfileManifest(dir, manifest) {
688
+ atomicWrite(join(dir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`);
689
+ }
690
+ function reconcileRemovedPackage(dir, packageName) {
691
+ const manifest = readProfileManifest(dir);
692
+ if (manifest.dependencies !== void 0) delete manifest.dependencies[packageName];
693
+ const bundles = manifest.dsh?.profile?.bundles;
694
+ if (bundles !== void 0) manifest.dsh.profile.bundles = bundles.filter((name) => name !== packageName);
695
+ writeProfileManifest(dir, manifest);
696
+ }
697
+ function dshHome(env = process.env) {
698
+ const configured = env.DSH_HOME?.trim();
699
+ return resolve(configured === void 0 || configured === "" ? join(homedir(), ".dsh") : configured);
700
+ }
701
+ function profileDirectory(profile = "web", env = process.env) {
702
+ return join(dshHome(env), "profiles", profile);
703
+ }
704
+ function readProfileManifest(dir) {
705
+ return readJsonObject(join(dir, "package.json"), {});
706
+ }
707
+ function profileManifestText(dir) {
708
+ try {
709
+ return readFileSync(join(dir, "package.json"), "utf8");
710
+ } catch (error) {
711
+ if (error.code === "ENOENT") return null;
712
+ fail("PROFILE_READ_FAILED", `Could not read profile manifest: ${error instanceof Error ? error.message : String(error)}`);
713
+ }
714
+ }
715
+ function restoreProfileManifest(dir, text) {
716
+ if (text === null) return;
717
+ atomicWrite(join(dir, "package.json"), text);
718
+ }
719
+ function packageManifest(dir, packageName) {
720
+ const file = join(dir, "node_modules", packageName, "package.json");
721
+ if (!existsSync(file)) return null;
722
+ return readJsonObject(file, {});
723
+ }
724
+ function bundleEntryIds(dir, packageName, patch) {
725
+ try {
726
+ const document = parseDocument(readFileSync(join(dir, "node_modules", packageName, patch), "utf8"));
727
+ if (document.errors.length > 0 || !Array.isArray(document.toJS())) return null;
728
+ const ids = [];
729
+ for (const row of document.toJS()) {
730
+ if (typeof row !== "object" || row === null || Array.isArray(row)) continue;
731
+ const inserted = row.insert;
732
+ if (!Array.isArray(inserted)) continue;
733
+ for (const entry of inserted) {
734
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return null;
735
+ const id = entry.id;
736
+ if (typeof id !== "string" || id.trim() === "") return null;
737
+ if (!ids.includes(id)) ids.push(id);
738
+ }
739
+ }
740
+ return ids.length === 0 ? null : ids;
741
+ } catch {
742
+ return null;
743
+ }
744
+ }
745
+ function packageSurface(dir, packageName, source) {
746
+ const manifest = packageManifest(dir, packageName);
747
+ const dsh = typeof manifest?.dsh === "object" && manifest.dsh !== null && !Array.isArray(manifest.dsh) ? manifest.dsh : {};
748
+ const bundle = typeof dsh.bundle === "object" && dsh.bundle !== null && !Array.isArray(dsh.bundle) ? dsh.bundle : {};
749
+ const patch = typeof bundle.patch === "string" && bundle.patch.trim() !== "" ? bundle.patch : null;
750
+ return {
751
+ packageName,
752
+ source,
753
+ bundle: patch !== null,
754
+ bundlePatch: patch,
755
+ client: dsh.client !== void 0,
756
+ entryIds: patch === null ? null : bundleEntryIds(dir, packageName, patch)
757
+ };
758
+ }
759
+ function statePath(dir) {
760
+ return join(dir, STATE_DIR, "state.json");
761
+ }
762
+ function readManagerState(dir) {
763
+ const state = readJsonObject(statePath(dir), {});
764
+ const disabled = {};
765
+ if (state.version === STATE_VERSION && typeof state.disabled === "object" && state.disabled !== null) {
766
+ for (const [name, ids] of Object.entries(state.disabled)) if (Array.isArray(ids) && ids.every((id) => typeof id === "string")) disabled[name] = [...new Set(ids)];
767
+ }
768
+ return {
769
+ version: STATE_VERSION,
770
+ disabled
771
+ };
772
+ }
773
+ function writeManagerState(dir, state) {
774
+ atomicWrite(statePath(dir), `${JSON.stringify(state, null, 2)}\n`);
775
+ }
776
+ function patchDocument(file) {
777
+ let source = "[]\n";
778
+ try {
779
+ source = readFileSync(file, "utf8");
780
+ } catch (error) {
781
+ if (error.code !== "ENOENT") fail("PROFILE_READ_FAILED", `Could not read profile patch: ${error instanceof Error ? error.message : String(error)}`);
782
+ }
783
+ const document = parseDocument(source);
784
+ if (document.errors.length > 0) fail("ENABLEMENT_CONFLICT", "Profile cordis.patch.yml is not valid YAML.");
785
+ const value = document.toJS();
786
+ if (value === null) document.contents = document.createNode([]);
787
+ else if (!Array.isArray(value)) fail("ENABLEMENT_CONFLICT", "Profile cordis.patch.yml must contain a patch list.");
788
+ return document;
789
+ }
790
+ function exactDisabledRow(value, id) {
791
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
792
+ return Object.keys(value).length === 2 && value.id === id && value.disabled === true;
793
+ }
794
+ function disablePackage(dir, surface) {
795
+ if (surface.packageName === "relay-dsh-plugin-manager") fail("PROTECTED_PLUGIN", "The plugin manager cannot disable itself.");
796
+ if (surface.entryIds === null || surface.entryIds.length === 0) fail("ENABLEMENT_UNSUPPORTED", `${surface.packageName} has no safely attributable Loader entry ids.`);
797
+ const file = join(dir, "cordis.patch.yml");
798
+ const state = readManagerState(dir);
799
+ const document = patchDocument(file);
800
+ const rows = document.toJS();
801
+ const alreadyOwned = new Set(state.disabled[surface.packageName] ?? []);
802
+ for (const id of surface.entryIds) {
803
+ const existing = rows.find((row) => typeof row === "object" && row !== null && !Array.isArray(row) && row.id === id);
804
+ if (existing !== void 0 && !(alreadyOwned.has(id) && exactDisabledRow(existing, id))) fail("ENABLEMENT_CONFLICT", `Profile patch already owns Loader entry "${id}"; it will not be overwritten.`);
805
+ if (existing === void 0) document.add({
806
+ id,
807
+ disabled: true
808
+ });
809
+ }
810
+ state.disabled[surface.packageName] = [...surface.entryIds];
811
+ atomicWrite(file, document.toString());
812
+ writeManagerState(dir, state);
813
+ return [...surface.entryIds];
814
+ }
815
+ function enablePackage(dir, surface) {
816
+ if (surface.packageName === "relay-dsh-plugin-manager") fail("PROTECTED_PLUGIN", "The plugin manager cannot change its own enablement.");
817
+ const state = readManagerState(dir);
818
+ const owned = state.disabled[surface.packageName];
819
+ if (owned === void 0 || owned.length === 0) return [];
820
+ const file = join(dir, "cordis.patch.yml");
821
+ const document = patchDocument(file);
822
+ const rows = document.toJS();
823
+ for (const id of owned) if (rows.filter((row) => typeof row === "object" && row !== null && !Array.isArray(row) && row.id === id).some((row) => !exactDisabledRow(row, id))) fail("ENABLEMENT_CONFLICT", `Manager-owned Loader entry "${id}" was modified and will not be removed.`);
824
+ const keep = rows.filter((row) => !owned.some((id) => exactDisabledRow(row, id)));
825
+ document.contents = document.createNode(keep);
826
+ delete state.disabled[surface.packageName];
827
+ atomicWrite(file, document.toString());
828
+ writeManagerState(dir, state);
829
+ return [...owned];
830
+ }
831
+ function runtimeState(entries) {
832
+ const phases = entries.map((entry) => entry.phase);
833
+ if (phases.includes("failed")) return "failed";
834
+ if (phases.includes("active")) return "active";
835
+ if (phases.some((phase) => phase === "loading" || phase === "pending")) return "loading";
836
+ if (entries.length > 0) return "inactive";
837
+ return "unknown";
838
+ }
839
+ function listPluginStatuses(dir, loaderEntries = []) {
840
+ const manifest = readProfileManifest(dir);
841
+ const bundles = new Set(manifest.dsh?.profile?.bundles ?? []);
842
+ const state = readManagerState(dir);
843
+ return Object.entries(manifest.dependencies ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([packageName, source]) => {
844
+ const surface = packageSurface(dir, packageName, source);
845
+ const ids = surface.entryIds;
846
+ const entries = ids === null ? [] : loaderEntries.filter((entry) => ids.includes(entry.id));
847
+ let enablement = "unknown";
848
+ if (ids !== null) {
849
+ const disabledIds = new Set(state.disabled[packageName] ?? []);
850
+ const disabled = ids.filter((id) => disabledIds.has(id) || entries.some((entry) => entry.id === id && entry.disabled)).length;
851
+ enablement = disabled === 0 ? "enabled" : disabled === ids.length ? "disabled" : "mixed";
852
+ }
853
+ const runtime = runtimeState(entries);
854
+ return {
855
+ packageName,
856
+ source,
857
+ bundle: bundles.has(packageName) || surface.bundle,
858
+ enablement,
859
+ runtime,
860
+ restartRequired: surface.bundle && runtime === "unknown" && !state.disabled[packageName],
861
+ entryIds: ids
862
+ };
863
+ });
864
+ }
865
+ //#endregion
866
+ //#region src/plans.ts
867
+ var PlanStore = class {
868
+ plans = /* @__PURE__ */ new Map();
869
+ used = /* @__PURE__ */ new Set();
870
+ now;
871
+ random;
872
+ ttlMs;
873
+ constructor(options = {}) {
874
+ this.now = options.now ?? Date.now;
875
+ this.random = options.random ?? randomUUID;
876
+ this.ttlMs = options.ttlMs ?? 10 * 6e4;
877
+ }
878
+ create(input) {
879
+ const created = this.now();
880
+ const id = this.random();
881
+ const confirmationToken = this.random();
882
+ const digest = createHash("sha256").update(JSON.stringify({
883
+ id,
884
+ ...input
885
+ })).digest("hex");
886
+ const plan = Object.freeze({
887
+ ...input,
888
+ id,
889
+ digest,
890
+ confirmationToken,
891
+ createdAt: new Date(created).toISOString(),
892
+ expiresAt: new Date(created + this.ttlMs).toISOString()
893
+ });
894
+ this.plans.set(confirmationToken, plan);
895
+ return plan;
896
+ }
897
+ consume(token) {
898
+ if (this.used.has(token)) fail("CONFIRMATION_REPLAYED", "Confirmation token has already been used.");
899
+ const plan = this.plans.get(token);
900
+ if (plan === void 0) fail("CONFIRMATION_REQUIRED", "A valid confirmation token is required.");
901
+ this.plans.delete(token);
902
+ this.used.add(token);
903
+ if (this.now() >= Date.parse(plan.expiresAt)) fail("CONFIRMATION_EXPIRED", "Confirmation token has expired.");
904
+ return plan;
905
+ }
906
+ };
907
+ //#endregion
908
+ //#region src/operations.ts
909
+ var OperationTracker = class {
910
+ records = /* @__PURE__ */ new Map();
911
+ active = null;
912
+ random;
913
+ now;
914
+ constructor(options = {}) {
915
+ this.random = options.random ?? randomUUID;
916
+ this.now = options.now ?? Date.now;
917
+ }
918
+ start(action, target, execute) {
919
+ if (this.active !== null) fail("OPERATION_BUSY", `Plugin operation ${this.active} is still running.`);
920
+ const id = this.random();
921
+ const controller = new AbortController();
922
+ const snapshot = {
923
+ id,
924
+ action,
925
+ target,
926
+ status: "queued",
927
+ progress: "queued",
928
+ startedAt: new Date(this.now()).toISOString()
929
+ };
930
+ const record = {
931
+ snapshot,
932
+ controller,
933
+ done: Promise.resolve()
934
+ };
935
+ this.records.set(id, record);
936
+ this.active = id;
937
+ record.done = Promise.resolve().then(async () => {
938
+ snapshot.status = "running";
939
+ snapshot.progress = "running";
940
+ try {
941
+ const result = await execute({
942
+ signal: controller.signal,
943
+ progress: (message) => {
944
+ snapshot.progress = message.slice(0, 500);
945
+ }
946
+ });
947
+ if (controller.signal.aborted) {
948
+ snapshot.status = "cancelled";
949
+ snapshot.progress = "cancelled";
950
+ } else {
951
+ snapshot.status = "succeeded";
952
+ snapshot.progress = "completed";
953
+ snapshot.result = result;
954
+ }
955
+ } catch (error) {
956
+ if (controller.signal.aborted) {
957
+ snapshot.status = "cancelled";
958
+ snapshot.progress = "cancelled";
959
+ } else {
960
+ snapshot.status = "failed";
961
+ snapshot.progress = "failed";
962
+ snapshot.error = {
963
+ ...typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? { code: error.code } : {},
964
+ message: error instanceof Error ? error.message : String(error)
965
+ };
966
+ }
967
+ } finally {
968
+ snapshot.finishedAt = new Date(this.now()).toISOString();
969
+ if (this.active === id) this.active = null;
970
+ }
971
+ });
972
+ return structuredClone(snapshot);
973
+ }
974
+ get(id) {
975
+ const record = this.records.get(id);
976
+ if (record === void 0) fail("OPERATION_NOT_FOUND", `Plugin operation ${id} was not found.`);
977
+ return structuredClone(record.snapshot);
978
+ }
979
+ cancel(id) {
980
+ const record = this.records.get(id);
981
+ if (record === void 0) fail("OPERATION_NOT_FOUND", `Plugin operation ${id} was not found.`);
982
+ if (record.snapshot.status === "queued" || record.snapshot.status === "running") {
983
+ record.snapshot.progress = "cancelling";
984
+ record.controller.abort(/* @__PURE__ */ new Error("Plugin operation cancelled by user."));
985
+ }
986
+ return structuredClone(record.snapshot);
987
+ }
988
+ async wait(id) {
989
+ const record = this.records.get(id);
990
+ if (record === void 0) fail("OPERATION_NOT_FOUND", `Plugin operation ${id} was not found.`);
991
+ await record.done;
992
+ return this.get(id);
993
+ }
994
+ };
995
+ //#endregion
996
+ //#region src/manager.ts
997
+ const FIBER_PHASE = {
998
+ 0: "pending",
999
+ 1: "loading",
1000
+ 2: "active",
1001
+ 3: "failed",
1002
+ 4: null,
1003
+ 5: "unloading"
1004
+ };
1005
+ const PROTECTED_ENTRY_IDS = /* @__PURE__ */ new Set([
1006
+ "relay-plugin-search-runtime",
1007
+ "relay-plugin-manager-host",
1008
+ "commands",
1009
+ "tools",
1010
+ "webserver",
1011
+ "web-runtime"
1012
+ ]);
1013
+ function safePackageName(value) {
1014
+ const name = value?.trim() ?? "";
1015
+ if (!NPM_NAME.test(name)) fail("INVALID_NPM_SPEC", "A valid installed package name is required.");
1016
+ return name;
1017
+ }
1018
+ function commandResult(result) {
1019
+ return {
1020
+ exitCode: result.exitCode,
1021
+ stdout: result.stdout,
1022
+ stderr: result.stderr
1023
+ };
1024
+ }
1025
+ function installedPackageManifest(profileDir, packageName) {
1026
+ try {
1027
+ return JSON.parse(readFileSync(join(profileDir, "node_modules", packageName, "package.json"), "utf8"));
1028
+ } catch {
1029
+ return null;
1030
+ }
1031
+ }
1032
+ function installedSourceMatches(packageName, dependency, installSpec) {
1033
+ if (parseGithubSpec(installSpec, true) !== null) return dependency === installSpec;
1034
+ const npm = parseNpmSpec(installSpec, true);
1035
+ return npm.package === packageName && dependency === npm.version;
1036
+ }
1037
+ var PluginManager = class {
1038
+ profileDir;
1039
+ searchRuntime;
1040
+ runner;
1041
+ hot;
1042
+ restarter;
1043
+ loader;
1044
+ inspect;
1045
+ plans;
1046
+ operations;
1047
+ fetchOptions;
1048
+ hmrTimeoutMs;
1049
+ constructor(dependencies) {
1050
+ this.profileDir = dependencies.profileDir;
1051
+ this.searchRuntime = dependencies.searchRuntime;
1052
+ this.runner = dependencies.runner;
1053
+ this.hot = dependencies.hot;
1054
+ this.restarter = dependencies.restarter;
1055
+ this.loader = dependencies.loader;
1056
+ this.inspect = dependencies.inspect ?? inspectPluginSource;
1057
+ this.plans = dependencies.plans ?? new PlanStore();
1058
+ this.operations = dependencies.operations ?? new OperationTracker();
1059
+ this.fetchOptions = dependencies.fetchOptions ?? {};
1060
+ this.hmrTimeoutMs = dependencies.hmrTimeoutMs ?? 5e3;
1061
+ }
1062
+ loaderEntries() {
1063
+ if (this.loader === void 0) return [];
1064
+ return [...this.loader.entries()].flatMap((entry) => {
1065
+ const id = entry.options?.id ?? entry.id;
1066
+ if (id === void 0 || id === "") return [];
1067
+ const rawPhase = entry.fiber?.state;
1068
+ const phase = typeof rawPhase === "number" ? FIBER_PHASE[rawPhase] ?? "unknown" : rawPhase ?? null;
1069
+ return [{
1070
+ id,
1071
+ ...entry.options?.name === void 0 ? {} : { name: entry.options.name },
1072
+ disabled: entry.disabled === true,
1073
+ phase
1074
+ }];
1075
+ });
1076
+ }
1077
+ list() {
1078
+ return listPluginStatuses(this.profileDir, this.loaderEntries());
1079
+ }
1080
+ async discover(request, signal) {
1081
+ if (request.action === "list") return {
1082
+ profile: "web",
1083
+ plugins: this.list()
1084
+ };
1085
+ if (request.action === "search") {
1086
+ const options = {
1087
+ ...this.fetchOptions,
1088
+ signal,
1089
+ maxResults: request.maxResults,
1090
+ inspect: this.inspect
1091
+ };
1092
+ return await searchPlugins(this.searchRuntime, request.query ?? "", options);
1093
+ }
1094
+ if (request.action === "inspect") {
1095
+ if (request.target === void 0) fail("INVALID_SOURCE", "A plugin source is required for inspection.");
1096
+ return await this.inspect(request.target, {
1097
+ ...this.fetchOptions,
1098
+ signal
1099
+ });
1100
+ }
1101
+ if (request.operationId !== void 0) return this.operations.get(request.operationId);
1102
+ if (request.target === void 0) return {
1103
+ profile: "web",
1104
+ plugins: this.list()
1105
+ };
1106
+ const name = safePackageName(request.target);
1107
+ const status = this.list().find((plugin) => plugin.packageName === name);
1108
+ if (status === void 0) fail("PLUGIN_NOT_INSTALLED", `${name} is not installed in the web profile.`);
1109
+ return status;
1110
+ }
1111
+ installed(name) {
1112
+ const source = readProfileManifest(this.profileDir).dependencies?.[name];
1113
+ if (source === void 0) fail("PLUGIN_NOT_INSTALLED", `${name} is not installed in the web profile.`);
1114
+ return {
1115
+ source,
1116
+ surface: packageSurface(this.profileDir, name, source)
1117
+ };
1118
+ }
1119
+ assertEnablementAllowed(surface) {
1120
+ if (surface.entryIds?.some((id) => PROTECTED_ENTRY_IDS.has(id)) === true) fail("PROTECTED_PLUGIN", `${surface.packageName} owns protected DSH infrastructure and cannot be toggled.`);
1121
+ }
1122
+ async updateInspection(name, sourceOverride, signal) {
1123
+ if (sourceOverride !== void 0) {
1124
+ const inspection = await this.inspect(sourceOverride, {
1125
+ ...this.fetchOptions,
1126
+ signal
1127
+ });
1128
+ if (inspection.packageName !== name) fail("PACKAGE_NAME_MISMATCH", "Update source resolves to a different package name.");
1129
+ return inspection;
1130
+ }
1131
+ const current = this.installed(name).source;
1132
+ const github = parseGithubSpec(current);
1133
+ const source = github === null ? name : renderSource({
1134
+ kind: "github",
1135
+ owner: github.owner,
1136
+ repo: github.repo
1137
+ });
1138
+ return await this.inspect(source, {
1139
+ ...this.fetchOptions,
1140
+ signal
1141
+ });
1142
+ }
1143
+ async plan(request, signal) {
1144
+ if (request.operation === "restart") {
1145
+ if (!this.restarter.available()) fail("RESTART_UNAVAILABLE", "Automatic restart is unavailable in this deployment.");
1146
+ return this.plans.create({
1147
+ action: "restart",
1148
+ profile: "web",
1149
+ impact: "Restart the running DSH process.",
1150
+ restartExpected: true
1151
+ });
1152
+ }
1153
+ if (request.operation === "install") {
1154
+ const source = request.source ?? request.target;
1155
+ if (source === void 0) fail("INVALID_SOURCE", "Install requires an npm or GitHub source.");
1156
+ const inspection = await this.inspect(source, {
1157
+ ...this.fetchOptions,
1158
+ signal
1159
+ });
1160
+ if (readProfileManifest(this.profileDir).dependencies?.[inspection.packageName] !== void 0) fail("PLUGIN_ALREADY_INSTALLED", `${inspection.packageName} is already installed; use update.`);
1161
+ return this.plans.create({
1162
+ action: "install",
1163
+ profile: "web",
1164
+ packageName: inspection.packageName,
1165
+ installSpec: inspection.installSpec,
1166
+ impact: `Install ${inspection.packageName} from ${inspection.installSpec}.`,
1167
+ restartExpected: inspection.bundlePatch !== null
1168
+ });
1169
+ }
1170
+ const name = safePackageName(request.target);
1171
+ const installed = this.installed(name);
1172
+ if (request.operation === "update") {
1173
+ const inspection = await this.updateInspection(name, request.source, signal);
1174
+ return this.plans.create({
1175
+ action: "update",
1176
+ profile: "web",
1177
+ packageName: name,
1178
+ currentSource: installed.source,
1179
+ installSpec: inspection.installSpec,
1180
+ impact: `Update ${name} from ${installed.source} to ${inspection.installSpec}.`,
1181
+ restartExpected: true
1182
+ });
1183
+ }
1184
+ if (request.operation === "enable" || request.operation === "disable") {
1185
+ this.assertEnablementAllowed(installed.surface);
1186
+ if (installed.surface.entryIds === null) fail("ENABLEMENT_UNSUPPORTED", `${name} has no safely attributable Loader entries.`);
1187
+ }
1188
+ return this.plans.create({
1189
+ action: request.operation,
1190
+ profile: "web",
1191
+ packageName: name,
1192
+ currentSource: installed.source,
1193
+ impact: request.operation === "disable" ? `Disable ${name}. Conversations currently using capabilities from this plugin may be interrupted; confirm from another backend or session when continuity matters.` : `${request.operation[0].toUpperCase()}${request.operation.slice(1)} ${name}.`,
1194
+ restartExpected: request.operation === "remove"
1195
+ });
1196
+ }
1197
+ execute(confirmationToken) {
1198
+ const plan = this.plans.consume(confirmationToken);
1199
+ const dependencies = readProfileManifest(this.profileDir).dependencies ?? {};
1200
+ if (plan.action === "install" && plan.packageName !== void 0 && dependencies[plan.packageName] !== void 0) fail("PLAN_STALE", `${plan.packageName} was installed after this plan was created; create a new plan.`);
1201
+ if (plan.action !== "install" && plan.action !== "restart" && plan.packageName !== void 0 && dependencies[plan.packageName] !== plan.currentSource) fail("PLAN_STALE", `${plan.packageName} changed after this plan was created; create a new plan.`);
1202
+ const target = plan.packageName ?? "dsh";
1203
+ return this.operations.start(plan.action, target, async (context) => {
1204
+ if (plan.action === "restart") {
1205
+ context.progress("scheduling restart");
1206
+ return {
1207
+ action: "restart",
1208
+ changed: true,
1209
+ restartRequired: false,
1210
+ restart: this.restarter.schedule()
1211
+ };
1212
+ }
1213
+ if (plan.packageName === void 0) fail("POSTCONDITION_FAILED", "Mutation plan has no package name.");
1214
+ if (plan.action === "install" || plan.action === "update") {
1215
+ if (plan.installSpec === void 0) fail("POSTCONDITION_FAILED", "Install/update plan has no immutable source.");
1216
+ return await this.installOrUpdate(plan.action, plan.packageName, plan.installSpec, context);
1217
+ }
1218
+ if (plan.action === "remove") return await this.remove(plan.packageName, context);
1219
+ return await this.toggle(plan.action, plan.packageName, context);
1220
+ });
1221
+ }
1222
+ operation(id) {
1223
+ return this.operations.get(id);
1224
+ }
1225
+ cancel(id) {
1226
+ return this.operations.cancel(id);
1227
+ }
1228
+ wait(id) {
1229
+ return this.operations.wait(id);
1230
+ }
1231
+ async installOrUpdate(action, packageName, installSpec, context) {
1232
+ const before = profileManifestText(this.profileDir);
1233
+ context.progress(`${action}: running official DSH plugin command`);
1234
+ const result = await this.runner.runPlugin("web", [
1235
+ "add",
1236
+ "--save-exact",
1237
+ installSpec
1238
+ ], context.signal, context.progress);
1239
+ if (result.exitCode !== 0 || result.timedOut || result.cancelled) {
1240
+ restoreProfileManifest(this.profileDir, before);
1241
+ fail("DSH_COMMAND_FAILED", `Official DSH plugin command failed with exit code ${result.exitCode}.`, commandResult(result));
1242
+ }
1243
+ const manifest = readProfileManifest(this.profileDir);
1244
+ const dependency = manifest.dependencies?.[packageName];
1245
+ const surface = dependency === void 0 ? null : packageSurface(this.profileDir, packageName, dependency);
1246
+ const installedManifest = installedPackageManifest(this.profileDir, packageName);
1247
+ const bundleCount = manifest.dsh?.profile?.bundles?.filter((name) => name === packageName).length ?? 0;
1248
+ const validSurface = surface !== null && (surface.bundle || surface.client);
1249
+ const validBundleMembership = surface !== null && (surface.bundle ? bundleCount === 1 : bundleCount === 0);
1250
+ const validIdentity = installedManifest?.name === packageName;
1251
+ const validSource = dependency !== void 0 && installedSourceMatches(packageName, dependency, installSpec);
1252
+ const npmSource = parseGithubSpec(installSpec, true) === null ? parseNpmSpec(installSpec, true) : null;
1253
+ const validVersion = npmSource === null || installedManifest?.version === npmSource.version;
1254
+ if (!validSource || !validSurface || !validBundleMembership || !validIdentity || !validVersion) {
1255
+ restoreProfileManifest(this.profileDir, before);
1256
+ fail("POSTCONDITION_FAILED", `Official command completed but ${packageName} did not satisfy profile postconditions.`);
1257
+ }
1258
+ let activation;
1259
+ if (action === "update") activation = {
1260
+ active: false,
1261
+ restartRequired: true,
1262
+ reason: "Updated bundles activate after DSH restart."
1263
+ };
1264
+ else {
1265
+ context.progress("install: attempting restart-free activation");
1266
+ activation = await this.hot.activate(surface);
1267
+ }
1268
+ return {
1269
+ action,
1270
+ packageName,
1271
+ installSpec,
1272
+ changed: true,
1273
+ activated: activation.active,
1274
+ restartRequired: activation.restartRequired,
1275
+ ...activation.reason === null ? {} : { reason: activation.reason },
1276
+ command: commandResult(result)
1277
+ };
1278
+ }
1279
+ async remove(packageName, context) {
1280
+ this.installed(packageName);
1281
+ const before = profileManifestText(this.profileDir);
1282
+ const wasHot = this.hot.isActive(packageName);
1283
+ context.progress("remove: running official DSH plugin command");
1284
+ const result = await this.runner.runPlugin("web", ["remove", packageName], context.signal, context.progress);
1285
+ const packageExists = existsSync(join(this.profileDir, "node_modules", packageName, "package.json"));
1286
+ if (result.exitCode !== 0 || result.timedOut || result.cancelled) {
1287
+ if (!result.cancelled && !packageExists) reconcileRemovedPackage(this.profileDir, packageName);
1288
+ else restoreProfileManifest(this.profileDir, before);
1289
+ if (packageExists || result.cancelled) fail("DSH_COMMAND_FAILED", `Official DSH plugin remove failed with exit code ${result.exitCode}.`, commandResult(result));
1290
+ }
1291
+ const manifest = readProfileManifest(this.profileDir);
1292
+ if (manifest.dependencies?.[packageName] !== void 0 || manifest.dsh?.profile?.bundles?.includes(packageName) === true) fail("POSTCONDITION_FAILED", `${packageName} remains in the profile after removal.`);
1293
+ const deactivated = wasHot ? await this.hot.deactivate(packageName) : false;
1294
+ return {
1295
+ action: "remove",
1296
+ packageName,
1297
+ changed: true,
1298
+ activated: false,
1299
+ restartRequired: !deactivated,
1300
+ ...deactivated ? {} : { reason: "A boot-loaded plugin remains in the current process until DSH restarts." },
1301
+ command: commandResult(result)
1302
+ };
1303
+ }
1304
+ async toggle(action, packageName, context) {
1305
+ const { surface } = this.installed(packageName);
1306
+ this.assertEnablementAllowed(surface);
1307
+ context.progress(`${action}: updating profile patch`);
1308
+ const ids = action === "disable" ? disablePackage(this.profileDir, surface) : enablePackage(this.profileDir, surface);
1309
+ if (action === "disable" && this.hot.isActive(packageName)) await this.hot.deactivate(packageName);
1310
+ if (action === "enable" && !this.loaderEntries().some((entry) => surface.entryIds?.includes(entry.id))) {
1311
+ const activation = await this.hot.activate(surface);
1312
+ return {
1313
+ action,
1314
+ packageName,
1315
+ changed: ids.length > 0,
1316
+ activated: activation.active,
1317
+ restartRequired: activation.restartRequired,
1318
+ ...activation.reason === null ? {} : { reason: activation.reason }
1319
+ };
1320
+ }
1321
+ const expectedDisabled = action === "disable";
1322
+ const deadline = Date.now() + this.hmrTimeoutMs;
1323
+ let verified = false;
1324
+ while (Date.now() < deadline && !context.signal.aborted) {
1325
+ const relevant = this.loaderEntries().filter((entry) => surface.entryIds?.includes(entry.id));
1326
+ if (relevant.length > 0 && relevant.every((entry) => entry.disabled === expectedDisabled)) {
1327
+ verified = true;
1328
+ break;
1329
+ }
1330
+ await new Promise((resolve) => setTimeout(resolve, 50));
1331
+ }
1332
+ return {
1333
+ action,
1334
+ packageName,
1335
+ changed: ids.length > 0,
1336
+ activated: action === "enable" && verified,
1337
+ restartRequired: !verified,
1338
+ ...verified ? {} : { reason: "Loader HMR state could not be verified; restart is required." }
1339
+ };
1340
+ }
1341
+ };
1342
+ //#endregion
1343
+ //#region src/providers.ts
1344
+ const MAX_PROVIDER_RESULTS = 12;
1345
+ function query(value) {
1346
+ const normalized = value.trim();
1347
+ if (normalized === "" || normalized.length > 120 || /[\u0000-\u001f\u007f]/u.test(normalized)) throw new Error("Search query must contain 1 to 120 printable characters.");
1348
+ return normalized;
1349
+ }
1350
+ function npmSearchProvider(fetchImpl = globalThis.fetch) {
1351
+ return {
1352
+ id: "npm",
1353
+ async search(request) {
1354
+ const text = query(request.query);
1355
+ const response = await fetchImpl(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(`${text} keywords:dsh-plugin`)}&size=${Math.min(request.maxResults, MAX_PROVIDER_RESULTS)}`, {
1356
+ signal: request.signal,
1357
+ headers: { accept: "application/json" }
1358
+ });
1359
+ if (!response.ok) throw new Error(`npm search returned HTTP ${response.status}`);
1360
+ const searched = ((await response.json()).objects ?? []).flatMap((entry) => {
1361
+ const name = entry.package?.name;
1362
+ if (typeof name !== "string" || !NPM_NAME.test(name)) return [];
1363
+ return [{
1364
+ id: `npm:${name}`,
1365
+ title: name,
1366
+ ...typeof entry.package?.description === "string" ? { description: entry.package.description } : {},
1367
+ ...typeof entry.package?.links?.homepage === "string" ? { homepage: entry.package.links.homepage } : {},
1368
+ ...typeof entry.package?.links?.repository === "string" ? { repository: entry.package.links.repository } : {},
1369
+ sources: [{
1370
+ kind: "npm",
1371
+ package: name
1372
+ }],
1373
+ ...typeof entry.score?.final === "number" ? { score: entry.score.final } : {}
1374
+ }];
1375
+ });
1376
+ if (!NPM_NAME.test(text) || searched.some((candidate) => candidate.sources.some((source) => source.kind === "npm" && source.package === text))) return searched;
1377
+ return [{
1378
+ id: `npm:${text}`,
1379
+ title: text,
1380
+ sources: [{
1381
+ kind: "npm",
1382
+ package: text
1383
+ }],
1384
+ score: Number.MAX_SAFE_INTEGER,
1385
+ evidence: ["Exact npm package-name query"]
1386
+ }, ...searched];
1387
+ }
1388
+ };
1389
+ }
1390
+ function githubSearchProvider(fetchImpl = globalThis.fetch, env = process.env) {
1391
+ return {
1392
+ id: "github",
1393
+ async search(request) {
1394
+ const text = query(request.query);
1395
+ const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
1396
+ const response = await fetchImpl(`https://api.github.com/search/repositories?q=${encodeURIComponent(`${text} topic:dsh-plugin`)}&per_page=${Math.min(request.maxResults, MAX_PROVIDER_RESULTS)}`, {
1397
+ signal: request.signal,
1398
+ headers: {
1399
+ accept: "application/vnd.github+json",
1400
+ "user-agent": "relay-dsh-plugin-manager",
1401
+ "x-github-api-version": "2022-11-28",
1402
+ ...token === void 0 || token === "" ? {} : { authorization: `Bearer ${token}` }
1403
+ }
1404
+ });
1405
+ if (!response.ok) throw new Error(`GitHub search returned HTTP ${response.status}`);
1406
+ return ((await response.json()).items ?? []).flatMap((entry) => {
1407
+ if (typeof entry.full_name !== "string") return [];
1408
+ const [owner, repo, ...extra] = entry.full_name.split("/");
1409
+ if (owner === void 0 || repo === void 0 || extra.length > 0) return [];
1410
+ return [{
1411
+ id: `github:${entry.id ?? entry.full_name}`,
1412
+ title: entry.full_name,
1413
+ ...typeof entry.description === "string" ? { description: entry.description } : {},
1414
+ ...typeof entry.html_url === "string" ? {
1415
+ homepage: entry.html_url,
1416
+ repository: entry.html_url
1417
+ } : {},
1418
+ sources: [{
1419
+ kind: "github",
1420
+ owner,
1421
+ repo
1422
+ }],
1423
+ ...typeof entry.stargazers_count === "number" ? { score: entry.stargazers_count } : {},
1424
+ evidence: [`GitHub stars: ${String(entry.stargazers_count ?? 0)}`]
1425
+ }];
1426
+ });
1427
+ }
1428
+ };
1429
+ }
1430
+ //#endregion
1431
+ //#region src/restart.ts
1432
+ function detectedSupervisor(env = process.env, ppid = process.ppid) {
1433
+ return ((env.INVOCATION_ID ?? "") !== "" || (env.JOURNAL_STREAM ?? "") !== "") && ppid === 1 ? "systemd" : null;
1434
+ }
1435
+ function restartAllowed(allowRestart, env = process.env, ppid = process.ppid) {
1436
+ if (allowRestart !== void 0) return allowRestart;
1437
+ return detectedSupervisor(env, ppid) === null;
1438
+ }
1439
+ var DshRestarter = class {
1440
+ options;
1441
+ constructor(options = {}) {
1442
+ this.options = options;
1443
+ }
1444
+ available() {
1445
+ return restartAllowed(this.options.allowRestart, this.options.env, this.options.ppid);
1446
+ }
1447
+ schedule() {
1448
+ if (!this.available()) fail("RESTART_UNAVAILABLE", "Automatic restart is disabled or owned by the process supervisor.");
1449
+ const argv = this.options.argv ?? process.argv;
1450
+ if (argv[1] === void 0) fail("RESTART_UNAVAILABLE", "The current DSH entry point cannot be identified.");
1451
+ const execPath = this.options.execPath ?? process.execPath;
1452
+ const cwd = this.options.cwd ?? process.cwd();
1453
+ const env = this.options.env ?? process.env;
1454
+ const logFile = join(tmpdir(), `relay-dsh-plugin-manager-restart-${Date.now()}.log`);
1455
+ const source = [
1456
+ "const { spawn } = require('node:child_process')",
1457
+ "const fs = require('node:fs')",
1458
+ "setTimeout(() => {",
1459
+ ` const out = fs.openSync(${JSON.stringify(logFile)}, 'a')`,
1460
+ ` const child = spawn(${JSON.stringify(execPath)}, ${JSON.stringify(argv.slice(1))}, {`,
1461
+ ` cwd: ${JSON.stringify(cwd)}, env: process.env, detached: true, stdio: ['ignore', out, out]`,
1462
+ " })",
1463
+ " child.on('error', error => fs.appendFileSync(" + JSON.stringify(logFile) + ", String(error) + '\\n'))",
1464
+ " child.unref()",
1465
+ "}, 1200)"
1466
+ ].join("\n");
1467
+ writeFileSync(logFile, "", {
1468
+ flag: "a",
1469
+ mode: 384
1470
+ });
1471
+ const helper = (this.options.spawn ?? spawn)(execPath, ["-e", source], {
1472
+ detached: true,
1473
+ stdio: "ignore",
1474
+ env
1475
+ });
1476
+ helper.unref();
1477
+ setTimeout(this.options.terminate ?? (() => process.kill(process.pid, "SIGTERM")), 500).unref();
1478
+ return {
1479
+ helperPid: helper.pid,
1480
+ logFile
1481
+ };
1482
+ }
1483
+ };
1484
+ //#endregion
1485
+ //#region src/runner.ts
1486
+ function resolveDshLaunch(options = {}) {
1487
+ const env = options.env ?? process.env;
1488
+ const argv = options.argv ?? process.argv;
1489
+ const execPath = options.execPath ?? process.execPath;
1490
+ const cwd = options.cwd ?? process.cwd();
1491
+ const platform = options.platform ?? process.platform;
1492
+ const configured = env.DSH_EXECUTABLE?.trim();
1493
+ let file;
1494
+ let prefix;
1495
+ if (configured !== void 0 && configured !== "") {
1496
+ file = configured;
1497
+ prefix = [];
1498
+ } else if (argv[1] !== void 0 && existsSync(argv[1])) {
1499
+ file = execPath;
1500
+ prefix = [realpathSync(argv[1])];
1501
+ } else {
1502
+ file = "dsh";
1503
+ prefix = [];
1504
+ }
1505
+ return {
1506
+ file,
1507
+ prefix,
1508
+ cwd,
1509
+ shell: platform === "win32" && /\.(?:cmd|bat)$/iu.test(file)
1510
+ };
1511
+ }
1512
+ function boundedAppend(current, chunk, maxBytes) {
1513
+ const combined = current + chunk.toString();
1514
+ return Buffer.byteLength(combined) <= maxBytes ? combined : combined.slice(-maxBytes);
1515
+ }
1516
+ var DshCliRunner = class {
1517
+ options;
1518
+ constructor(options = {}) {
1519
+ this.options = options;
1520
+ }
1521
+ runPlugin(profile, args, signal, progress = () => void 0) {
1522
+ const launch = resolveDshLaunch(this.options);
1523
+ const spawnImpl = this.options.spawn ?? spawn;
1524
+ const timeoutMs = this.options.timeoutMs ?? 5 * 6e4;
1525
+ const maxOutput = this.options.maxOutputBytes ?? 64 * 1024;
1526
+ return new Promise((resolve, reject) => {
1527
+ let child;
1528
+ try {
1529
+ child = spawnImpl(launch.file, [
1530
+ ...launch.prefix,
1531
+ "plugin",
1532
+ "--profile",
1533
+ profile,
1534
+ ...args
1535
+ ], {
1536
+ cwd: launch.cwd,
1537
+ env: this.options.env ?? process.env,
1538
+ shell: launch.shell,
1539
+ stdio: [
1540
+ "ignore",
1541
+ "pipe",
1542
+ "pipe"
1543
+ ]
1544
+ });
1545
+ } catch (error) {
1546
+ reject(error);
1547
+ return;
1548
+ }
1549
+ let stdout = "";
1550
+ let stderr = "";
1551
+ let timedOut = false;
1552
+ let cancelled = false;
1553
+ let settled = false;
1554
+ const terminate = (reason) => {
1555
+ if (reason === "timeout") timedOut = true;
1556
+ else cancelled = true;
1557
+ child.kill("SIGTERM");
1558
+ setTimeout(() => {
1559
+ if (!settled) child.kill("SIGKILL");
1560
+ }, 2e3).unref();
1561
+ };
1562
+ const timer = setTimeout(() => terminate("timeout"), timeoutMs);
1563
+ const onAbort = () => terminate("cancel");
1564
+ if (signal.aborted) onAbort();
1565
+ else signal.addEventListener("abort", onAbort, { once: true });
1566
+ child.stdout?.on("data", (chunk) => {
1567
+ stdout = boundedAppend(stdout, chunk, maxOutput);
1568
+ progress(chunk.toString().trim().slice(-500));
1569
+ });
1570
+ child.stderr?.on("data", (chunk) => {
1571
+ stderr = boundedAppend(stderr, chunk, maxOutput);
1572
+ progress(chunk.toString().trim().slice(-500));
1573
+ });
1574
+ child.once("error", (error) => {
1575
+ clearTimeout(timer);
1576
+ signal.removeEventListener("abort", onAbort);
1577
+ settled = true;
1578
+ reject(error);
1579
+ });
1580
+ child.once("close", (code, closeSignal) => {
1581
+ clearTimeout(timer);
1582
+ signal.removeEventListener("abort", onAbort);
1583
+ settled = true;
1584
+ resolve({
1585
+ exitCode: code ?? 1,
1586
+ signal: closeSignal,
1587
+ stdout,
1588
+ stderr,
1589
+ cancelled,
1590
+ timedOut
1591
+ });
1592
+ });
1593
+ });
1594
+ }
1595
+ };
1596
+ //#endregion
1597
+ //#region src/index.ts
1598
+ const name = "relay-dsh-plugin-manager";
1599
+ const inject = [
1600
+ "pluginSearch",
1601
+ "tools",
1602
+ "commands",
1603
+ "loader"
1604
+ ];
1605
+ function apply(ctx, config = {}) {
1606
+ const profileDir = profileDirectory("web");
1607
+ ctx.pluginSearch.register(npmSearchProvider());
1608
+ ctx.pluginSearch.register(githubSearchProvider());
1609
+ registerConversationSurface(ctx, new PluginManager({
1610
+ profileDir,
1611
+ searchRuntime: ctx.pluginSearch,
1612
+ runner: new DshCliRunner(),
1613
+ hot: new HotRuntime(ctx, profileDir),
1614
+ restarter: new DshRestarter({ allowRestart: config.allowRestart }),
1615
+ loader: ctx.loader
1616
+ }));
1617
+ }
1618
+ //#endregion
1619
+ export { PluginManager, apply, inject, name };
1620
+
1621
+ //# sourceMappingURL=index.js.map