@hachej/boring-ui-plugin-cli 0.1.33 → 0.1.34
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/bin.js +2 -1
- package/dist/chunk-6FD653KO.js +605 -0
- package/dist/{chunk-DH4PSVGY.js → chunk-HY4ZELTX.js} +90 -159
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -1
- package/dist/plugin-sources.d.ts +71 -0
- package/dist/plugin-sources.js +18 -0
- package/package.json +5 -1
package/dist/bin.js
CHANGED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
// src/server/pluginSources.ts
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync
|
|
12
|
+
} from "fs";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
15
|
+
|
|
16
|
+
// src/manifest.ts
|
|
17
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
18
|
+
var PLUGIN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
19
|
+
function isValidBoringPluginId(id) {
|
|
20
|
+
return typeof id === "string" && id.length > 0 && PLUGIN_ID_RE.test(id);
|
|
21
|
+
}
|
|
22
|
+
function isSafePluginRelativePath(value) {
|
|
23
|
+
return typeof value === "string" && value.length > 0 && value !== "." && !value.includes("\0") && !value.includes("\\") && !value.startsWith("/") && !value.startsWith("//") && !/^[A-Za-z]:[\\/]/.test(value) && !value.split("/").includes("..");
|
|
24
|
+
}
|
|
25
|
+
function issue(code, field, message) {
|
|
26
|
+
return { code, field, message };
|
|
27
|
+
}
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function validateStringArray(issues, value, field, pathLike) {
|
|
32
|
+
if (value === void 0) return;
|
|
33
|
+
if (!Array.isArray(value)) {
|
|
34
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be an array`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
value.forEach((entry, index) => {
|
|
38
|
+
const itemField = `${field}[${index}]`;
|
|
39
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
40
|
+
issues.push(issue("INVALID_FIELD", itemField, `${itemField} must be a non-empty string`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (pathLike && !isSafePluginRelativePath(entry)) {
|
|
44
|
+
issues.push(issue("INVALID_PATH", itemField, `${itemField} must be a safe relative path`));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
var REMOVED_BORING_UI_FIELDS = ["outputs", "panels", "commands", "leftTabs", "surfaceResolvers", "providers", "bindings", "catalogs"];
|
|
49
|
+
function validateBoringField(issues, boring) {
|
|
50
|
+
if (boring === void 0) return void 0;
|
|
51
|
+
if (!isRecord(boring)) {
|
|
52
|
+
issues.push(issue("INVALID_FIELD", "boring", "boring must be an object when provided"));
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
for (const field of REMOVED_BORING_UI_FIELDS) {
|
|
56
|
+
if (boring[field] !== void 0) {
|
|
57
|
+
issues.push(issue(
|
|
58
|
+
"INVALID_FIELD",
|
|
59
|
+
`boring.${field}`,
|
|
60
|
+
`boring.${field} is not supported; declare front contributions in boring.front via definePlugin({ ... })`
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
|
|
65
|
+
issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
|
|
66
|
+
}
|
|
67
|
+
const front = boring.front;
|
|
68
|
+
if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
|
|
69
|
+
issues.push(issue("INVALID_PATH", "boring.front", "boring.front must be a safe relative path"));
|
|
70
|
+
}
|
|
71
|
+
const server = boring.server;
|
|
72
|
+
if (server !== void 0 && server !== false && (typeof server !== "string" || !isSafePluginRelativePath(server))) {
|
|
73
|
+
issues.push(issue("INVALID_PATH", "boring.server", "boring.server must be a safe relative path or false"));
|
|
74
|
+
}
|
|
75
|
+
if (boring.label !== void 0 && typeof boring.label !== "string") {
|
|
76
|
+
issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...typeof boring.id === "string" ? { id: boring.id } : {},
|
|
80
|
+
...typeof boring.front === "string" ? { front: boring.front } : {},
|
|
81
|
+
...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
|
|
82
|
+
...typeof boring.label === "string" ? { label: boring.label } : {}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var REMOTE_PI_PACKAGE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
86
|
+
function isRemotePiPackageSource(value) {
|
|
87
|
+
return REMOTE_PI_PACKAGE_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
88
|
+
}
|
|
89
|
+
function isSafePiPackageSource(value) {
|
|
90
|
+
if (value.length === 0) return false;
|
|
91
|
+
if (isRemotePiPackageSource(value)) return true;
|
|
92
|
+
const path = value.startsWith("file:") ? value.slice("file:".length) : value;
|
|
93
|
+
if (path === "." || path === "./") return true;
|
|
94
|
+
const normalized = path.startsWith("./") ? path.slice(2) : path;
|
|
95
|
+
return isSafePluginRelativePath(normalized);
|
|
96
|
+
}
|
|
97
|
+
function validatePiPackages(issues, value) {
|
|
98
|
+
if (value === void 0) return;
|
|
99
|
+
if (!Array.isArray(value)) {
|
|
100
|
+
issues.push(issue("INVALID_FIELD", "pi.packages", "pi.packages must be an array when provided"));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
value.forEach((entry, index) => {
|
|
104
|
+
const field = `pi.packages[${index}]`;
|
|
105
|
+
if (typeof entry === "string") {
|
|
106
|
+
if (!isSafePiPackageSource(entry)) {
|
|
107
|
+
issues.push(issue("INVALID_PATH", field, `${field} must be a safe package source`));
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(entry)) {
|
|
112
|
+
issues.push(issue("INVALID_FIELD", field, `${field} must be a string or package source object`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (typeof entry.source !== "string" || entry.source.length === 0) {
|
|
116
|
+
issues.push(issue("INVALID_FIELD", `${field}.source`, `${field}.source must be a non-empty string`));
|
|
117
|
+
} else if (!isSafePiPackageSource(entry.source)) {
|
|
118
|
+
issues.push(issue("INVALID_PATH", `${field}.source`, `${field}.source must be a safe package source`));
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function validatePiField(issues, pi) {
|
|
123
|
+
if (pi === void 0) return void 0;
|
|
124
|
+
if (!isRecord(pi)) {
|
|
125
|
+
issues.push(issue("INVALID_FIELD", "pi", "pi must be an object when provided"));
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
validateStringArray(issues, pi.extensions, "pi.extensions", true);
|
|
129
|
+
validateStringArray(issues, pi.skills, "pi.skills", true);
|
|
130
|
+
validatePiPackages(issues, pi.packages);
|
|
131
|
+
if (pi.systemPrompt !== void 0 && typeof pi.systemPrompt !== "string") {
|
|
132
|
+
issues.push(issue("INVALID_FIELD", "pi.systemPrompt", "pi.systemPrompt must be a string when provided"));
|
|
133
|
+
}
|
|
134
|
+
return pi;
|
|
135
|
+
}
|
|
136
|
+
function validateBoringPluginManifest(raw) {
|
|
137
|
+
const issues = [];
|
|
138
|
+
if (!isRecord(raw)) {
|
|
139
|
+
return {
|
|
140
|
+
valid: false,
|
|
141
|
+
issues: [issue("INVALID_FIELD", "<root>", "package.json manifest must be an object")]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (raw.name !== void 0 && typeof raw.name !== "string") {
|
|
145
|
+
issues.push(issue("INVALID_FIELD", "name", "name must be a string when provided"));
|
|
146
|
+
}
|
|
147
|
+
if (raw.version !== void 0 && typeof raw.version !== "string") {
|
|
148
|
+
issues.push(issue("INVALID_VERSION", "version", "version must be a string when provided"));
|
|
149
|
+
} else if (typeof raw.version === "string" && raw.version.length > 0 && !SEMVER_RE.test(raw.version)) {
|
|
150
|
+
issues.push(issue("INVALID_VERSION", "version", "version must be a valid semver string"));
|
|
151
|
+
}
|
|
152
|
+
const boring = validateBoringField(issues, raw.boring);
|
|
153
|
+
const pi = validatePiField(issues, raw.pi);
|
|
154
|
+
if (!boring && !pi) {
|
|
155
|
+
issues.push(issue("MISSING_REQUIRED_FIELD", "boring|pi", "package.json must include boring and/or pi plugin metadata"));
|
|
156
|
+
}
|
|
157
|
+
if (issues.length > 0) return { valid: false, issues };
|
|
158
|
+
return {
|
|
159
|
+
valid: true,
|
|
160
|
+
packageJson: {
|
|
161
|
+
...typeof raw.name === "string" ? { name: raw.name } : {},
|
|
162
|
+
...typeof raw.version === "string" ? { version: raw.version } : {},
|
|
163
|
+
...boring ? { boring } : {},
|
|
164
|
+
...pi ? { pi } : {}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/server/pluginSources.ts
|
|
170
|
+
function defaultWorkspaceRoot() {
|
|
171
|
+
return process.env.BORING_AGENT_WORKSPACE_ROOT ?? process.cwd();
|
|
172
|
+
}
|
|
173
|
+
function defaultGlobalRoot() {
|
|
174
|
+
return process.env.BORING_UI_PLUGIN_GLOBAL_ROOT ?? join(homedir(), ".pi", "agent");
|
|
175
|
+
}
|
|
176
|
+
function resolvePluginSourceScopePaths(scope, opts = {}) {
|
|
177
|
+
if (scope === "global") {
|
|
178
|
+
const baseDir2 = resolve(opts.globalRoot ?? defaultGlobalRoot());
|
|
179
|
+
return {
|
|
180
|
+
scope,
|
|
181
|
+
baseDir: baseDir2,
|
|
182
|
+
extensionsDir: join(baseDir2, "extensions"),
|
|
183
|
+
gitDir: join(baseDir2, "git"),
|
|
184
|
+
npmDir: join(baseDir2, "npm"),
|
|
185
|
+
settingsPath: join(baseDir2, "settings.json")
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const workspaceRoot = resolve(opts.workspaceRoot ?? defaultWorkspaceRoot());
|
|
189
|
+
const baseDir = join(workspaceRoot, ".pi");
|
|
190
|
+
return {
|
|
191
|
+
scope,
|
|
192
|
+
workspaceRoot,
|
|
193
|
+
baseDir,
|
|
194
|
+
extensionsDir: join(baseDir, "extensions"),
|
|
195
|
+
gitDir: join(baseDir, "git"),
|
|
196
|
+
npmDir: join(baseDir, "npm"),
|
|
197
|
+
settingsPath: join(baseDir, "settings.json")
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function ensureScopeDirs(paths) {
|
|
201
|
+
mkdirSync(paths.baseDir, { recursive: true });
|
|
202
|
+
mkdirSync(paths.extensionsDir, { recursive: true });
|
|
203
|
+
mkdirSync(paths.gitDir, { recursive: true });
|
|
204
|
+
mkdirSync(paths.npmDir, { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
function readPiSettings(settingsPath) {
|
|
207
|
+
if (!existsSync(settingsPath)) return {};
|
|
208
|
+
let parsed;
|
|
209
|
+
try {
|
|
210
|
+
parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
throw new Error(`invalid Pi settings file ${settingsPath}: ${message}`);
|
|
214
|
+
}
|
|
215
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
216
|
+
throw new Error(`invalid Pi settings file ${settingsPath}: expected object`);
|
|
217
|
+
}
|
|
218
|
+
return parsed;
|
|
219
|
+
}
|
|
220
|
+
function writePiSettings(settingsPath, settings) {
|
|
221
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
222
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
223
|
+
`, "utf8");
|
|
224
|
+
}
|
|
225
|
+
function packageEntries(settings) {
|
|
226
|
+
if (!Array.isArray(settings.packages)) return [];
|
|
227
|
+
return settings.packages.flatMap((entry) => {
|
|
228
|
+
if (typeof entry === "string") return [entry];
|
|
229
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) return [entry];
|
|
230
|
+
return [];
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function packageEntrySource(entry) {
|
|
234
|
+
return typeof entry === "string" ? entry : typeof entry.source === "string" ? entry.source : void 0;
|
|
235
|
+
}
|
|
236
|
+
function resolveMaybePath(value) {
|
|
237
|
+
if (value === "~") return homedir();
|
|
238
|
+
if (value.startsWith("~/")) return resolve(join(homedir(), value.slice(2)));
|
|
239
|
+
if (isAbsolute(value) || value.startsWith(".")) return resolve(value);
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
function resolvePackageSourcePath(settingsDir, source) {
|
|
243
|
+
const path = source.startsWith("file:") ? source.slice("file:".length) : source;
|
|
244
|
+
if (path.startsWith("npm:") || path.startsWith("git:") || path.startsWith("github:") || /^(https?|ssh):\/\//.test(path)) return void 0;
|
|
245
|
+
if (path === "~" || path.startsWith("~/")) return resolveMaybePath(path);
|
|
246
|
+
return isAbsolute(path) ? resolve(path) : resolve(settingsDir, path);
|
|
247
|
+
}
|
|
248
|
+
function pathInside(parent, child) {
|
|
249
|
+
const rel = relative(parent, child);
|
|
250
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
251
|
+
}
|
|
252
|
+
function sourceForLocalPackage(paths, rootDir) {
|
|
253
|
+
if (paths.workspaceRoot && pathInside(paths.workspaceRoot, rootDir)) {
|
|
254
|
+
const rel2 = relative(paths.baseDir, rootDir).split("\\").join("/");
|
|
255
|
+
if (!rel2 || rel2 === ".") return ".";
|
|
256
|
+
return rel2.startsWith(".") ? rel2 : rel2.startsWith("..") ? rel2 : `./${rel2}`;
|
|
257
|
+
}
|
|
258
|
+
const rel = relative(paths.baseDir, rootDir).split("\\").join("/");
|
|
259
|
+
if (!rel || rel === ".") return ".";
|
|
260
|
+
if (!rel.startsWith("..") && !isAbsolute(rel)) return rel.startsWith(".") ? rel : `./${rel}`;
|
|
261
|
+
return rootDir;
|
|
262
|
+
}
|
|
263
|
+
function inferKind(paths, rootDir, source) {
|
|
264
|
+
if (source.startsWith("npm:") || pathInside(paths.npmDir, rootDir)) return "npm";
|
|
265
|
+
if (source.startsWith("git:") || source.startsWith("github:") || /^(https?|ssh):\/\//.test(source) || pathInside(paths.gitDir, rootDir)) return "git";
|
|
266
|
+
return "local";
|
|
267
|
+
}
|
|
268
|
+
function run(command, args, opts = {}) {
|
|
269
|
+
const result = spawnSync(command, args, {
|
|
270
|
+
cwd: opts.cwd,
|
|
271
|
+
encoding: "utf8",
|
|
272
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
273
|
+
});
|
|
274
|
+
if (result.status === 0) return;
|
|
275
|
+
const stderr = result.stderr?.trim();
|
|
276
|
+
const stdout = result.stdout?.trim();
|
|
277
|
+
const details = stderr || stdout ? `: ${stderr || stdout}` : "";
|
|
278
|
+
throw new Error(`${command} ${args.join(" ")} failed${details}`);
|
|
279
|
+
}
|
|
280
|
+
function runWithStdout(command, args, opts = {}) {
|
|
281
|
+
const result = spawnSync(command, args, {
|
|
282
|
+
cwd: opts.cwd,
|
|
283
|
+
encoding: "utf8",
|
|
284
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
285
|
+
});
|
|
286
|
+
if (result.status !== 0) {
|
|
287
|
+
const stderr = result.stderr?.trim();
|
|
288
|
+
const stdout = result.stdout?.trim();
|
|
289
|
+
const details = stderr || stdout ? `: ${stderr || stdout}` : "";
|
|
290
|
+
throw new Error(`${command} ${args.join(" ")} failed${details}`);
|
|
291
|
+
}
|
|
292
|
+
return result.stdout;
|
|
293
|
+
}
|
|
294
|
+
function readPackageJson(pluginRoot) {
|
|
295
|
+
const pkgPath = join(pluginRoot, "package.json");
|
|
296
|
+
if (!existsSync(pkgPath)) throw new Error(`package.json missing in plugin source: ${pluginRoot}`);
|
|
297
|
+
let parsed;
|
|
298
|
+
try {
|
|
299
|
+
parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
302
|
+
throw new Error(`package.json is not valid JSON in ${pluginRoot}: ${message}`);
|
|
303
|
+
}
|
|
304
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
305
|
+
throw new Error(`package.json must be an object in ${pluginRoot}`);
|
|
306
|
+
}
|
|
307
|
+
return parsed;
|
|
308
|
+
}
|
|
309
|
+
function pluginIdFromPackageJson(pkg, rootDir) {
|
|
310
|
+
const boring = pkg.boring && typeof pkg.boring === "object" && !Array.isArray(pkg.boring) ? pkg.boring : void 0;
|
|
311
|
+
const pi = pkg.pi && typeof pkg.pi === "object" && !Array.isArray(pkg.pi) ? pkg.pi : void 0;
|
|
312
|
+
const explicitId = typeof boring?.id === "string" && boring.id.trim() ? boring.id.trim() : typeof pi?.id === "string" && pi.id.trim() ? pi.id.trim() : void 0;
|
|
313
|
+
if (explicitId) return explicitId;
|
|
314
|
+
const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : void 0;
|
|
315
|
+
return ((name ?? basename(rootDir)) || "plugin").replace(/^@/, "").replaceAll("/", "-");
|
|
316
|
+
}
|
|
317
|
+
function validateInstallablePluginRoot(pluginRoot) {
|
|
318
|
+
const resolvedRoot = resolve(pluginRoot);
|
|
319
|
+
const pkg = readPackageJson(resolvedRoot);
|
|
320
|
+
const validation = validateBoringPluginManifest(pkg);
|
|
321
|
+
if (!validation.valid) {
|
|
322
|
+
const issues = validation.issues.map((issue2) => `${issue2.field}: ${issue2.message}`).join("; ");
|
|
323
|
+
throw new Error(`invalid Boring plugin manifest in ${resolvedRoot}: ${issues}`);
|
|
324
|
+
}
|
|
325
|
+
const id = pluginIdFromPackageJson(validation.packageJson, resolvedRoot);
|
|
326
|
+
return {
|
|
327
|
+
id,
|
|
328
|
+
...typeof validation.packageJson.name === "string" ? { packageName: validation.packageJson.name } : {},
|
|
329
|
+
...typeof validation.packageJson.version === "string" ? { version: validation.packageJson.version } : {},
|
|
330
|
+
dependencyHints: dependencyHints(resolvedRoot, pkg)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
var HOST_PROVIDED_DEPENDENCIES = /* @__PURE__ */ new Set(["react", "react-dom", "@hachej/boring-workspace", "@hachej/boring-ui-kit"]);
|
|
334
|
+
function dependencyHints(pluginRoot, pkg) {
|
|
335
|
+
const dependencies = pkg.dependencies;
|
|
336
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) return [];
|
|
337
|
+
const hints = [];
|
|
338
|
+
for (const dep of Object.keys(dependencies)) {
|
|
339
|
+
if (HOST_PROVIDED_DEPENDENCIES.has(dep)) continue;
|
|
340
|
+
const depDir = dep.startsWith("@") ? join(pluginRoot, "node_modules", ...dep.split("/")) : join(pluginRoot, "node_modules", dep);
|
|
341
|
+
if (!existsSync(depDir)) hints.push(`Missing dependency: ${dep}
|
|
342
|
+
Run: cd ${pluginRoot} && npm install`);
|
|
343
|
+
}
|
|
344
|
+
return hints;
|
|
345
|
+
}
|
|
346
|
+
function classifySource(source) {
|
|
347
|
+
if (source.startsWith("npm:")) return { kind: "npm", spec: source.slice("npm:".length), original: source };
|
|
348
|
+
if (source.startsWith("git:")) return { ...normalizeGitSource(source.slice("git:".length)), original: source };
|
|
349
|
+
if (source.startsWith("github:")) return { ...normalizeGitSource(source), original: source };
|
|
350
|
+
if (/^(https?|ssh):\/\//.test(source)) return { kind: "git", spec: source, original: source };
|
|
351
|
+
const maybePath = resolveMaybePath(source);
|
|
352
|
+
if (existsSync(maybePath)) return { kind: "local", spec: maybePath, original: source };
|
|
353
|
+
throw new Error(`unsupported plugin source ${JSON.stringify(source)}. Use a local path, npm:<package>, git:<repo>, github:<owner>/<repo>, or an http(s) git URL.`);
|
|
354
|
+
}
|
|
355
|
+
function normalizeGitSource(raw) {
|
|
356
|
+
let spec = raw;
|
|
357
|
+
let ref;
|
|
358
|
+
const hashIndex = spec.lastIndexOf("#");
|
|
359
|
+
if (hashIndex > 0) {
|
|
360
|
+
ref = spec.slice(hashIndex + 1);
|
|
361
|
+
spec = spec.slice(0, hashIndex);
|
|
362
|
+
} else {
|
|
363
|
+
const slashIndex = spec.lastIndexOf("/");
|
|
364
|
+
const atIndex = spec.lastIndexOf("@");
|
|
365
|
+
if (atIndex > slashIndex && !spec.slice(0, atIndex).includes(":")) {
|
|
366
|
+
ref = spec.slice(atIndex + 1);
|
|
367
|
+
spec = spec.slice(0, atIndex);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (spec.startsWith("github:")) spec = `https://github.com/${spec.slice("github:".length)}`;
|
|
371
|
+
if (spec.startsWith("github.com/")) spec = `https://${spec}`;
|
|
372
|
+
return { kind: "git", spec, ...ref ? { ref } : {} };
|
|
373
|
+
}
|
|
374
|
+
function safeInstallDir(parent, id) {
|
|
375
|
+
const cleaned = id.replace(/[^A-Za-z0-9._:-]+/g, "-");
|
|
376
|
+
if (!cleaned || cleaned === "." || cleaned === "..") throw new Error(`invalid plugin id ${JSON.stringify(id)}`);
|
|
377
|
+
return join(parent, cleaned);
|
|
378
|
+
}
|
|
379
|
+
function moveFreshDir(from, to) {
|
|
380
|
+
if (existsSync(to)) throw new Error(`plugin install target already exists: ${to}. Remove it first with boring-ui-plugin remove ${basename(to)}`);
|
|
381
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
382
|
+
renameSync(from, to);
|
|
383
|
+
}
|
|
384
|
+
function installLocalSource(source, paths) {
|
|
385
|
+
const rootDir = realpathSync(resolveMaybePath(source));
|
|
386
|
+
return { rootDir, packageSource: sourceForLocalPackage(paths, rootDir) };
|
|
387
|
+
}
|
|
388
|
+
function installPluginDependencies(pluginRoot) {
|
|
389
|
+
const pkg = readPackageJson(pluginRoot);
|
|
390
|
+
const declared = pkg.dependencies;
|
|
391
|
+
if (!declared || typeof declared !== "object" || Array.isArray(declared)) return;
|
|
392
|
+
const installable = Object.fromEntries(
|
|
393
|
+
Object.entries(declared).filter(([name]) => !HOST_PROVIDED_DEPENDENCIES.has(name))
|
|
394
|
+
);
|
|
395
|
+
if (Object.keys(installable).length === 0) return;
|
|
396
|
+
if (Object.keys(installable).length !== Object.keys(declared).length) {
|
|
397
|
+
writeFileSync(
|
|
398
|
+
join(pluginRoot, "package.json"),
|
|
399
|
+
`${JSON.stringify({ ...pkg, dependencies: installable }, null, 2)}
|
|
400
|
+
`,
|
|
401
|
+
"utf8"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
run("npm", ["install", "--omit=dev", "--no-audit", "--no-fund", "--legacy-peer-deps"], { cwd: pluginRoot });
|
|
406
|
+
} catch (err) {
|
|
407
|
+
rmSync(pluginRoot, { recursive: true, force: true });
|
|
408
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
409
|
+
throw new Error(`failed to install dependencies for plugin at ${pluginRoot}; install rolled back. ${detail}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function withStagingDir(parent, fn) {
|
|
413
|
+
mkdirSync(parent, { recursive: true });
|
|
414
|
+
const staging = mkdtempSync(join(parent, ".staging-"));
|
|
415
|
+
try {
|
|
416
|
+
return fn(staging);
|
|
417
|
+
} finally {
|
|
418
|
+
rmSync(staging, { recursive: true, force: true });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function installGitSource(spec, paths, ref) {
|
|
422
|
+
return withStagingDir(paths.gitDir, (staging) => {
|
|
423
|
+
const cloneDir = join(staging, "repo");
|
|
424
|
+
run("git", ["clone", "--quiet", spec, cloneDir]);
|
|
425
|
+
if (ref) run("git", ["checkout", "--quiet", ref], { cwd: cloneDir });
|
|
426
|
+
const meta = validateInstallablePluginRoot(cloneDir);
|
|
427
|
+
const target = safeInstallDir(paths.gitDir, meta.id);
|
|
428
|
+
moveFreshDir(cloneDir, target);
|
|
429
|
+
installPluginDependencies(target);
|
|
430
|
+
return { rootDir: target, packageSource: sourceForLocalPackage(paths, target), ...ref ? { ref } : {} };
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function installNpmSource(spec, paths) {
|
|
434
|
+
return withStagingDir(paths.npmDir, (staging) => {
|
|
435
|
+
const packDir = join(staging, "pack");
|
|
436
|
+
const extractDir = join(staging, "extract");
|
|
437
|
+
mkdirSync(packDir, { recursive: true });
|
|
438
|
+
mkdirSync(extractDir, { recursive: true });
|
|
439
|
+
const stdout = runWithStdout("npm", ["pack", "--silent", spec, "--pack-destination", packDir], { cwd: staging });
|
|
440
|
+
const tarballName = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
441
|
+
if (!tarballName) throw new Error(`npm pack did not produce a tarball for ${spec}`);
|
|
442
|
+
const tarball = isAbsolute(tarballName) ? tarballName : join(packDir, tarballName);
|
|
443
|
+
run("tar", ["-xzf", tarball, "-C", extractDir, "--strip-components", "1"]);
|
|
444
|
+
const meta = validateInstallablePluginRoot(extractDir);
|
|
445
|
+
const target = safeInstallDir(paths.npmDir, meta.id);
|
|
446
|
+
moveFreshDir(extractDir, target);
|
|
447
|
+
installPluginDependencies(target);
|
|
448
|
+
return { rootDir: target, packageSource: sourceForLocalPackage(paths, target) };
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function recordFromPackageSource(paths, entry) {
|
|
452
|
+
const packageSource = packageEntrySource(entry);
|
|
453
|
+
if (!packageSource) return null;
|
|
454
|
+
const rootDir = resolvePackageSourcePath(paths.baseDir, packageSource);
|
|
455
|
+
if (!rootDir || !existsSync(join(rootDir, "package.json"))) return null;
|
|
456
|
+
const meta = validateInstallablePluginRoot(rootDir);
|
|
457
|
+
return {
|
|
458
|
+
id: meta.id,
|
|
459
|
+
kind: inferKind(paths, rootDir, packageSource),
|
|
460
|
+
scope: paths.scope,
|
|
461
|
+
packageSource,
|
|
462
|
+
source: rootDir,
|
|
463
|
+
rootDir,
|
|
464
|
+
...meta.packageName ? { packageName: meta.packageName } : {},
|
|
465
|
+
...meta.version ? { version: meta.version } : {}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function readPluginSourceRecords(paths) {
|
|
469
|
+
return packageEntries(readPiSettings(paths.settingsPath)).flatMap((entry) => {
|
|
470
|
+
try {
|
|
471
|
+
const record = recordFromPackageSource(paths, entry);
|
|
472
|
+
return record ? [record] : [];
|
|
473
|
+
} catch {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function readPluginSourceRecordsForRoots(opts) {
|
|
479
|
+
const local = resolvePluginSourceScopePaths("local", opts);
|
|
480
|
+
const global = resolvePluginSourceScopePaths("global", opts);
|
|
481
|
+
return [...readPluginSourceRecords(global), ...readPluginSourceRecords(local)];
|
|
482
|
+
}
|
|
483
|
+
function upsertPackageSource(paths, record) {
|
|
484
|
+
const settings = readPiSettings(paths.settingsPath);
|
|
485
|
+
const entries = packageEntries(settings);
|
|
486
|
+
let replaced = false;
|
|
487
|
+
const nextEntries = [];
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
const existing = recordFromPackageSource(paths, entry);
|
|
490
|
+
const existingSource = packageEntrySource(entry);
|
|
491
|
+
if (existing?.id === record.id || existing?.rootDir === record.rootDir || existingSource === record.packageSource) {
|
|
492
|
+
replaced = true;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
nextEntries.push(entry);
|
|
496
|
+
}
|
|
497
|
+
nextEntries.push(record.packageSource);
|
|
498
|
+
settings.packages = nextEntries;
|
|
499
|
+
writePiSettings(paths.settingsPath, settings);
|
|
500
|
+
return replaced;
|
|
501
|
+
}
|
|
502
|
+
function removePackageSource(paths, target) {
|
|
503
|
+
const settings = readPiSettings(paths.settingsPath);
|
|
504
|
+
const entries = packageEntries(settings);
|
|
505
|
+
const resolvedTarget = resolveMaybePath(target);
|
|
506
|
+
let removed = null;
|
|
507
|
+
const nextEntries = [];
|
|
508
|
+
for (const entry of entries) {
|
|
509
|
+
const packageSource = packageEntrySource(entry);
|
|
510
|
+
let record = null;
|
|
511
|
+
try {
|
|
512
|
+
record = recordFromPackageSource(paths, entry);
|
|
513
|
+
} catch {
|
|
514
|
+
record = null;
|
|
515
|
+
}
|
|
516
|
+
const rootDir = packageSource ? resolvePackageSourcePath(paths.baseDir, packageSource) : void 0;
|
|
517
|
+
const matches = record ? record.id === target || record.packageSource === target || record.source === target || record.rootDir === target || record.source === resolvedTarget || record.rootDir === resolvedTarget : packageSource === target || rootDir === target || rootDir === resolvedTarget;
|
|
518
|
+
if (matches && !removed) {
|
|
519
|
+
removed = record ?? (packageSource ? {
|
|
520
|
+
id: target,
|
|
521
|
+
kind: rootDir ? inferKind(paths, rootDir, packageSource) : "local",
|
|
522
|
+
scope: paths.scope,
|
|
523
|
+
packageSource,
|
|
524
|
+
source: rootDir ?? packageSource,
|
|
525
|
+
rootDir: rootDir ?? packageSource
|
|
526
|
+
} : null);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (matches) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
nextEntries.push(entry);
|
|
533
|
+
}
|
|
534
|
+
if (!removed) return null;
|
|
535
|
+
settings.packages = nextEntries;
|
|
536
|
+
writePiSettings(paths.settingsPath, settings);
|
|
537
|
+
return removed;
|
|
538
|
+
}
|
|
539
|
+
function installPluginSource(opts) {
|
|
540
|
+
const scope = opts.scope ?? "local";
|
|
541
|
+
const paths = resolvePluginSourceScopePaths(scope, opts);
|
|
542
|
+
ensureScopeDirs(paths);
|
|
543
|
+
const classified = classifySource(opts.source);
|
|
544
|
+
const installed = classified.kind === "local" ? installLocalSource(classified.spec, paths) : classified.kind === "git" ? installGitSource(classified.spec, paths, classified.ref) : installNpmSource(classified.spec, paths);
|
|
545
|
+
const meta = validateInstallablePluginRoot(installed.rootDir);
|
|
546
|
+
const record = {
|
|
547
|
+
id: meta.id,
|
|
548
|
+
kind: classified.kind,
|
|
549
|
+
scope,
|
|
550
|
+
packageSource: installed.packageSource,
|
|
551
|
+
source: installed.rootDir,
|
|
552
|
+
rootDir: installed.rootDir,
|
|
553
|
+
...meta.packageName ? { packageName: meta.packageName } : {},
|
|
554
|
+
...meta.version ? { version: meta.version } : {},
|
|
555
|
+
...installed.ref ? { ref: installed.ref } : {}
|
|
556
|
+
};
|
|
557
|
+
const replaced = upsertPackageSource(paths, record);
|
|
558
|
+
return { record, scopePaths: paths, dependencyHints: meta.dependencyHints, replaced };
|
|
559
|
+
}
|
|
560
|
+
function listPluginSources(opts = {}) {
|
|
561
|
+
const scopes = opts.scope === "all" ? [resolvePluginSourceScopePaths("global", opts), resolvePluginSourceScopePaths("local", opts)] : [resolvePluginSourceScopePaths(opts.scope ?? "local", opts)];
|
|
562
|
+
return {
|
|
563
|
+
scopes,
|
|
564
|
+
records: scopes.flatMap((paths) => readPluginSourceRecords(paths))
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function removePluginSource(opts) {
|
|
568
|
+
const scope = opts.scope ?? "local";
|
|
569
|
+
const paths = resolvePluginSourceScopePaths(scope, opts);
|
|
570
|
+
const record = removePackageSource(paths, opts.target);
|
|
571
|
+
if (!record) throw new Error(`plugin source not found in ${scope} scope: ${opts.target}`);
|
|
572
|
+
let removedSourceDir = false;
|
|
573
|
+
if (record.kind === "git" || record.kind === "npm") {
|
|
574
|
+
const expectedRoot = record.kind === "git" ? paths.gitDir : paths.npmDir;
|
|
575
|
+
const resolvedRoot = resolve(record.rootDir);
|
|
576
|
+
if (pathInside(expectedRoot, resolvedRoot)) {
|
|
577
|
+
rmSync(resolvedRoot, { recursive: true, force: true });
|
|
578
|
+
removedSourceDir = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { record, scopePaths: paths, removedSourceDir };
|
|
582
|
+
}
|
|
583
|
+
function formatPluginSourceList(result) {
|
|
584
|
+
if (result.records.length === 0) {
|
|
585
|
+
const scopes = result.scopes.map((scope) => scope.scope).join("+");
|
|
586
|
+
return `No plugins installed in ${scopes} scope.`;
|
|
587
|
+
}
|
|
588
|
+
return result.records.sort((a, b) => a.scope.localeCompare(b.scope) || a.id.localeCompare(b.id)).map((record) => `${record.id}
|
|
589
|
+
scope ${record.scope}
|
|
590
|
+
kind ${record.kind}
|
|
591
|
+
source ${record.packageSource}
|
|
592
|
+
dir ${record.rootDir}`).join("\n");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export {
|
|
596
|
+
isSafePluginRelativePath,
|
|
597
|
+
validateBoringPluginManifest,
|
|
598
|
+
resolvePluginSourceScopePaths,
|
|
599
|
+
readPluginSourceRecords,
|
|
600
|
+
readPluginSourceRecordsForRoots,
|
|
601
|
+
installPluginSource,
|
|
602
|
+
listPluginSources,
|
|
603
|
+
removePluginSource,
|
|
604
|
+
formatPluginSourceList
|
|
605
|
+
};
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatPluginSourceList,
|
|
3
|
+
installPluginSource,
|
|
4
|
+
isSafePluginRelativePath,
|
|
5
|
+
listPluginSources,
|
|
6
|
+
removePluginSource,
|
|
7
|
+
validateBoringPluginManifest
|
|
8
|
+
} from "./chunk-6FD653KO.js";
|
|
9
|
+
|
|
1
10
|
// src/server/index.ts
|
|
2
11
|
import { join as join4, resolve as resolve4 } from "path";
|
|
3
12
|
|
|
@@ -198,161 +207,6 @@ function resolveBundledTemplatesDir() {
|
|
|
198
207
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "fs";
|
|
199
208
|
import { builtinModules, createRequire } from "module";
|
|
200
209
|
import { extname, isAbsolute, join as join3, relative as relative2, resolve as resolve3 } from "path";
|
|
201
|
-
|
|
202
|
-
// src/manifest.ts
|
|
203
|
-
var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
204
|
-
var PLUGIN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
205
|
-
function isValidBoringPluginId(id) {
|
|
206
|
-
return typeof id === "string" && id.length > 0 && PLUGIN_ID_RE.test(id);
|
|
207
|
-
}
|
|
208
|
-
function isSafePluginRelativePath(value) {
|
|
209
|
-
return typeof value === "string" && value.length > 0 && value !== "." && !value.includes("\0") && !value.includes("\\") && !value.startsWith("/") && !value.startsWith("//") && !/^[A-Za-z]:[\\/]/.test(value) && !value.split("/").includes("..");
|
|
210
|
-
}
|
|
211
|
-
function issue(code, field, message) {
|
|
212
|
-
return { code, field, message };
|
|
213
|
-
}
|
|
214
|
-
function isRecord(value) {
|
|
215
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
216
|
-
}
|
|
217
|
-
function validateStringArray(issues, value, field, pathLike) {
|
|
218
|
-
if (value === void 0) return;
|
|
219
|
-
if (!Array.isArray(value)) {
|
|
220
|
-
issues.push(issue("INVALID_FIELD", field, `${field} must be an array`));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
value.forEach((entry, index) => {
|
|
224
|
-
const itemField = `${field}[${index}]`;
|
|
225
|
-
if (typeof entry !== "string" || entry.length === 0) {
|
|
226
|
-
issues.push(issue("INVALID_FIELD", itemField, `${itemField} must be a non-empty string`));
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (pathLike && !isSafePluginRelativePath(entry)) {
|
|
230
|
-
issues.push(issue("INVALID_PATH", itemField, `${itemField} must be a safe relative path`));
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
var REMOVED_BORING_UI_FIELDS = ["outputs", "panels", "commands", "leftTabs", "surfaceResolvers", "providers", "bindings", "catalogs"];
|
|
235
|
-
function validateBoringField(issues, boring) {
|
|
236
|
-
if (boring === void 0) return void 0;
|
|
237
|
-
if (!isRecord(boring)) {
|
|
238
|
-
issues.push(issue("INVALID_FIELD", "boring", "boring must be an object when provided"));
|
|
239
|
-
return void 0;
|
|
240
|
-
}
|
|
241
|
-
for (const field of REMOVED_BORING_UI_FIELDS) {
|
|
242
|
-
if (boring[field] !== void 0) {
|
|
243
|
-
issues.push(issue(
|
|
244
|
-
"INVALID_FIELD",
|
|
245
|
-
`boring.${field}`,
|
|
246
|
-
`boring.${field} is not supported; declare front contributions in boring.front via definePlugin({ ... })`
|
|
247
|
-
));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (boring.id !== void 0 && (typeof boring.id !== "string" || !isValidBoringPluginId(boring.id))) {
|
|
251
|
-
issues.push(issue("INVALID_ID", "boring.id", "boring.id must start with a letter or number and use only letters, numbers, dot, underscore, colon, or dash"));
|
|
252
|
-
}
|
|
253
|
-
const front = boring.front;
|
|
254
|
-
if (front !== void 0 && (typeof front !== "string" || !isSafePluginRelativePath(front))) {
|
|
255
|
-
issues.push(issue("INVALID_PATH", "boring.front", "boring.front must be a safe relative path"));
|
|
256
|
-
}
|
|
257
|
-
const server = boring.server;
|
|
258
|
-
if (server !== void 0 && server !== false && (typeof server !== "string" || !isSafePluginRelativePath(server))) {
|
|
259
|
-
issues.push(issue("INVALID_PATH", "boring.server", "boring.server must be a safe relative path or false"));
|
|
260
|
-
}
|
|
261
|
-
if (boring.label !== void 0 && typeof boring.label !== "string") {
|
|
262
|
-
issues.push(issue("INVALID_FIELD", "boring.label", "boring.label must be a string when provided"));
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
...typeof boring.id === "string" ? { id: boring.id } : {},
|
|
266
|
-
...typeof boring.front === "string" ? { front: boring.front } : {},
|
|
267
|
-
...typeof boring.server === "string" || boring.server === false ? { server: boring.server } : {},
|
|
268
|
-
...typeof boring.label === "string" ? { label: boring.label } : {}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
var REMOTE_PI_PACKAGE_PREFIXES = ["npm:", "git:", "github:", "http:", "https:", "ssh:"];
|
|
272
|
-
function isRemotePiPackageSource(value) {
|
|
273
|
-
return REMOTE_PI_PACKAGE_PREFIXES.some((prefix) => value.startsWith(prefix));
|
|
274
|
-
}
|
|
275
|
-
function isSafePiPackageSource(value) {
|
|
276
|
-
if (value.length === 0) return false;
|
|
277
|
-
if (isRemotePiPackageSource(value)) return true;
|
|
278
|
-
const path = value.startsWith("file:") ? value.slice("file:".length) : value;
|
|
279
|
-
if (path === "." || path === "./") return true;
|
|
280
|
-
const normalized = path.startsWith("./") ? path.slice(2) : path;
|
|
281
|
-
return isSafePluginRelativePath(normalized);
|
|
282
|
-
}
|
|
283
|
-
function validatePiPackages(issues, value) {
|
|
284
|
-
if (value === void 0) return;
|
|
285
|
-
if (!Array.isArray(value)) {
|
|
286
|
-
issues.push(issue("INVALID_FIELD", "pi.packages", "pi.packages must be an array when provided"));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
value.forEach((entry, index) => {
|
|
290
|
-
const field = `pi.packages[${index}]`;
|
|
291
|
-
if (typeof entry === "string") {
|
|
292
|
-
if (!isSafePiPackageSource(entry)) {
|
|
293
|
-
issues.push(issue("INVALID_PATH", field, `${field} must be a safe package source`));
|
|
294
|
-
}
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (!isRecord(entry)) {
|
|
298
|
-
issues.push(issue("INVALID_FIELD", field, `${field} must be a string or package source object`));
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (typeof entry.source !== "string" || entry.source.length === 0) {
|
|
302
|
-
issues.push(issue("INVALID_FIELD", `${field}.source`, `${field}.source must be a non-empty string`));
|
|
303
|
-
} else if (!isSafePiPackageSource(entry.source)) {
|
|
304
|
-
issues.push(issue("INVALID_PATH", `${field}.source`, `${field}.source must be a safe package source`));
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
function validatePiField(issues, pi) {
|
|
309
|
-
if (pi === void 0) return void 0;
|
|
310
|
-
if (!isRecord(pi)) {
|
|
311
|
-
issues.push(issue("INVALID_FIELD", "pi", "pi must be an object when provided"));
|
|
312
|
-
return void 0;
|
|
313
|
-
}
|
|
314
|
-
validateStringArray(issues, pi.extensions, "pi.extensions", true);
|
|
315
|
-
validateStringArray(issues, pi.skills, "pi.skills", true);
|
|
316
|
-
validatePiPackages(issues, pi.packages);
|
|
317
|
-
if (pi.systemPrompt !== void 0 && typeof pi.systemPrompt !== "string") {
|
|
318
|
-
issues.push(issue("INVALID_FIELD", "pi.systemPrompt", "pi.systemPrompt must be a string when provided"));
|
|
319
|
-
}
|
|
320
|
-
return pi;
|
|
321
|
-
}
|
|
322
|
-
function validateBoringPluginManifest(raw) {
|
|
323
|
-
const issues = [];
|
|
324
|
-
if (!isRecord(raw)) {
|
|
325
|
-
return {
|
|
326
|
-
valid: false,
|
|
327
|
-
issues: [issue("INVALID_FIELD", "<root>", "package.json manifest must be an object")]
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
if (raw.name !== void 0 && typeof raw.name !== "string") {
|
|
331
|
-
issues.push(issue("INVALID_FIELD", "name", "name must be a string when provided"));
|
|
332
|
-
}
|
|
333
|
-
if (raw.version !== void 0 && typeof raw.version !== "string") {
|
|
334
|
-
issues.push(issue("INVALID_VERSION", "version", "version must be a string when provided"));
|
|
335
|
-
} else if (typeof raw.version === "string" && raw.version.length > 0 && !SEMVER_RE.test(raw.version)) {
|
|
336
|
-
issues.push(issue("INVALID_VERSION", "version", "version must be a valid semver string"));
|
|
337
|
-
}
|
|
338
|
-
const boring = validateBoringField(issues, raw.boring);
|
|
339
|
-
const pi = validatePiField(issues, raw.pi);
|
|
340
|
-
if (!boring && !pi) {
|
|
341
|
-
issues.push(issue("MISSING_REQUIRED_FIELD", "boring|pi", "package.json must include boring and/or pi plugin metadata"));
|
|
342
|
-
}
|
|
343
|
-
if (issues.length > 0) return { valid: false, issues };
|
|
344
|
-
return {
|
|
345
|
-
valid: true,
|
|
346
|
-
packageJson: {
|
|
347
|
-
...typeof raw.name === "string" ? { name: raw.name } : {},
|
|
348
|
-
...typeof raw.version === "string" ? { version: raw.version } : {},
|
|
349
|
-
...boring ? { boring } : {},
|
|
350
|
-
...pi ? { pi } : {}
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// src/server/verifyPlugin.ts
|
|
356
210
|
function pluginFileSignature(path) {
|
|
357
211
|
if (!path || !existsSync3(path)) return "missing";
|
|
358
212
|
const stat = statSync2(path);
|
|
@@ -577,7 +431,7 @@ function verifySinglePlugin(pluginDir) {
|
|
|
577
431
|
if (isObject(parsed)) validateDependencyManifest(pluginDir, parsed, errors);
|
|
578
432
|
const result = validateBoringPluginManifest(parsed);
|
|
579
433
|
if (!result.valid) {
|
|
580
|
-
for (const
|
|
434
|
+
for (const issue of result.issues) errors.push(formatIssue(issue));
|
|
581
435
|
return { id, dir: pluginDir, ok: false, errors, warnings };
|
|
582
436
|
}
|
|
583
437
|
const manifest = result.packageJson;
|
|
@@ -627,8 +481,8 @@ function verifySinglePlugin(pluginDir) {
|
|
|
627
481
|
}
|
|
628
482
|
return { id, dir: pluginDir, ok: errors.length === 0, errors, warnings };
|
|
629
483
|
}
|
|
630
|
-
function formatIssue(
|
|
631
|
-
return `${
|
|
484
|
+
function formatIssue(issue) {
|
|
485
|
+
return `${issue.code}: ${issue.field}: ${issue.message}`;
|
|
632
486
|
}
|
|
633
487
|
function formatVerifyResult(result) {
|
|
634
488
|
if (result.extensionsDirMissing) {
|
|
@@ -987,7 +841,10 @@ function pluginCommandUsage() {
|
|
|
987
841
|
" boring-ui-plugin create <name> [--path <dir>]",
|
|
988
842
|
" boring-ui-plugin scaffold <name> [workspace]",
|
|
989
843
|
" boring-ui-plugin verify [name] [workspace]",
|
|
990
|
-
" boring-ui-plugin test <name> [--url <url>] [--workspace <id>] [--panel-id <id>] [--timeout-ms <ms>] [--json]"
|
|
844
|
+
" boring-ui-plugin test <name> [--url <url>] [--workspace <id>] [--panel-id <id>] [--timeout-ms <ms>] [--json]",
|
|
845
|
+
" boring-ui-plugin install [-l|--local|--global] [--workspace <dir>] <source>",
|
|
846
|
+
" boring-ui-plugin list [--local|--global|--all] [--workspace <dir>] [--json]",
|
|
847
|
+
" boring-ui-plugin remove [-l|--local|--global] [--workspace <dir>] <id-or-source>"
|
|
991
848
|
].join("\n");
|
|
992
849
|
}
|
|
993
850
|
function handleStatus(json) {
|
|
@@ -1054,6 +911,77 @@ function readOption(argv, name) {
|
|
|
1054
911
|
if (index === -1) return void 0;
|
|
1055
912
|
return argv[index + 1];
|
|
1056
913
|
}
|
|
914
|
+
function pluginSourceScope(argv) {
|
|
915
|
+
if (argv.includes("--global")) return "global";
|
|
916
|
+
return "local";
|
|
917
|
+
}
|
|
918
|
+
function pluginSourceWorkspaceRoot(argv) {
|
|
919
|
+
return readOption(argv, "--workspace");
|
|
920
|
+
}
|
|
921
|
+
function commandPositionals(argv) {
|
|
922
|
+
const out = [];
|
|
923
|
+
for (let i = 0; i < argv.length; i++) {
|
|
924
|
+
const arg = argv[i];
|
|
925
|
+
if (arg === "--workspace") {
|
|
926
|
+
i++;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (arg.startsWith("-")) continue;
|
|
930
|
+
out.push(arg);
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
}
|
|
934
|
+
function handleInstall(argv, json) {
|
|
935
|
+
const source = commandPositionals(argv)[1];
|
|
936
|
+
if (!source) throw new Error("usage: boring-ui-plugin install [--local|--global] [--workspace <dir>] <source>");
|
|
937
|
+
const result = installPluginSource({
|
|
938
|
+
source,
|
|
939
|
+
scope: pluginSourceScope(argv),
|
|
940
|
+
...pluginSourceWorkspaceRoot(argv) ? { workspaceRoot: pluginSourceWorkspaceRoot(argv) } : {}
|
|
941
|
+
});
|
|
942
|
+
if (json) {
|
|
943
|
+
console.log(JSON.stringify(result, null, 2));
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (result.record.kind === "git" || result.record.kind === "npm") {
|
|
947
|
+
console.warn("Security: Boring plugins run as trusted local code in CLI mode. Review third-party source before installing.");
|
|
948
|
+
}
|
|
949
|
+
console.log(`${result.replaced ? "updated" : "installed"} ${result.record.id}`);
|
|
950
|
+
console.log(` scope ${result.record.scope}`);
|
|
951
|
+
console.log(` kind ${result.record.kind}`);
|
|
952
|
+
console.log(` dir ${result.record.rootDir}`);
|
|
953
|
+
if (result.dependencyHints.length > 0) {
|
|
954
|
+
console.log("");
|
|
955
|
+
console.log("Dependencies are not installed by boring-ui-plugin install. Run package-manager commands in the plugin folder:");
|
|
956
|
+
for (const hint of result.dependencyHints) console.log(hint);
|
|
957
|
+
}
|
|
958
|
+
console.log("");
|
|
959
|
+
console.log("Next step: ask the user to run /reload in the workspace UI.");
|
|
960
|
+
}
|
|
961
|
+
function handleList(argv, json) {
|
|
962
|
+
const scope = argv.includes("--all") ? "all" : pluginSourceScope(argv);
|
|
963
|
+
const result = listPluginSources({
|
|
964
|
+
scope,
|
|
965
|
+
...pluginSourceWorkspaceRoot(argv) ? { workspaceRoot: pluginSourceWorkspaceRoot(argv) } : {}
|
|
966
|
+
});
|
|
967
|
+
console.log(json ? JSON.stringify(result, null, 2) : formatPluginSourceList(result));
|
|
968
|
+
}
|
|
969
|
+
function handleRemove(argv, json) {
|
|
970
|
+
const target = commandPositionals(argv)[1];
|
|
971
|
+
if (!target) throw new Error("usage: boring-ui-plugin remove [--local|--global] [--workspace <dir>] <id-or-source>");
|
|
972
|
+
const result = removePluginSource({
|
|
973
|
+
target,
|
|
974
|
+
scope: pluginSourceScope(argv),
|
|
975
|
+
...pluginSourceWorkspaceRoot(argv) ? { workspaceRoot: pluginSourceWorkspaceRoot(argv) } : {}
|
|
976
|
+
});
|
|
977
|
+
if (json) {
|
|
978
|
+
console.log(JSON.stringify(result, null, 2));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
console.log(`removed ${result.record.id}`);
|
|
982
|
+
console.log(` scope ${result.record.scope}`);
|
|
983
|
+
if (result.removedSourceDir) console.log(` deleted ${result.record.rootDir}`);
|
|
984
|
+
}
|
|
1057
985
|
async function handleTest(argv, positionals, json) {
|
|
1058
986
|
const name = positionals[0];
|
|
1059
987
|
if (!name) throw new Error("usage: boring-ui-plugin test <name> [--url <local-server-url>] [--workspace <id>] [--panel-id <id>] [--timeout-ms <ms>] [--json]");
|
|
@@ -1080,6 +1008,9 @@ async function runBoringUiPluginCli(argv = process.argv.slice(2)) {
|
|
|
1080
1008
|
if (command === "scaffold") return handleScaffold(rest);
|
|
1081
1009
|
if (command === "verify") return handleVerify(rest);
|
|
1082
1010
|
if (command === "test") return await handleTest(argv, rest, json);
|
|
1011
|
+
if (command === "install") return handleInstall(argv, json);
|
|
1012
|
+
if (command === "list") return handleList(argv, json);
|
|
1013
|
+
if (command === "remove") return handleRemove(argv, json);
|
|
1083
1014
|
console.log(pluginCommandUsage());
|
|
1084
1015
|
}
|
|
1085
1016
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { InstallPluginSourceOptions, ListPluginSourcesOptions, PluginInstallResult, PluginInstallScope, PluginListResult, PluginRemoveResult, PluginSourceKind, PluginSourceRecord, PluginSourceScopePaths, RemovePluginSourceOptions, formatPluginSourceList, installPluginSource, listPluginSources, readPluginSourceRecords, readPluginSourceRecordsForRoots, removePluginSource, resolvePluginSourceScopePaths } from './plugin-sources.js';
|
|
2
|
+
|
|
1
3
|
interface CreatePluginOptions {
|
|
2
4
|
name: string;
|
|
3
5
|
path?: string;
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,29 @@ import {
|
|
|
8
8
|
runPluginSelfTest,
|
|
9
9
|
scaffoldPlugin,
|
|
10
10
|
verifyPlugin
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-HY4ZELTX.js";
|
|
12
|
+
import {
|
|
13
|
+
formatPluginSourceList,
|
|
14
|
+
installPluginSource,
|
|
15
|
+
listPluginSources,
|
|
16
|
+
readPluginSourceRecords,
|
|
17
|
+
readPluginSourceRecordsForRoots,
|
|
18
|
+
removePluginSource,
|
|
19
|
+
resolvePluginSourceScopePaths
|
|
20
|
+
} from "./chunk-6FD653KO.js";
|
|
12
21
|
export {
|
|
13
22
|
createPlugin,
|
|
14
23
|
findHintForError,
|
|
24
|
+
formatPluginSourceList,
|
|
15
25
|
formatSelfTestResult,
|
|
16
26
|
formatVerifyResult,
|
|
27
|
+
installPluginSource,
|
|
28
|
+
listPluginSources,
|
|
17
29
|
pluginCommandUsage,
|
|
30
|
+
readPluginSourceRecords,
|
|
31
|
+
readPluginSourceRecordsForRoots,
|
|
32
|
+
removePluginSource,
|
|
33
|
+
resolvePluginSourceScopePaths,
|
|
18
34
|
runBoringUiPluginCli,
|
|
19
35
|
runPluginSelfTest,
|
|
20
36
|
scaffoldPlugin,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
type PluginInstallScope = "local" | "global";
|
|
2
|
+
type PluginSourceKind = "local" | "git" | "npm";
|
|
3
|
+
interface PluginSourceRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: PluginSourceKind;
|
|
6
|
+
scope: PluginInstallScope;
|
|
7
|
+
/** Pi package source entry as stored in settings.json. */
|
|
8
|
+
packageSource: string;
|
|
9
|
+
/** Resolved package root when the source is locally inspectable. */
|
|
10
|
+
source: string;
|
|
11
|
+
rootDir: string;
|
|
12
|
+
packageName?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
ref?: string;
|
|
15
|
+
}
|
|
16
|
+
interface PluginSourceScopePaths {
|
|
17
|
+
scope: PluginInstallScope;
|
|
18
|
+
workspaceRoot?: string;
|
|
19
|
+
baseDir: string;
|
|
20
|
+
extensionsDir: string;
|
|
21
|
+
gitDir: string;
|
|
22
|
+
npmDir: string;
|
|
23
|
+
settingsPath: string;
|
|
24
|
+
}
|
|
25
|
+
interface InstallPluginSourceOptions {
|
|
26
|
+
source: string;
|
|
27
|
+
scope?: PluginInstallScope;
|
|
28
|
+
workspaceRoot?: string;
|
|
29
|
+
globalRoot?: string;
|
|
30
|
+
}
|
|
31
|
+
interface RemovePluginSourceOptions {
|
|
32
|
+
target: string;
|
|
33
|
+
scope?: PluginInstallScope;
|
|
34
|
+
workspaceRoot?: string;
|
|
35
|
+
globalRoot?: string;
|
|
36
|
+
}
|
|
37
|
+
interface ListPluginSourcesOptions {
|
|
38
|
+
scope?: PluginInstallScope | "all";
|
|
39
|
+
workspaceRoot?: string;
|
|
40
|
+
globalRoot?: string;
|
|
41
|
+
}
|
|
42
|
+
interface PluginInstallResult {
|
|
43
|
+
record: PluginSourceRecord;
|
|
44
|
+
scopePaths: PluginSourceScopePaths;
|
|
45
|
+
dependencyHints: string[];
|
|
46
|
+
replaced: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface PluginRemoveResult {
|
|
49
|
+
record: PluginSourceRecord;
|
|
50
|
+
scopePaths: PluginSourceScopePaths;
|
|
51
|
+
removedSourceDir: boolean;
|
|
52
|
+
}
|
|
53
|
+
interface PluginListResult {
|
|
54
|
+
records: PluginSourceRecord[];
|
|
55
|
+
scopes: PluginSourceScopePaths[];
|
|
56
|
+
}
|
|
57
|
+
declare function resolvePluginSourceScopePaths(scope: PluginInstallScope, opts?: {
|
|
58
|
+
workspaceRoot?: string;
|
|
59
|
+
globalRoot?: string;
|
|
60
|
+
}): PluginSourceScopePaths;
|
|
61
|
+
declare function readPluginSourceRecords(paths: PluginSourceScopePaths): PluginSourceRecord[];
|
|
62
|
+
declare function readPluginSourceRecordsForRoots(opts: {
|
|
63
|
+
workspaceRoot: string;
|
|
64
|
+
globalRoot?: string;
|
|
65
|
+
}): PluginSourceRecord[];
|
|
66
|
+
declare function installPluginSource(opts: InstallPluginSourceOptions): PluginInstallResult;
|
|
67
|
+
declare function listPluginSources(opts?: ListPluginSourcesOptions): PluginListResult;
|
|
68
|
+
declare function removePluginSource(opts: RemovePluginSourceOptions): PluginRemoveResult;
|
|
69
|
+
declare function formatPluginSourceList(result: PluginListResult): string;
|
|
70
|
+
|
|
71
|
+
export { type InstallPluginSourceOptions, type ListPluginSourcesOptions, type PluginInstallResult, type PluginInstallScope, type PluginListResult, type PluginRemoveResult, type PluginSourceKind, type PluginSourceRecord, type PluginSourceScopePaths, type RemovePluginSourceOptions, formatPluginSourceList, installPluginSource, listPluginSources, readPluginSourceRecords, readPluginSourceRecordsForRoots, removePluginSource, resolvePluginSourceScopePaths };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatPluginSourceList,
|
|
3
|
+
installPluginSource,
|
|
4
|
+
listPluginSources,
|
|
5
|
+
readPluginSourceRecords,
|
|
6
|
+
readPluginSourceRecordsForRoots,
|
|
7
|
+
removePluginSource,
|
|
8
|
+
resolvePluginSourceScopePaths
|
|
9
|
+
} from "./chunk-6FD653KO.js";
|
|
10
|
+
export {
|
|
11
|
+
formatPluginSourceList,
|
|
12
|
+
installPluginSource,
|
|
13
|
+
listPluginSources,
|
|
14
|
+
readPluginSourceRecords,
|
|
15
|
+
readPluginSourceRecordsForRoots,
|
|
16
|
+
removePluginSource,
|
|
17
|
+
resolvePluginSourceScopePaths
|
|
18
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-ui-plugin-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "Slim boring-ui plugin authoring CLI for workspace runtimes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"import": "./dist/index.js",
|
|
17
17
|
"types": "./dist/index.d.ts"
|
|
18
18
|
},
|
|
19
|
+
"./plugin-sources": {
|
|
20
|
+
"import": "./dist/plugin-sources.js",
|
|
21
|
+
"types": "./dist/plugin-sources.d.ts"
|
|
22
|
+
},
|
|
19
23
|
"./package.json": "./package.json"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|