@oh-my-pi/pi-coding-agent 1.337.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/CHANGELOG.md +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { extractPackageName, parsePluginSpec } from "./parser.js";
|
|
4
|
+
import {
|
|
5
|
+
getPluginsDir,
|
|
6
|
+
getPluginsLockfile,
|
|
7
|
+
getPluginsNodeModules,
|
|
8
|
+
getPluginsPackageJson,
|
|
9
|
+
getProjectPluginOverrides,
|
|
10
|
+
} from "./paths.js";
|
|
11
|
+
import type {
|
|
12
|
+
DoctorCheck,
|
|
13
|
+
DoctorOptions,
|
|
14
|
+
InstalledPlugin,
|
|
15
|
+
InstallOptions,
|
|
16
|
+
PluginManifest,
|
|
17
|
+
PluginRuntimeConfig,
|
|
18
|
+
PluginSettingSchema,
|
|
19
|
+
ProjectPluginOverrides,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Validation
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/** Valid npm package name pattern (scoped and unscoped, with optional version) */
|
|
27
|
+
const VALID_PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9-._^~>=<]+)?$/i;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate package name to prevent command injection.
|
|
31
|
+
*/
|
|
32
|
+
function validatePackageName(name: string): void {
|
|
33
|
+
// Remove version specifier for validation
|
|
34
|
+
const baseName = extractPackageName(name);
|
|
35
|
+
if (!VALID_PACKAGE_NAME.test(baseName)) {
|
|
36
|
+
throw new Error(`Invalid package name: ${name}`);
|
|
37
|
+
}
|
|
38
|
+
// Extra safety: no shell metacharacters
|
|
39
|
+
if (/[;&|`$(){}[\]<>\\]/.test(name)) {
|
|
40
|
+
throw new Error(`Invalid characters in package name: ${name}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Plugin Manager
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
export class PluginManager {
|
|
49
|
+
private runtimeConfig: PluginRuntimeConfig;
|
|
50
|
+
private cwd: string;
|
|
51
|
+
|
|
52
|
+
constructor(cwd: string = process.cwd()) {
|
|
53
|
+
this.cwd = cwd;
|
|
54
|
+
this.runtimeConfig = this.loadRuntimeConfig();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ==========================================================================
|
|
58
|
+
// Runtime Config Management
|
|
59
|
+
// ==========================================================================
|
|
60
|
+
|
|
61
|
+
private loadRuntimeConfig(): PluginRuntimeConfig {
|
|
62
|
+
const lockPath = getPluginsLockfile();
|
|
63
|
+
if (!existsSync(lockPath)) {
|
|
64
|
+
return { plugins: {}, settings: {} };
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return { plugins: {}, settings: {} };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private saveRuntimeConfig(): void {
|
|
74
|
+
this.ensurePluginsDir();
|
|
75
|
+
writeFileSync(getPluginsLockfile(), JSON.stringify(this.runtimeConfig, null, 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private loadProjectOverrides(): ProjectPluginOverrides {
|
|
79
|
+
const overridesPath = getProjectPluginOverrides(this.cwd);
|
|
80
|
+
if (!existsSync(overridesPath)) {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(readFileSync(overridesPath, "utf-8"));
|
|
85
|
+
} catch {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ==========================================================================
|
|
91
|
+
// Directory Management
|
|
92
|
+
// ==========================================================================
|
|
93
|
+
|
|
94
|
+
private ensurePluginsDir(): void {
|
|
95
|
+
const dir = getPluginsDir();
|
|
96
|
+
if (!existsSync(dir)) {
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
const nodeModules = getPluginsNodeModules();
|
|
100
|
+
if (!existsSync(nodeModules)) {
|
|
101
|
+
mkdirSync(nodeModules, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private ensurePackageJson(): void {
|
|
106
|
+
this.ensurePluginsDir();
|
|
107
|
+
const pkgJsonPath = getPluginsPackageJson();
|
|
108
|
+
if (!existsSync(pkgJsonPath)) {
|
|
109
|
+
writeFileSync(
|
|
110
|
+
pkgJsonPath,
|
|
111
|
+
JSON.stringify(
|
|
112
|
+
{
|
|
113
|
+
name: "pi-plugins",
|
|
114
|
+
private: true,
|
|
115
|
+
dependencies: {},
|
|
116
|
+
},
|
|
117
|
+
null,
|
|
118
|
+
2,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ==========================================================================
|
|
125
|
+
// Install / Uninstall
|
|
126
|
+
// ==========================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Install a plugin from npm with optional feature selection.
|
|
130
|
+
*
|
|
131
|
+
* @param specString - Package specifier with optional features: "pkg", "pkg[feat]", "pkg[*]", "pkg[]"
|
|
132
|
+
* @param options - Install options
|
|
133
|
+
* @returns Installed plugin metadata
|
|
134
|
+
*/
|
|
135
|
+
async install(specString: string, options: InstallOptions = {}): Promise<InstalledPlugin> {
|
|
136
|
+
const spec = parsePluginSpec(specString);
|
|
137
|
+
validatePackageName(spec.packageName);
|
|
138
|
+
|
|
139
|
+
this.ensurePackageJson();
|
|
140
|
+
|
|
141
|
+
if (options.dryRun) {
|
|
142
|
+
return {
|
|
143
|
+
name: spec.packageName,
|
|
144
|
+
version: "0.0.0-dryrun",
|
|
145
|
+
path: "",
|
|
146
|
+
manifest: { version: "0.0.0-dryrun" },
|
|
147
|
+
enabledFeatures: spec.features === "*" ? null : (spec.features as string[] | null),
|
|
148
|
+
enabled: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Run npm install
|
|
153
|
+
const proc = Bun.spawn(["npm", "install", spec.packageName], {
|
|
154
|
+
cwd: getPluginsDir(),
|
|
155
|
+
stdin: "ignore",
|
|
156
|
+
stdout: "pipe",
|
|
157
|
+
stderr: "pipe",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const exitCode = await proc.exited;
|
|
161
|
+
if (exitCode !== 0) {
|
|
162
|
+
const stderr = await new Response(proc.stderr).text();
|
|
163
|
+
throw new Error(`npm install failed: ${stderr}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve actual package name (strip version specifier)
|
|
167
|
+
const actualName = extractPackageName(spec.packageName);
|
|
168
|
+
const pkgPath = join(getPluginsNodeModules(), actualName, "package.json");
|
|
169
|
+
|
|
170
|
+
if (!existsSync(pkgPath)) {
|
|
171
|
+
throw new Error(`Package installed but package.json not found at ${pkgPath}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
175
|
+
const manifest: PluginManifest = pkg.omp || pkg.pi || { version: pkg.version };
|
|
176
|
+
manifest.version = pkg.version;
|
|
177
|
+
|
|
178
|
+
// Resolve enabled features
|
|
179
|
+
let enabledFeatures: string[] | null = null;
|
|
180
|
+
if (spec.features === "*") {
|
|
181
|
+
// All features
|
|
182
|
+
enabledFeatures = manifest.features ? Object.keys(manifest.features) : null;
|
|
183
|
+
} else if (Array.isArray(spec.features)) {
|
|
184
|
+
if (spec.features.length > 0) {
|
|
185
|
+
// Validate requested features exist
|
|
186
|
+
if (manifest.features) {
|
|
187
|
+
for (const feat of spec.features) {
|
|
188
|
+
if (!(feat in manifest.features)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Unknown feature "${feat}" in ${actualName}. Available: ${Object.keys(manifest.features).join(", ")}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
enabledFeatures = spec.features;
|
|
196
|
+
} else {
|
|
197
|
+
// Empty array = no optional features
|
|
198
|
+
enabledFeatures = [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// null = use defaults
|
|
202
|
+
|
|
203
|
+
// Update runtime config
|
|
204
|
+
this.runtimeConfig.plugins[pkg.name] = {
|
|
205
|
+
version: pkg.version,
|
|
206
|
+
enabledFeatures,
|
|
207
|
+
enabled: true,
|
|
208
|
+
};
|
|
209
|
+
this.saveRuntimeConfig();
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
name: pkg.name,
|
|
213
|
+
version: pkg.version,
|
|
214
|
+
path: join(getPluginsNodeModules(), actualName),
|
|
215
|
+
manifest,
|
|
216
|
+
enabledFeatures,
|
|
217
|
+
enabled: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Uninstall a plugin.
|
|
223
|
+
*/
|
|
224
|
+
async uninstall(name: string): Promise<void> {
|
|
225
|
+
validatePackageName(name);
|
|
226
|
+
this.ensurePackageJson();
|
|
227
|
+
|
|
228
|
+
const proc = Bun.spawn(["npm", "uninstall", name], {
|
|
229
|
+
cwd: getPluginsDir(),
|
|
230
|
+
stdin: "ignore",
|
|
231
|
+
stdout: "pipe",
|
|
232
|
+
stderr: "pipe",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const exitCode = await proc.exited;
|
|
236
|
+
if (exitCode !== 0) {
|
|
237
|
+
throw new Error(`npm uninstall failed for ${name}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Remove from runtime config
|
|
241
|
+
delete this.runtimeConfig.plugins[name];
|
|
242
|
+
delete this.runtimeConfig.settings[name];
|
|
243
|
+
this.saveRuntimeConfig();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* List all installed plugins.
|
|
248
|
+
*/
|
|
249
|
+
async list(): Promise<InstalledPlugin[]> {
|
|
250
|
+
const pkgJsonPath = getPluginsPackageJson();
|
|
251
|
+
if (!existsSync(pkgJsonPath)) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
256
|
+
const deps = pkg.dependencies || {};
|
|
257
|
+
const projectOverrides = this.loadProjectOverrides();
|
|
258
|
+
const plugins: InstalledPlugin[] = [];
|
|
259
|
+
|
|
260
|
+
for (const [name] of Object.entries(deps)) {
|
|
261
|
+
const pluginPkgPath = join(getPluginsNodeModules(), name, "package.json");
|
|
262
|
+
if (existsSync(pluginPkgPath)) {
|
|
263
|
+
const pluginPkg = JSON.parse(readFileSync(pluginPkgPath, "utf-8"));
|
|
264
|
+
const manifest: PluginManifest = pluginPkg.omp || pluginPkg.pi || { version: pluginPkg.version };
|
|
265
|
+
manifest.version = pluginPkg.version;
|
|
266
|
+
|
|
267
|
+
const runtimeState = this.runtimeConfig.plugins[name] || {
|
|
268
|
+
version: pluginPkg.version,
|
|
269
|
+
enabledFeatures: null,
|
|
270
|
+
enabled: true,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Apply project overrides
|
|
274
|
+
const isDisabledInProject = projectOverrides.disabled?.includes(name) ?? false;
|
|
275
|
+
const projectFeatures = projectOverrides.features?.[name];
|
|
276
|
+
|
|
277
|
+
plugins.push({
|
|
278
|
+
name,
|
|
279
|
+
version: pluginPkg.version,
|
|
280
|
+
path: join(getPluginsNodeModules(), name),
|
|
281
|
+
manifest,
|
|
282
|
+
enabledFeatures: projectFeatures ?? runtimeState.enabledFeatures,
|
|
283
|
+
enabled: runtimeState.enabled && !isDisabledInProject,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return plugins;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Link a local plugin for development.
|
|
293
|
+
*/
|
|
294
|
+
async link(localPath: string): Promise<InstalledPlugin> {
|
|
295
|
+
const absolutePath = resolve(this.cwd, localPath);
|
|
296
|
+
|
|
297
|
+
const pkgFile = join(absolutePath, "package.json");
|
|
298
|
+
if (!existsSync(pkgFile)) {
|
|
299
|
+
throw new Error(`package.json not found at ${absolutePath}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const pkg = JSON.parse(readFileSync(pkgFile, "utf-8"));
|
|
303
|
+
if (!pkg.name) {
|
|
304
|
+
throw new Error("package.json must have a name field");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.ensurePluginsDir();
|
|
308
|
+
|
|
309
|
+
const linkPath = join(getPluginsNodeModules(), pkg.name);
|
|
310
|
+
|
|
311
|
+
// Handle scoped packages
|
|
312
|
+
if (pkg.name.startsWith("@")) {
|
|
313
|
+
const scopeDir = join(getPluginsNodeModules(), pkg.name.split("/")[0]);
|
|
314
|
+
if (!existsSync(scopeDir)) {
|
|
315
|
+
mkdirSync(scopeDir, { recursive: true });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Remove existing
|
|
320
|
+
try {
|
|
321
|
+
const stat = lstatSync(linkPath);
|
|
322
|
+
if (stat.isSymbolicLink() || stat.isDirectory()) {
|
|
323
|
+
unlinkSync(linkPath);
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Doesn't exist
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
symlinkSync(absolutePath, linkPath);
|
|
330
|
+
|
|
331
|
+
const manifest: PluginManifest = pkg.omp || pkg.pi || { version: pkg.version };
|
|
332
|
+
manifest.version = pkg.version;
|
|
333
|
+
|
|
334
|
+
// Add to runtime config
|
|
335
|
+
this.runtimeConfig.plugins[pkg.name] = {
|
|
336
|
+
version: pkg.version,
|
|
337
|
+
enabledFeatures: null,
|
|
338
|
+
enabled: true,
|
|
339
|
+
};
|
|
340
|
+
this.saveRuntimeConfig();
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
name: pkg.name,
|
|
344
|
+
version: pkg.version,
|
|
345
|
+
path: absolutePath,
|
|
346
|
+
manifest,
|
|
347
|
+
enabledFeatures: null,
|
|
348
|
+
enabled: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ==========================================================================
|
|
353
|
+
// Enable / Disable
|
|
354
|
+
// ==========================================================================
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Enable or disable a plugin globally.
|
|
358
|
+
*/
|
|
359
|
+
async setEnabled(name: string, enabled: boolean): Promise<void> {
|
|
360
|
+
if (!this.runtimeConfig.plugins[name]) {
|
|
361
|
+
throw new Error(`Plugin ${name} not found in runtime config`);
|
|
362
|
+
}
|
|
363
|
+
this.runtimeConfig.plugins[name].enabled = enabled;
|
|
364
|
+
this.saveRuntimeConfig();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ==========================================================================
|
|
368
|
+
// Features
|
|
369
|
+
// ==========================================================================
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get enabled features for a plugin.
|
|
373
|
+
*/
|
|
374
|
+
getEnabledFeatures(name: string): string[] | null {
|
|
375
|
+
return this.runtimeConfig.plugins[name]?.enabledFeatures ?? null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Set enabled features for a plugin.
|
|
380
|
+
*/
|
|
381
|
+
async setEnabledFeatures(name: string, features: string[] | null): Promise<void> {
|
|
382
|
+
if (!this.runtimeConfig.plugins[name]) {
|
|
383
|
+
throw new Error(`Plugin ${name} not found in runtime config`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Validate features if setting specific ones
|
|
387
|
+
if (features && features.length > 0) {
|
|
388
|
+
const plugins = await this.list();
|
|
389
|
+
const plugin = plugins.find((p) => p.name === name);
|
|
390
|
+
if (plugin?.manifest.features) {
|
|
391
|
+
for (const feat of features) {
|
|
392
|
+
if (!(feat in plugin.manifest.features)) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Unknown feature "${feat}" in ${name}. Available: ${Object.keys(plugin.manifest.features).join(", ")}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.runtimeConfig.plugins[name].enabledFeatures = features;
|
|
402
|
+
this.saveRuntimeConfig();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ==========================================================================
|
|
406
|
+
// Settings
|
|
407
|
+
// ==========================================================================
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get all settings for a plugin.
|
|
411
|
+
*/
|
|
412
|
+
getPluginSettings(name: string): Record<string, unknown> {
|
|
413
|
+
const global = this.runtimeConfig.settings[name] || {};
|
|
414
|
+
const projectOverrides = this.loadProjectOverrides();
|
|
415
|
+
const project = projectOverrides.settings?.[name] || {};
|
|
416
|
+
|
|
417
|
+
// Project settings override global
|
|
418
|
+
return { ...global, ...project };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Set a plugin setting value.
|
|
423
|
+
*/
|
|
424
|
+
setPluginSetting(name: string, key: string, value: unknown): void {
|
|
425
|
+
if (!this.runtimeConfig.settings[name]) {
|
|
426
|
+
this.runtimeConfig.settings[name] = {};
|
|
427
|
+
}
|
|
428
|
+
this.runtimeConfig.settings[name][key] = value;
|
|
429
|
+
this.saveRuntimeConfig();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Delete a plugin setting.
|
|
434
|
+
*/
|
|
435
|
+
deletePluginSetting(name: string, key: string): void {
|
|
436
|
+
if (this.runtimeConfig.settings[name]) {
|
|
437
|
+
delete this.runtimeConfig.settings[name][key];
|
|
438
|
+
this.saveRuntimeConfig();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ==========================================================================
|
|
443
|
+
// Doctor
|
|
444
|
+
// ==========================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Run health checks on the plugin system.
|
|
448
|
+
*/
|
|
449
|
+
async doctor(options: DoctorOptions = {}): Promise<DoctorCheck[]> {
|
|
450
|
+
const checks: DoctorCheck[] = [];
|
|
451
|
+
|
|
452
|
+
// Check 1: Plugins directory exists
|
|
453
|
+
const pluginsDir = getPluginsDir();
|
|
454
|
+
checks.push({
|
|
455
|
+
name: "plugins_directory",
|
|
456
|
+
status: existsSync(pluginsDir) ? "ok" : "warning",
|
|
457
|
+
message: existsSync(pluginsDir) ? `Found at ${pluginsDir}` : "Not created yet",
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Check 2: package.json exists
|
|
461
|
+
const pkgJsonPath = getPluginsPackageJson();
|
|
462
|
+
const hasPkgJson = existsSync(pkgJsonPath);
|
|
463
|
+
checks.push({
|
|
464
|
+
name: "package_manifest",
|
|
465
|
+
status: hasPkgJson ? "ok" : "warning",
|
|
466
|
+
message: hasPkgJson ? "Found" : "Not created yet",
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Check 3: node_modules exists
|
|
470
|
+
const nodeModulesPath = getPluginsNodeModules();
|
|
471
|
+
const hasNodeModules = existsSync(nodeModulesPath);
|
|
472
|
+
checks.push({
|
|
473
|
+
name: "node_modules",
|
|
474
|
+
status: hasNodeModules ? "ok" : hasPkgJson ? "error" : "warning",
|
|
475
|
+
message: hasNodeModules ? "Found" : "Missing (run npm install in plugins dir)",
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (!hasPkgJson) {
|
|
479
|
+
return checks;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Check each installed plugin
|
|
483
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
484
|
+
const deps = pkg.dependencies || {};
|
|
485
|
+
|
|
486
|
+
for (const [name] of Object.entries(deps)) {
|
|
487
|
+
const pluginPath = join(nodeModulesPath, name);
|
|
488
|
+
const pluginPkgPath = join(pluginPath, "package.json");
|
|
489
|
+
|
|
490
|
+
if (!existsSync(pluginPath)) {
|
|
491
|
+
const fixed = options.fix ? await this.fixMissingPlugin() : false;
|
|
492
|
+
checks.push({
|
|
493
|
+
name: `plugin:${name}`,
|
|
494
|
+
status: "error",
|
|
495
|
+
message: "Missing from node_modules",
|
|
496
|
+
fixed,
|
|
497
|
+
});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!existsSync(pluginPkgPath)) {
|
|
502
|
+
checks.push({
|
|
503
|
+
name: `plugin:${name}`,
|
|
504
|
+
status: "error",
|
|
505
|
+
message: "Missing package.json",
|
|
506
|
+
});
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const pluginPkg = JSON.parse(readFileSync(pluginPkgPath, "utf-8"));
|
|
511
|
+
const hasManifest = !!(pluginPkg.omp || pluginPkg.pi);
|
|
512
|
+
const manifest: PluginManifest | undefined = pluginPkg.omp || pluginPkg.pi;
|
|
513
|
+
|
|
514
|
+
checks.push({
|
|
515
|
+
name: `plugin:${name}`,
|
|
516
|
+
status: hasManifest ? "ok" : "warning",
|
|
517
|
+
message: hasManifest
|
|
518
|
+
? `v${pluginPkg.version}${pluginPkg.description ? ` - ${pluginPkg.description}` : ""}`
|
|
519
|
+
: `v${pluginPkg.version} - No omp/pi manifest (not a pi plugin)`,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Check tools path exists if specified
|
|
523
|
+
if (manifest?.tools) {
|
|
524
|
+
const toolsPath = join(pluginPath, manifest.tools);
|
|
525
|
+
if (!existsSync(toolsPath)) {
|
|
526
|
+
checks.push({
|
|
527
|
+
name: `plugin:${name}:tools`,
|
|
528
|
+
status: "error",
|
|
529
|
+
message: `Tools entry "${manifest.tools}" not found`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Check hooks path exists if specified
|
|
535
|
+
if (manifest?.hooks) {
|
|
536
|
+
const hooksPath = join(pluginPath, manifest.hooks);
|
|
537
|
+
if (!existsSync(hooksPath)) {
|
|
538
|
+
checks.push({
|
|
539
|
+
name: `plugin:${name}:hooks`,
|
|
540
|
+
status: "error",
|
|
541
|
+
message: `Hooks entry "${manifest.hooks}" not found`,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Check enabled features exist in manifest
|
|
547
|
+
const runtimeState = this.runtimeConfig.plugins[name];
|
|
548
|
+
if (runtimeState?.enabledFeatures && manifest?.features) {
|
|
549
|
+
for (const feat of runtimeState.enabledFeatures) {
|
|
550
|
+
if (!(feat in manifest.features)) {
|
|
551
|
+
const fixed = options.fix ? this.removeInvalidFeature(name, feat) : false;
|
|
552
|
+
checks.push({
|
|
553
|
+
name: `plugin:${name}:feature:${feat}`,
|
|
554
|
+
status: "warning",
|
|
555
|
+
message: `Enabled feature "${feat}" not in manifest`,
|
|
556
|
+
fixed,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check for orphaned runtime config entries
|
|
564
|
+
for (const name of Object.keys(this.runtimeConfig.plugins)) {
|
|
565
|
+
if (!(name in deps)) {
|
|
566
|
+
const fixed = options.fix ? this.removeOrphanedConfig(name) : false;
|
|
567
|
+
checks.push({
|
|
568
|
+
name: `orphan:${name}`,
|
|
569
|
+
status: "warning",
|
|
570
|
+
message: "Plugin in config but not installed",
|
|
571
|
+
fixed,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return checks;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private async fixMissingPlugin(): Promise<boolean> {
|
|
580
|
+
try {
|
|
581
|
+
const proc = Bun.spawn(["npm", "install"], {
|
|
582
|
+
cwd: getPluginsDir(),
|
|
583
|
+
stdin: "ignore",
|
|
584
|
+
stdout: "pipe",
|
|
585
|
+
stderr: "pipe",
|
|
586
|
+
});
|
|
587
|
+
return (await proc.exited) === 0;
|
|
588
|
+
} catch {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private removeInvalidFeature(name: string, feat: string): boolean {
|
|
594
|
+
const state = this.runtimeConfig.plugins[name];
|
|
595
|
+
if (state?.enabledFeatures) {
|
|
596
|
+
state.enabledFeatures = state.enabledFeatures.filter((f) => f !== feat);
|
|
597
|
+
this.saveRuntimeConfig();
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private removeOrphanedConfig(name: string): boolean {
|
|
604
|
+
delete this.runtimeConfig.plugins[name];
|
|
605
|
+
delete this.runtimeConfig.settings[name];
|
|
606
|
+
this.saveRuntimeConfig();
|
|
607
|
+
return true;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// =============================================================================
|
|
612
|
+
// Setting Validation
|
|
613
|
+
// =============================================================================
|
|
614
|
+
|
|
615
|
+
export interface ValidationResult {
|
|
616
|
+
valid: boolean;
|
|
617
|
+
error?: string;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Validate a setting value against its schema.
|
|
622
|
+
*/
|
|
623
|
+
export function validateSetting(value: unknown, schema: PluginSettingSchema): ValidationResult {
|
|
624
|
+
switch (schema.type) {
|
|
625
|
+
case "string":
|
|
626
|
+
if (typeof value !== "string") {
|
|
627
|
+
return { valid: false, error: "Expected string" };
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
|
|
631
|
+
case "number":
|
|
632
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
633
|
+
return { valid: false, error: "Expected number" };
|
|
634
|
+
}
|
|
635
|
+
if (schema.min !== undefined && value < schema.min) {
|
|
636
|
+
return { valid: false, error: `Must be >= ${schema.min}` };
|
|
637
|
+
}
|
|
638
|
+
if (schema.max !== undefined && value > schema.max) {
|
|
639
|
+
return { valid: false, error: `Must be <= ${schema.max}` };
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
|
|
643
|
+
case "boolean":
|
|
644
|
+
if (typeof value !== "boolean") {
|
|
645
|
+
return { valid: false, error: "Expected boolean" };
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
|
|
649
|
+
case "enum":
|
|
650
|
+
if (!schema.values.includes(String(value))) {
|
|
651
|
+
return { valid: false, error: `Must be one of: ${schema.values.join(", ")}` };
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return { valid: true };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Parse a string value according to a setting schema's type.
|
|
661
|
+
*/
|
|
662
|
+
export function parseSettingValue(valueStr: string, schema: PluginSettingSchema): unknown {
|
|
663
|
+
switch (schema.type) {
|
|
664
|
+
case "number":
|
|
665
|
+
return Number(valueStr);
|
|
666
|
+
|
|
667
|
+
case "boolean":
|
|
668
|
+
return valueStr === "true" || valueStr === "yes" || valueStr === "1";
|
|
669
|
+
default:
|
|
670
|
+
return valueStr;
|
|
671
|
+
}
|
|
672
|
+
}
|