@wrongstack/cli 0.308.6 → 0.309.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KV6DNQT6.js → chunk-BZJ4MP32.js} +24 -30
- package/dist/{chunk-RK7E3IXT.js → chunk-H6C7SO5H.js} +499 -13
- package/dist/{cli-main-HCCYX3D5.js → cli-main-RG2PTQ6S.js} +865 -597
- package/dist/{execution-6HIKTAUB.js → execution-TYUXABWI.js} +51 -2
- package/dist/fleet/host-explore-companion.d.ts +43 -0
- package/dist/fleet/host.d.ts +5 -0
- package/dist/index.js +6 -6
- package/dist/plugin-management.d.ts +9 -0
- package/dist/{plugin-usage-PTEETL3J.js → plugin-usage-26LIIW3P.js} +2 -2
- package/dist/{plugins-XRGWC3NJ.js → plugins-EEKHCMBE.js} +3 -3
- package/dist/{providers-models-4FAKM2WO.js → providers-models-D23WUD74.js} +2 -2
- package/dist/slash-commands/effort.d.ts +33 -0
- package/dist/wiring/external-plugins.d.ts +58 -0
- package/dist/wiring/plugins.d.ts +8 -0
- package/package.json +25 -25
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PLUGIN_AUDIT_ENTRIES
|
|
3
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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-
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
-
|
|
336
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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-
|
|
995
|
+
//# sourceMappingURL=chunk-H6C7SO5H.js.map
|