@wrongstack/cli 0.308.5 → 0.308.7

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.
@@ -1,6 +1,7 @@
1
1
  import {
2
- PLUGIN_AUDIT_ENTRIES
3
- } from "./chunk-RK7E3IXT.js";
2
+ PLUGIN_AUDIT_ENTRIES,
3
+ loadExternalPlugins
4
+ } from "./chunk-H6C7SO5H.js";
4
5
  import {
5
6
  patchConfig
6
7
  } from "./chunk-TYO2OVAD.js";
@@ -144,34 +145,23 @@ async function setupPlugins(params) {
144
145
  }
145
146
  }
146
147
  }
147
- const userPlugins = [];
148
- if (config.features?.plugins !== false) {
149
- for (const p of config.plugins ?? []) {
150
- const spec = typeof p === "string" ? p : p.name;
151
- const { enabled } = resolvePluginEnablement({
152
- name: pluginNameFromSpec(spec) ?? spec,
153
- aliases: [spec],
154
- defaultState: "active",
155
- config,
156
- // The resolver already extracts an entry's name before calling this.
157
- matches: (configuredName) => configuredName === spec
158
- });
159
- if (!enabled) continue;
160
- const bareName = pluginNameFromSpec(spec);
161
- if (bareName && warnIfDeprecatedPluginName(bareName, log)) {
162
- continue;
163
- }
164
- if (builtinPluginNameFromSpec(spec)) {
165
- continue;
166
- }
167
- try {
168
- const mod = await import(spec);
169
- if (mod.default) userPlugins.push(mod.default);
170
- } catch (err) {
171
- log.warn(`Plugin "${spec}" failed to load`, err);
172
- }
148
+ const userPlugins = config.features?.plugins === false ? [] : await loadExternalPlugins(
149
+ {
150
+ config,
151
+ log,
152
+ globalRoot: paths?.globalRoot,
153
+ projectRoot: paths?.projectRoot ?? process.cwd(),
154
+ reservedNames: /* @__PURE__ */ new Set([
155
+ ...BUILTIN_PLUGIN_CONFIG_NAMES,
156
+ ...builtinPlugins.map((p) => p.name)
157
+ ])
158
+ },
159
+ {
160
+ nameFromSpec: pluginNameFromSpec,
161
+ isBuiltinSpec: (spec) => builtinPluginNameFromSpec(spec) !== null,
162
+ warnIfDeprecated: (name) => name !== "" && warnIfDeprecatedPluginName(name, log)
173
163
  }
174
- }
164
+ );
175
165
  const allPlugins = [...builtinPlugins, ...userPlugins];
176
166
  if (allPlugins.length === 0) return;
177
167
  const pluginOptions = buildPluginOptions(config, allPlugins);
@@ -203,6 +193,10 @@ async function setupPlugins(params) {
203
193
  const pluginHost = await loadPlugins(allPlugins, {
204
194
  log,
205
195
  pluginOptions,
196
+ // First-party plugins keep the historical warn-only capability checks;
197
+ // external (third-party) plugins are held to their declared
198
+ // capabilities strictly — an undeclared API call rejects the plugin.
199
+ enforceCapabilities: (plugin) => !builtinPlugins.includes(plugin),
206
200
  apiFactory: (plugin) => createApi(plugin.name, {
207
201
  // First-party plugins come from BUILTIN_PLUGIN_FACTORIES — trust them
208
202
  // ("official") so they can claim bare slash command names (/prompts,
@@ -262,4 +256,4 @@ export {
262
256
  BUILTIN_PLUGIN_FACTORIES,
263
257
  setupPlugins
264
258
  };
265
- //# sourceMappingURL=chunk-KV6DNQT6.js.map
259
+ //# sourceMappingURL=chunk-BZJ4MP32.js.map
@@ -1,11 +1,251 @@
1
1
 
2
+ // src/wiring/external-plugins.ts
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import {
6
+ defaultPluginTrustPath,
7
+ discoverExternalPlugins,
8
+ hashFileContents,
9
+ normalizeTrustKey,
10
+ pinPluginTrust,
11
+ readPluginTrustStore,
12
+ resolvePluginEnablement,
13
+ resolvePluginTarget,
14
+ verifyPluginTrust
15
+ } from "@wrongstack/core/plugin";
16
+ function normalizeConfigPath(raw, projectRoot) {
17
+ if (raw.startsWith("file:")) return fileURLToPath(raw);
18
+ return isAbsolute(raw) ? raw : resolve(projectRoot, raw);
19
+ }
20
+ function resolveSpecifierEntry(spec) {
21
+ const metaResolve = import.meta.resolve;
22
+ if (typeof metaResolve !== "function") return void 0;
23
+ try {
24
+ const url = metaResolve(spec);
25
+ return url.startsWith("file:") ? fileURLToPath(url) : void 0;
26
+ } catch {
27
+ return void 0;
28
+ }
29
+ }
30
+ function validateExternalPluginModule(mod) {
31
+ const candidate = mod?.default;
32
+ if (candidate === null || typeof candidate !== "object") {
33
+ return {
34
+ error: `default export missing (got ${candidate === null ? "null" : typeof candidate}) \u2014 external plugin modules must default-export a Plugin object`
35
+ };
36
+ }
37
+ const p = candidate;
38
+ if (typeof p.name !== "string" || p.name.length === 0) {
39
+ return { error: "plugin.name must be a non-empty string" };
40
+ }
41
+ if (typeof p.apiVersion !== "string" || p.apiVersion.length === 0) {
42
+ return { error: 'plugin.apiVersion must be a non-empty string (e.g. "^0.1")' };
43
+ }
44
+ if (typeof p.setup !== "function") {
45
+ return { error: "plugin.setup(api) must be a function" };
46
+ }
47
+ return { plugin: candidate };
48
+ }
49
+ async function loadExternalPlugins(ctx, hooks) {
50
+ const { config, log } = ctx;
51
+ if (config.features?.plugins === false) return [];
52
+ const importModule = ctx.importModule ?? ((url) => import(url));
53
+ const trustEnabled = config.features?.pluginsTrust !== false && (ctx.globalRoot !== void 0 || ctx.trustStorePath !== void 0);
54
+ const trustStorePath = ctx.trustStorePath ?? (ctx.globalRoot !== void 0 ? defaultPluginTrustPath(ctx.globalRoot) : void 0);
55
+ let trustStore;
56
+ if (trustEnabled && trustStorePath !== void 0) {
57
+ try {
58
+ trustStore = await readPluginTrustStore(trustStorePath);
59
+ } catch (err) {
60
+ log.error(`[plugins] ${err instanceof Error ? err.message : String(err)}`);
61
+ return [];
62
+ }
63
+ }
64
+ const sources = [];
65
+ const configNames = /* @__PURE__ */ new Set();
66
+ for (const p of config.plugins ?? []) {
67
+ const spec = typeof p === "string" ? p : p.name;
68
+ const configuredPath = typeof p === "string" || p.path === void 0 ? void 0 : p.path;
69
+ const { enabled } = resolvePluginEnablement({
70
+ name: hooks.nameFromSpec(spec) ?? (typeof p === "string" ? spec : p.name),
71
+ aliases: [spec],
72
+ defaultState: "active",
73
+ config,
74
+ matches: (configuredName) => configuredName === spec
75
+ });
76
+ if (!enabled) continue;
77
+ if (hooks.warnIfDeprecated(hooks.nameFromSpec(spec) ?? "")) continue;
78
+ if (hooks.isBuiltinSpec(spec)) continue;
79
+ if (configuredPath !== void 0) {
80
+ const target = normalizeConfigPath(configuredPath, ctx.projectRoot);
81
+ const entryPath = await resolvePluginTarget(target);
82
+ if (entryPath === null) {
83
+ log.warn(
84
+ `[plugins] config entry "${spec}" path "${configuredPath}" has no resolvable entry (not a file, and no package.json/index fallbacks)`
85
+ );
86
+ continue;
87
+ }
88
+ sources.push({ kind: "path", spec, entryPath, label: spec });
89
+ if (typeof p !== "string") configNames.add(p.name);
90
+ } else {
91
+ sources.push({ kind: "spec", spec, label: spec });
92
+ const bare = hooks.nameFromSpec(spec);
93
+ if (bare) configNames.add(bare);
94
+ }
95
+ }
96
+ const roots = [
97
+ ctx.globalRoot !== void 0 ? { dir: join(ctx.globalRoot, "plugins"), defaultState: "active" } : void 0,
98
+ {
99
+ dir: join(ctx.projectRoot, ".wrongstack", "plugins"),
100
+ // Project-local plugins are opt-in: a cloned repository must not
101
+ // auto-execute plugin code the user never enabled.
102
+ defaultState: "inactive"
103
+ }
104
+ ].filter((r) => r !== void 0);
105
+ const discovery = await discoverExternalPlugins(roots.map((r) => r.dir));
106
+ for (const skipped of discovery.skipped) {
107
+ log.warn(
108
+ `[plugins] discovered candidate "${skipped.name}" in ${skipped.root} skipped \u2014 ${skipped.reason}`
109
+ );
110
+ }
111
+ for (const candidate of discovery.candidates) {
112
+ const root = roots.find((r) => r.dir === candidate.root);
113
+ const defaultState = root?.defaultState ?? "inactive";
114
+ const { enabled, source: enablementSource } = resolvePluginEnablement({
115
+ name: candidate.name,
116
+ aliases: [candidate.name],
117
+ defaultState,
118
+ config,
119
+ matches: (configuredName) => configuredName === candidate.name
120
+ });
121
+ if (!enabled) {
122
+ if (enablementSource !== "default") {
123
+ log.info(
124
+ `[plugins] discovered plugin "${candidate.name}" disabled by ${enablementSource}`
125
+ );
126
+ }
127
+ continue;
128
+ }
129
+ if (configNames.has(candidate.name)) {
130
+ log.info(
131
+ `[plugins] discovered plugin "${candidate.name}" also configured in config.plugins \u2014 config entry wins`
132
+ );
133
+ continue;
134
+ }
135
+ sources.push({
136
+ kind: "discovery",
137
+ spec: candidate.entryPath,
138
+ entryPath: candidate.entryPath,
139
+ label: candidate.name
140
+ });
141
+ }
142
+ const loaded = [];
143
+ const seenNames = /* @__PURE__ */ new Set();
144
+ for (const source of sources) {
145
+ const entryPath = source.kind === "spec" ? resolveSpecifierEntry(source.spec) : source.entryPath;
146
+ if (trustEnabled && trustStorePath !== void 0 && entryPath !== void 0) {
147
+ const gate = await gateExternalPluginTrust({
148
+ entryPath,
149
+ spec: source.spec,
150
+ label: source.label,
151
+ store: trustStore,
152
+ storePath: trustStorePath,
153
+ log
154
+ });
155
+ if (!gate.ok) continue;
156
+ if (gate.pinned) trustStore = gate.store;
157
+ } else if (trustEnabled && trustStorePath !== void 0 && entryPath === void 0) {
158
+ log.warn(
159
+ `[plugins] trust check skipped for "${source.label}" \u2014 entry file could not be resolved pre-import`
160
+ );
161
+ }
162
+ const importTarget = source.kind === "spec" ? source.spec : pathToFileURL(source.entryPath).href;
163
+ let mod;
164
+ try {
165
+ mod = await importModule(importTarget);
166
+ } catch (err) {
167
+ log.warn(`[plugins] external plugin "${source.label}" failed to load`, err);
168
+ continue;
169
+ }
170
+ const validated = validateExternalPluginModule(mod);
171
+ if ("error" in validated) {
172
+ log.warn(`[plugins] external plugin "${source.label}" invalid \u2014 ${validated.error}`);
173
+ continue;
174
+ }
175
+ const plugin = validated.plugin;
176
+ if (ctx.reservedNames.has(plugin.name)) {
177
+ log.warn(
178
+ `[plugins] external plugin "${source.label}" declares reserved name "${plugin.name}" \u2014 built-in plugins own that name; rename the plugin`
179
+ );
180
+ continue;
181
+ }
182
+ if (seenNames.has(plugin.name)) {
183
+ log.warn(
184
+ `[plugins] external plugin "${source.label}" duplicates already-loaded plugin "${plugin.name}" \u2014 first one wins`
185
+ );
186
+ continue;
187
+ }
188
+ seenNames.add(plugin.name);
189
+ loaded.push(plugin);
190
+ }
191
+ return loaded;
192
+ }
193
+ async function gateExternalPluginTrust(args) {
194
+ const { spec, label, store, storePath, log } = args;
195
+ const entryPath = normalizeTrustKey(args.entryPath);
196
+ if (!store) return { ok: true, pinned: false };
197
+ let integrity;
198
+ try {
199
+ integrity = await hashFileContents(entryPath);
200
+ } catch (err) {
201
+ log.warn(
202
+ `[plugins] trust check skipped for "${label}" \u2014 entry could not be read (${err instanceof Error ? err.message : String(err)})`
203
+ );
204
+ return { ok: true, pinned: false };
205
+ }
206
+ const verification = verifyPluginTrust(entryPath, integrity, store);
207
+ if (verification.status === "trusted") return { ok: true, pinned: false };
208
+ if (verification.status === "unpinned") {
209
+ try {
210
+ const next = await pinPluginTrust(storePath, entryPath, entryPath, integrity, spec);
211
+ log.info(
212
+ `[plugins] external plugin "${label}" trusted on first use (pinned ${integrity.slice(0, 19)}\u2026)`
213
+ );
214
+ return { ok: true, pinned: true, store: next };
215
+ } catch (err) {
216
+ log.warn(
217
+ `[plugins] could not persist trust pin for "${label}" (${err instanceof Error ? err.message : String(err)})`
218
+ );
219
+ return { ok: true, pinned: false };
220
+ }
221
+ }
222
+ log.error(
223
+ `[plugins] REFUSING external plugin "${label}" \u2014 its entry file changed since it was first trusted (pinned ${verification.pinnedAt}). If you expected this update, re-pin it: wstack plugin trust ${label}`
224
+ );
225
+ return { ok: false, pinned: false };
226
+ }
227
+
2
228
  // src/plugin-management.ts
229
+ import { execFile } from "node:child_process";
3
230
  import * as fs from "node:fs/promises";
4
- import { resolvePluginEnablement } from "@wrongstack/core/plugin";
231
+ import { homedir } from "node:os";
232
+ import { join as join2 } from "node:path";
233
+ import {
234
+ defaultPluginTrustPath as defaultPluginTrustPath2,
235
+ discoverExternalPlugins as discoverExternalPlugins2,
236
+ hashFileContents as hashFileContents2,
237
+ normalizeTrustKey as normalizeTrustKey2,
238
+ pinPluginTrust as pinPluginTrust2,
239
+ readPluginTrustStore as readPluginTrustStore2,
240
+ resolvePluginEnablement as resolvePluginEnablement2,
241
+ resolvePluginTarget as resolvePluginTarget2,
242
+ unpinPluginTrust
243
+ } from "@wrongstack/core/plugin";
5
244
  import { atomicWrite } from "@wrongstack/core/utils";
6
245
  import {
7
246
  PLUGIN_AUDIT_ENTRIES
8
247
  } from "@wrongstack/plugins/plugin-audit-catalog";
248
+ import { resolveExecInvocation } from "@wrongstack/plugins/runtime";
9
249
  var OFFICIAL_PLUGINS = [
10
250
  {
11
251
  alias: "telegram",
@@ -50,7 +290,12 @@ async function runPluginManagementCommand(args, deps) {
50
290
  if (sub === "add" || sub === "install") {
51
291
  const spec = args[1];
52
292
  if (!spec) {
53
- return errorResult("Usage: wstack plugin add <specifier|official-alias> [--disabled]");
293
+ return errorResult(
294
+ "Usage: wstack plugin add <specifier|official-alias> [--disabled] [--install [--pm npm|pnpm|yarn|bun] [--run-scripts]]"
295
+ );
296
+ }
297
+ if (args.includes("--install") && !OFFICIAL_ALIASES.has(resolvePluginSpecifier(spec))) {
298
+ return installPluginPackage(spec, args, deps);
54
299
  }
55
300
  return upsertPlugin(
56
301
  resolvePluginSpecifier(spec),
@@ -85,6 +330,9 @@ async function runPluginManagementCommand(args, deps) {
85
330
  }
86
331
  return togglePlugin(resolvePluginToggleSpecifier(spec), deps);
87
332
  }
333
+ if (sub === "trust") {
334
+ return runPluginTrustCommand(args.slice(1), deps);
335
+ }
88
336
  if (sub === "manager") {
89
337
  return runPluginManagerPolicyCommand(args.slice(1), deps);
90
338
  }
@@ -93,7 +341,7 @@ async function runPluginManagementCommand(args, deps) {
93
341
  }
94
342
  return errorResult(
95
343
  `Unknown plugin subcommand: ${sub}
96
- Usage: wstack plugin [list|status|report|menu|official|add|install|remove|enable|disable|toggle|manager|llm]`
344
+ Usage: wstack plugin [list|status|report|menu|official|add|install|remove|enable|disable|toggle|trust|manager|llm]`
97
345
  );
98
346
  }
99
347
  var PLUGIN_MANAGER_POLICY_USAGE = [
@@ -303,7 +551,8 @@ function renderConfiguredPlugins(config) {
303
551
  const name = pluginName(p);
304
552
  const enabled = typeof p === "object" && p.enabled === false ? "disabled" : "enabled";
305
553
  const official = OFFICIAL_PLUGINS.find((entry) => entry.specifier === name);
306
- const suffix = official ? ` (${official.alias})` : "";
554
+ const isExternalPath = typeof p === "object" && p.path !== void 0;
555
+ const suffix = official ? ` (${official.alias})` : isExternalPath ? " (external)" : "";
307
556
  return ` ${`${name}${suffix}`.padEnd(44)} ${enabled}`;
308
557
  }).join("\n");
309
558
  }
@@ -332,9 +581,10 @@ function renderPluginAuditReport(config) {
332
581
  for (const plugin of extra) {
333
582
  const name = pluginName(plugin);
334
583
  const state = typeof plugin === "object" && plugin.enabled === false ? "disabled" : "enabled";
335
- lines.push(
336
- ` ${name.padEnd(24)} ${state.padEnd(8)} config risk=custom user-configured plugin`
337
- );
584
+ const path = typeof plugin === "object" && plugin.path !== void 0 ? plugin.path : "";
585
+ const risk = path ? "external" : "custom";
586
+ const note = path ? `user-configured external plugin (${path})` : "user-configured plugin";
587
+ lines.push(` ${name.padEnd(24)} ${state.padEnd(8)} config risk=${risk.padEnd(6)} ${note}`);
338
588
  }
339
589
  }
340
590
  lines.push(
@@ -353,7 +603,8 @@ async function readConfig(file) {
353
603
  function pluginName(p) {
354
604
  return typeof p === "string" ? p : p.name;
355
605
  }
356
- function pluginEntry(spec, enabled) {
606
+ function pluginEntry(spec, enabled, path) {
607
+ if (path !== void 0) return { name: spec, path, enabled };
357
608
  return enabled ? spec : { name: spec, enabled: false };
358
609
  }
359
610
  function pluginMatchesToggleSpec(p, spec) {
@@ -368,7 +619,7 @@ var AUDIT_STATE_SOURCE_LABEL = {
368
619
  default: "default"
369
620
  };
370
621
  function effectiveAuditState(config, entry) {
371
- const { enabled, source } = resolvePluginEnablement({
622
+ const { enabled, source } = resolvePluginEnablement2({
372
623
  name: entry.name,
373
624
  defaultState: entry.defaultState,
374
625
  config,
@@ -392,7 +643,7 @@ function auditEntryFor(spec) {
392
643
  function officialPluginState(config, spec) {
393
644
  const match = (config.plugins ?? []).find((p) => pluginName(p) === spec);
394
645
  if (!match) return "not configured";
395
- const { enabled } = resolvePluginEnablement({
646
+ const { enabled } = resolvePluginEnablement2({
396
647
  name: spec,
397
648
  config,
398
649
  matches: (candidate) => pluginMatchesToggleSpec(candidate, spec)
@@ -409,7 +660,7 @@ async function togglePlugin(spec, deps) {
409
660
  const idx = plugins.findIndex((p) => pluginMatchesToggleSpec(p, spec));
410
661
  const configured = idx >= 0 ? plugins[idx] : void 0;
411
662
  const effectiveConfig = { ...deps.config, ...existing };
412
- const { enabled } = resolvePluginEnablement({
663
+ const { enabled } = resolvePluginEnablement2({
413
664
  name: spec,
414
665
  defaultState: auditEntry?.defaultState,
415
666
  config: effectiveConfig,
@@ -458,7 +709,7 @@ async function upsertPlugin(spec, opts, deps, verb) {
458
709
  const existing = await readConfig(deps.configPath);
459
710
  const plugins = Array.isArray(existing.plugins) ? existing.plugins : [];
460
711
  const idx = plugins.findIndex((p) => pluginName(p) === spec);
461
- const nextEntry = pluginEntry(spec, opts.enabled);
712
+ const nextEntry = pluginEntry(spec, opts.enabled, opts.path);
462
713
  if (idx >= 0) plugins[idx] = nextEntry;
463
714
  else plugins.push(nextEntry);
464
715
  const features = {
@@ -498,12 +749,247 @@ async function removePlugin(spec, deps) {
498
749
  function errorResult(message) {
499
750
  return { code: 1, level: "error", message };
500
751
  }
752
+ function globalPluginsRoot(deps) {
753
+ return join2(deps.globalRoot ?? join2(homedir(), ".wrongstack"), "plugins");
754
+ }
755
+ async function resolveExternalPluginEntry(name, deps) {
756
+ const projectRoot = process.cwd();
757
+ for (const p of deps.config.plugins ?? []) {
758
+ if (typeof p === "object" && p.name === name && p.path !== void 0) {
759
+ const target = normalizeConfigPath(p.path, projectRoot);
760
+ const entry = await resolvePluginTarget2(target);
761
+ if (entry) return entry;
762
+ }
763
+ }
764
+ const { candidates } = await discoverExternalPlugins2([
765
+ globalPluginsRoot(deps),
766
+ join2(projectRoot, ".wrongstack", "plugins")
767
+ ]);
768
+ const discovered = candidates.find((c) => c.name === name);
769
+ if (discovered) return discovered.entryPath;
770
+ for (const p of deps.config.plugins ?? []) {
771
+ const spec = typeof p === "string" ? p : p.name;
772
+ if (spec === name) {
773
+ const resolved = resolveSpecifierEntry(spec);
774
+ if (resolved) return resolved;
775
+ }
776
+ }
777
+ return void 0;
778
+ }
779
+ var PLUGIN_TRUST_USAGE = [
780
+ "Usage:",
781
+ " wstack plugin trust List pinned external plugins.",
782
+ " wstack plugin trust <name> Re-pin an external plugin whose code changed.",
783
+ " wstack plugin trust <name> --remove Drop the pin (plugin re-trusts on next load)."
784
+ ].join("\n");
785
+ async function runPluginTrustCommand(args, deps) {
786
+ const storePath = defaultPluginTrustPath2(join2(deps.globalRoot ?? join2(homedir(), ".wrongstack")));
787
+ const positional = args.filter((a) => !a.startsWith("--"));
788
+ const name = positional[0];
789
+ const remove = args.includes("--remove") || args.includes("--unpin");
790
+ if (args.includes("--list") || name === void 0 && !remove && args.length === 0) {
791
+ const store2 = await readPluginTrustStore2(storePath);
792
+ const entries = Object.entries(store2.pinned);
793
+ if (entries.length === 0) {
794
+ return { code: 0, level: "output", message: "No external plugins pinned yet." };
795
+ }
796
+ return {
797
+ code: 0,
798
+ level: "output",
799
+ message: [
800
+ `Pinned external plugins (${entries.length}):`,
801
+ ...entries.map(
802
+ ([entry2, pin]) => ` ${entry2}
803
+ pinned ${pin.pinnedAt}${pin.spec ? ` from ${pin.spec}` : ""}`
804
+ )
805
+ ].join("\n")
806
+ };
807
+ }
808
+ if (!name) return errorResult(PLUGIN_TRUST_USAGE);
809
+ const store = await readPluginTrustStore2(storePath);
810
+ let entry;
811
+ if (store.pinned[name] !== void 0) {
812
+ entry = name;
813
+ } else if (store.pinned[normalizeTrustKey2(name)] !== void 0) {
814
+ entry = normalizeTrustKey2(name);
815
+ } else {
816
+ entry = await resolveExternalPluginEntry(name, deps);
817
+ }
818
+ if (remove) {
819
+ if (!entry) return errorResult(`No trust pin found for "${name}".`);
820
+ await unpinPluginTrust(storePath, normalizeTrustKey2(entry));
821
+ return { code: 0, level: "info", message: `Removed trust pin for "${entry}".` };
822
+ }
823
+ if (!entry) {
824
+ return errorResult(
825
+ `Could not resolve an external plugin named "${name}" \u2014 check config.plugins entries and the plugins directories.`
826
+ );
827
+ }
828
+ const canonical = normalizeTrustKey2(entry);
829
+ let integrity;
830
+ try {
831
+ integrity = await hashFileContents2(canonical);
832
+ } catch (err) {
833
+ return errorResult(
834
+ `Could not read "${canonical}" (${err instanceof Error ? err.message : String(err)}).`
835
+ );
836
+ }
837
+ await pinPluginTrust2(storePath, canonical, canonical, integrity, name);
838
+ return {
839
+ code: 0,
840
+ level: "info",
841
+ message: `Re-pinned "${name}" \u2192 ${integrity.slice(0, 19)}\u2026 (${canonical}).`
842
+ };
843
+ }
844
+ function detectPackageManager(args) {
845
+ const override = args.find((a) => a.startsWith("--pm="));
846
+ if (override) {
847
+ const pm = override.slice(5);
848
+ if (pm === "npm" || pm === "pnpm" || pm === "yarn" || pm === "bun") return pm;
849
+ }
850
+ const idx = args.indexOf("--pm");
851
+ if (idx >= 0 && args[idx + 1]) {
852
+ const pm = args[idx + 1];
853
+ if (pm === "npm" || pm === "pnpm" || pm === "yarn" || pm === "bun") return pm;
854
+ }
855
+ const ua = process.env.npm_config_user_agent ?? "";
856
+ if (ua.startsWith("pnpm")) return "pnpm";
857
+ if (ua.startsWith("yarn")) return "yarn";
858
+ if (ua.startsWith("bun")) return "bun";
859
+ return "npm";
860
+ }
861
+ function packageNameFromSpec(spec) {
862
+ const stripped = spec.trim();
863
+ if (stripped.startsWith("@")) {
864
+ const scopedEnd = stripped.indexOf("/", 1);
865
+ if (scopedEnd === -1) return stripped.split("@")[0] ?? stripped;
866
+ const rest = stripped.slice(scopedEnd + 1);
867
+ const at2 = rest.lastIndexOf("@");
868
+ return at2 === -1 ? stripped : stripped.slice(0, scopedEnd + 1 + at2);
869
+ }
870
+ const at = stripped.indexOf("@");
871
+ return at === -1 ? stripped : stripped.slice(0, at);
872
+ }
873
+ function buildInstallArgs(pm, targetDir, runScripts, spec) {
874
+ const ignoreScripts = runScripts ? [] : ["--ignore-scripts"];
875
+ switch (pm) {
876
+ case "pnpm":
877
+ return ["add", "--dir", targetDir, ...ignoreScripts, spec];
878
+ case "yarn":
879
+ return ["add", "--cwd", targetDir, ...ignoreScripts, spec];
880
+ case "bun":
881
+ return ["add", "--cwd", targetDir, ...ignoreScripts, spec];
882
+ default:
883
+ return [
884
+ "install",
885
+ "--prefix",
886
+ targetDir,
887
+ "--no-audit",
888
+ "--no-fund",
889
+ ...ignoreScripts,
890
+ spec
891
+ ];
892
+ }
893
+ }
894
+ function runPackageManagerInstall(pm, args, cwd) {
895
+ return new Promise((resolvePromise) => {
896
+ let invocation;
897
+ try {
898
+ invocation = resolveExecInvocation(pm, args);
899
+ } catch (err) {
900
+ resolvePromise({
901
+ code: 127,
902
+ stdout: "",
903
+ stderr: err instanceof Error ? err.message : String(err)
904
+ });
905
+ return;
906
+ }
907
+ execFile(
908
+ invocation.cmd,
909
+ invocation.args,
910
+ {
911
+ cwd,
912
+ timeout: 3e5,
913
+ maxBuffer: 16 * 1024 * 1024,
914
+ windowsHide: true,
915
+ ...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
916
+ },
917
+ (err, stdout, stderr) => {
918
+ const code = err ? err.code ?? 1 : 0;
919
+ resolvePromise({
920
+ code: typeof code === "number" ? code : 1,
921
+ stdout: typeof stdout === "string" ? stdout : "",
922
+ stderr: typeof stderr === "string" ? stderr : ""
923
+ });
924
+ }
925
+ );
926
+ });
927
+ }
928
+ async function installPluginPackage(spec, args, deps) {
929
+ const pm = detectPackageManager(args);
930
+ const runScripts = args.includes("--run-scripts");
931
+ const targetDir = globalPluginsRoot(deps);
932
+ try {
933
+ await fs.mkdir(targetDir, { recursive: true });
934
+ const pkgJsonPath = join2(targetDir, "package.json");
935
+ try {
936
+ await fs.access(pkgJsonPath);
937
+ } catch {
938
+ await fs.writeFile(
939
+ pkgJsonPath,
940
+ `${JSON.stringify({ name: "wrongstack-user-plugins", private: true, version: "0.0.0" }, null, 2)}
941
+ `,
942
+ "utf8"
943
+ );
944
+ }
945
+ } catch (err) {
946
+ return errorResult(
947
+ `Could not prepare plugin directory ${targetDir}: ${err instanceof Error ? err.message : String(err)}`
948
+ );
949
+ }
950
+ const run = deps.runPackageManager ?? runPackageManagerInstall;
951
+ const pmArgs = buildInstallArgs(pm, targetDir, runScripts, spec);
952
+ let result;
953
+ try {
954
+ result = await run(pm, pmArgs, targetDir);
955
+ } catch (err) {
956
+ return errorResult(
957
+ `${pm} failed to start: ${err instanceof Error ? err.message : String(err)}`
958
+ );
959
+ }
960
+ if (result.code !== 0) {
961
+ const tail = (result.stderr || result.stdout).trim().split("\n").slice(-8).join("\n");
962
+ return errorResult(`${pm} ${pmArgs.join(" ")} failed (exit ${result.code}):
963
+ ${tail}`);
964
+ }
965
+ const pkgName = packageNameFromSpec(spec);
966
+ const installPath = join2(targetDir, "node_modules", ...pkgName.split("/"));
967
+ try {
968
+ await fs.access(installPath);
969
+ } catch {
970
+ return errorResult(
971
+ `${pm} reported success but "${installPath}" does not exist \u2014 check the package name "${pkgName}".`
972
+ );
973
+ }
974
+ const upsert = await upsertPlugin(
975
+ pkgName,
976
+ { enabled: !args.includes("--disabled"), path: installPath },
977
+ deps,
978
+ "Installed"
979
+ );
980
+ if (upsert.code !== 0) return upsert;
981
+ return {
982
+ ...upsert,
983
+ message: `${upsert.message} Loaded from ${installPath}. It will be trusted (pinned) on first load.` + (runScripts ? "" : " Install scripts were skipped (default --ignore-scripts; pass --run-scripts if the package needs build steps).")
984
+ };
985
+ }
501
986
  function isRecord(value) {
502
987
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
503
988
  }
504
989
 
505
990
  export {
991
+ loadExternalPlugins,
506
992
  PLUGIN_AUDIT_ENTRIES,
507
993
  runPluginManagementCommand
508
994
  };
509
- //# sourceMappingURL=chunk-RK7E3IXT.js.map
995
+ //# sourceMappingURL=chunk-H6C7SO5H.js.map