@oh-my-pi/cli 0.3.0 → 0.5.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/README.md +79 -84
- package/dist/cli.js +5025 -1016
- package/dist/commands/config.d.ts +27 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/features.d.ts.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install.d.ts +6 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/link.d.ts +1 -0
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/outdated.d.ts.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/uninstall.d.ts +3 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/why.d.ts.map +1 -1
- package/dist/conflicts.d.ts +7 -2
- package/dist/conflicts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lock.d.ts.map +1 -1
- package/dist/lockfile.d.ts +24 -3
- package/dist/lockfile.d.ts.map +1 -1
- package/dist/manifest.d.ts +12 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/npm.d.ts +11 -0
- package/dist/npm.d.ts.map +1 -1
- package/dist/output.d.ts +51 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/progress.d.ts +78 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/symlinks.d.ts +1 -0
- package/dist/symlinks.d.ts.map +1 -1
- package/package.json +24 -10
- package/.github/icon.png +0 -0
- package/.github/logo.png +0 -0
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/publish.yml +0 -42
- package/biome.json +0 -29
- package/bun.lock +0 -109
- package/plugins/exa/README.md +0 -153
- package/plugins/exa/package.json +0 -56
- package/plugins/exa/tools/exa/company.ts +0 -35
- package/plugins/exa/tools/exa/index.ts +0 -66
- package/plugins/exa/tools/exa/linkedin.ts +0 -35
- package/plugins/exa/tools/exa/researcher.ts +0 -40
- package/plugins/exa/tools/exa/runtime.json +0 -4
- package/plugins/exa/tools/exa/search.ts +0 -46
- package/plugins/exa/tools/exa/shared.ts +0 -230
- package/plugins/exa/tools/exa/websets.ts +0 -62
- package/plugins/metal-theme/README.md +0 -13
- package/plugins/metal-theme/omp.json +0 -8
- package/plugins/metal-theme/package.json +0 -19
- package/plugins/metal-theme/themes/metal.json +0 -79
- package/plugins/subagents/README.md +0 -25
- package/plugins/subagents/agents/explore.md +0 -71
- package/plugins/subagents/agents/planner.md +0 -51
- package/plugins/subagents/agents/reviewer.md +0 -53
- package/plugins/subagents/agents/task.md +0 -46
- package/plugins/subagents/commands/architect-plan.md +0 -9
- package/plugins/subagents/commands/implement-with-critic.md +0 -10
- package/plugins/subagents/commands/implement.md +0 -10
- package/plugins/subagents/omp.json +0 -15
- package/plugins/subagents/package.json +0 -26
- package/plugins/subagents/tools/task/index.ts +0 -1019
- package/plugins/user-prompt/README.md +0 -130
- package/plugins/user-prompt/package.json +0 -19
- package/plugins/user-prompt/tools/user-prompt/index.ts +0 -235
- package/scripts/bump-version.sh +0 -52
- package/scripts/publish.sh +0 -35
- package/src/cli.ts +0 -242
- package/src/commands/config.ts +0 -384
- package/src/commands/create.ts +0 -203
- package/src/commands/doctor.ts +0 -305
- package/src/commands/enable.ts +0 -122
- package/src/commands/env.ts +0 -38
- package/src/commands/features.ts +0 -295
- package/src/commands/info.ts +0 -120
- package/src/commands/init.ts +0 -60
- package/src/commands/install.ts +0 -700
- package/src/commands/link.ts +0 -159
- package/src/commands/list.ts +0 -186
- package/src/commands/outdated.ts +0 -87
- package/src/commands/search.ts +0 -77
- package/src/commands/uninstall.ts +0 -124
- package/src/commands/update.ts +0 -170
- package/src/commands/why.ts +0 -136
- package/src/conflicts.ts +0 -116
- package/src/errors.ts +0 -22
- package/src/index.ts +0 -46
- package/src/lock.ts +0 -46
- package/src/lockfile.ts +0 -132
- package/src/manifest.ts +0 -360
- package/src/npm.ts +0 -206
- package/src/paths.ts +0 -137
- package/src/runtime.ts +0 -116
- package/src/symlinks.ts +0 -455
- package/tsconfig.json +0 -28
package/src/runtime.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { OmpVariable, PluginPackageJson, PluginsJson } from "@omp/manifest";
|
|
2
|
-
import { loadPluginsJson, readPluginPackageJson } from "@omp/manifest";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Collect all variables from a plugin (top-level + enabled features)
|
|
6
|
-
*/
|
|
7
|
-
function collectVariables(
|
|
8
|
-
pkgJson: PluginPackageJson,
|
|
9
|
-
enabledFeatures: string[],
|
|
10
|
-
): Record<string, OmpVariable> {
|
|
11
|
-
const vars: Record<string, OmpVariable> = {};
|
|
12
|
-
|
|
13
|
-
// Top-level variables
|
|
14
|
-
if (pkgJson.omp?.variables) {
|
|
15
|
-
Object.assign(vars, pkgJson.omp.variables);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Variables from enabled features
|
|
19
|
-
if (pkgJson.omp?.features) {
|
|
20
|
-
for (const fname of enabledFeatures) {
|
|
21
|
-
const feature = pkgJson.omp.features[fname];
|
|
22
|
-
if (feature?.variables) {
|
|
23
|
-
Object.assign(vars, feature.variables);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return vars;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolve which features are currently enabled
|
|
33
|
-
*
|
|
34
|
-
* - null/undefined: use plugin defaults (features with default !== false)
|
|
35
|
-
* - ["*"]: explicitly all features
|
|
36
|
-
* - []: no optional features
|
|
37
|
-
* - ["f1", "f2"]: specific features
|
|
38
|
-
*/
|
|
39
|
-
function resolveEnabledFeatures(
|
|
40
|
-
allFeatureNames: string[],
|
|
41
|
-
storedFeatures: string[] | null | undefined,
|
|
42
|
-
pluginFeatures: Record<string, { default?: boolean }>,
|
|
43
|
-
): string[] {
|
|
44
|
-
// Explicit "all features" request
|
|
45
|
-
if (Array.isArray(storedFeatures) && storedFeatures.includes("*")) return allFeatureNames;
|
|
46
|
-
// Explicit feature list (including empty array = no features)
|
|
47
|
-
if (Array.isArray(storedFeatures)) return storedFeatures;
|
|
48
|
-
// null/undefined = use defaults
|
|
49
|
-
return Object.entries(pluginFeatures)
|
|
50
|
-
.filter(([_, f]) => f.default !== false)
|
|
51
|
-
.map(([name]) => name);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Get all environment variables for enabled plugins
|
|
56
|
-
*/
|
|
57
|
-
export async function getPluginEnvVars(global = true): Promise<Record<string, string>> {
|
|
58
|
-
const pluginsJson = await loadPluginsJson(global);
|
|
59
|
-
const env: Record<string, string> = {};
|
|
60
|
-
|
|
61
|
-
for (const pluginName of Object.keys(pluginsJson.plugins)) {
|
|
62
|
-
// Skip disabled plugins
|
|
63
|
-
if (pluginsJson.disabled?.includes(pluginName)) continue;
|
|
64
|
-
|
|
65
|
-
const pkgJson = await readPluginPackageJson(pluginName, global);
|
|
66
|
-
if (!pkgJson?.omp) continue;
|
|
67
|
-
|
|
68
|
-
const config = pluginsJson.config?.[pluginName];
|
|
69
|
-
const allFeatureNames = Object.keys(pkgJson.omp.features || {});
|
|
70
|
-
const enabledFeatures = resolveEnabledFeatures(
|
|
71
|
-
allFeatureNames,
|
|
72
|
-
config?.features,
|
|
73
|
-
pkgJson.omp.features || {},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// Collect variables from top-level and enabled features
|
|
77
|
-
const variables = collectVariables(pkgJson, enabledFeatures);
|
|
78
|
-
|
|
79
|
-
for (const [key, varDef] of Object.entries(variables)) {
|
|
80
|
-
if (varDef.env) {
|
|
81
|
-
const value = config?.variables?.[key] ?? varDef.default;
|
|
82
|
-
if (value !== undefined) {
|
|
83
|
-
env[varDef.env] = String(value);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return env;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Generate shell export statements
|
|
94
|
-
* omp env > ~/.pi/env.sh && source ~/.pi/env.sh
|
|
95
|
-
*/
|
|
96
|
-
export async function generateEnvScript(global = true, shell: "sh" | "fish" = "sh"): Promise<string> {
|
|
97
|
-
const vars = await getPluginEnvVars(global);
|
|
98
|
-
|
|
99
|
-
if (shell === "fish") {
|
|
100
|
-
return Object.entries(vars)
|
|
101
|
-
.map(([k, v]) => `set -gx ${k} ${JSON.stringify(v)}`)
|
|
102
|
-
.join("\n");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// POSIX sh/bash/zsh
|
|
106
|
-
return Object.entries(vars)
|
|
107
|
-
.map(([k, v]) => `export ${k}=${JSON.stringify(v)}`)
|
|
108
|
-
.join("\n");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Get environment variables as a JSON object for programmatic use
|
|
113
|
-
*/
|
|
114
|
-
export async function getEnvJson(global = true): Promise<Record<string, string>> {
|
|
115
|
-
return getPluginEnvVars(global);
|
|
116
|
-
}
|
package/src/symlinks.ts
DELETED
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { copyFile, mkdir, readlink, rm, symlink } from "node:fs/promises";
|
|
3
|
-
import { platform } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import type { OmpFeature, OmpInstallEntry, PluginPackageJson, PluginRuntimeConfig } from "@omp/manifest";
|
|
6
|
-
import { getPluginSourceDir } from "@omp/manifest";
|
|
7
|
-
import { PI_CONFIG_DIR, PROJECT_PI_DIR } from "@omp/paths";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get all install entries from package.json.
|
|
12
|
-
* Features no longer have install entries - all files are always installed.
|
|
13
|
-
*/
|
|
14
|
-
export function getInstallEntries(pkgJson: PluginPackageJson): OmpInstallEntry[] {
|
|
15
|
-
return pkgJson.omp?.install ?? [];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @deprecated Use getInstallEntries instead. Features no longer have install arrays.
|
|
20
|
-
*/
|
|
21
|
-
export function getEnabledInstallEntries(
|
|
22
|
-
pkgJson: PluginPackageJson,
|
|
23
|
-
_enabledFeatures?: string[],
|
|
24
|
-
): OmpInstallEntry[] {
|
|
25
|
-
return getInstallEntries(pkgJson);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get all available feature names from a plugin
|
|
30
|
-
*/
|
|
31
|
-
export function getAllFeatureNames(pkgJson: PluginPackageJson): string[] {
|
|
32
|
-
return Object.keys(pkgJson.omp?.features || {});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Get features that are enabled by default (default !== false)
|
|
37
|
-
*/
|
|
38
|
-
export function getDefaultFeatures(features: Record<string, OmpFeature>): string[] {
|
|
39
|
-
return Object.entries(features)
|
|
40
|
-
.filter(([_, f]) => f.default !== false)
|
|
41
|
-
.map(([name]) => name);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const isWindows = platform() === "win32";
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Format permission-related errors with actionable guidance
|
|
48
|
-
*/
|
|
49
|
-
function formatPermissionError(err: NodeJS.ErrnoException, path: string): string {
|
|
50
|
-
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
51
|
-
return `Permission denied: Cannot write to ${path}. Check directory permissions or run with appropriate privileges.`;
|
|
52
|
-
}
|
|
53
|
-
return err.message;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Validates that a target path stays within the base directory.
|
|
58
|
-
* Prevents path traversal attacks via malicious dest entries like '../../../etc/passwd'.
|
|
59
|
-
*/
|
|
60
|
-
function isPathWithinBase(basePath: string, targetPath: string): boolean {
|
|
61
|
-
const normalizedBase = resolve(basePath);
|
|
62
|
-
const resolvedTarget = resolve(basePath, targetPath);
|
|
63
|
-
// Must start with base path followed by separator (or be exactly the base)
|
|
64
|
-
return resolvedTarget === normalizedBase || resolvedTarget.startsWith(`${normalizedBase}/`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get the base directory for symlink destinations based on scope
|
|
69
|
-
*/
|
|
70
|
-
function getBaseDir(global: boolean): string {
|
|
71
|
-
return global ? PI_CONFIG_DIR : PROJECT_PI_DIR;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface SymlinkResult {
|
|
75
|
-
created: string[];
|
|
76
|
-
errors: string[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface SymlinkRemovalResult {
|
|
80
|
-
removed: string[];
|
|
81
|
-
errors: string[];
|
|
82
|
-
skippedNonSymlinks: string[]; // Files that exist but aren't symlinks
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Create symlinks (or copy files with copy:true) for a plugin's omp.install entries
|
|
87
|
-
* @param skipDestinations - Set of destination paths to skip (e.g., due to conflict resolution)
|
|
88
|
-
* @param enabledFeatures - Features to write into runtime.json (if plugin has one)
|
|
89
|
-
*/
|
|
90
|
-
export async function createPluginSymlinks(
|
|
91
|
-
pluginName: string,
|
|
92
|
-
pkgJson: PluginPackageJson,
|
|
93
|
-
global = true,
|
|
94
|
-
verbose = true,
|
|
95
|
-
skipDestinations?: Set<string>,
|
|
96
|
-
enabledFeatures?: string[],
|
|
97
|
-
): Promise<SymlinkResult> {
|
|
98
|
-
const result: SymlinkResult = { created: [], errors: [] };
|
|
99
|
-
const sourceDir = getPluginSourceDir(pluginName, global);
|
|
100
|
-
|
|
101
|
-
const installEntries = getInstallEntries(pkgJson);
|
|
102
|
-
if (installEntries.length === 0) {
|
|
103
|
-
if (verbose) {
|
|
104
|
-
console.log(chalk.dim(" No omp.install entries found"));
|
|
105
|
-
}
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const baseDir = getBaseDir(global);
|
|
110
|
-
|
|
111
|
-
for (const entry of installEntries) {
|
|
112
|
-
// Skip destinations that the user chose to keep from existing plugins
|
|
113
|
-
if (skipDestinations?.has(entry.dest)) {
|
|
114
|
-
if (verbose) {
|
|
115
|
-
console.log(chalk.dim(` Skipped: ${entry.dest} (conflict resolved to existing plugin)`));
|
|
116
|
-
}
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Validate dest path stays within base directory (prevents path traversal attacks)
|
|
121
|
-
if (!isPathWithinBase(baseDir, entry.dest)) {
|
|
122
|
-
const msg = `Path traversal blocked: ${entry.dest} escapes base directory`;
|
|
123
|
-
result.errors.push(msg);
|
|
124
|
-
if (verbose) {
|
|
125
|
-
console.log(chalk.red(` ✗ ${msg}`));
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
const src = join(sourceDir, entry.src);
|
|
132
|
-
const dest = join(baseDir, entry.dest);
|
|
133
|
-
|
|
134
|
-
// Check if source exists
|
|
135
|
-
if (!existsSync(src)) {
|
|
136
|
-
result.errors.push(`Source not found: ${entry.src}`);
|
|
137
|
-
if (verbose) {
|
|
138
|
-
console.log(chalk.yellow(` ⚠ Source not found: ${entry.src}`));
|
|
139
|
-
}
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Create parent directory
|
|
144
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
145
|
-
|
|
146
|
-
// Handle copy vs symlink
|
|
147
|
-
if (entry.copy) {
|
|
148
|
-
// For copy entries (like runtime.json), copy the file
|
|
149
|
-
// But DON'T overwrite if it already exists (preserves user edits)
|
|
150
|
-
if (!existsSync(dest)) {
|
|
151
|
-
await copyFile(src, dest);
|
|
152
|
-
result.created.push(entry.dest);
|
|
153
|
-
if (verbose) {
|
|
154
|
-
console.log(chalk.dim(` Copied: ${entry.dest} (from ${entry.src})`));
|
|
155
|
-
}
|
|
156
|
-
} else {
|
|
157
|
-
if (verbose) {
|
|
158
|
-
console.log(chalk.dim(` Exists: ${entry.dest} (preserved)`));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
// Remove existing symlink/file if it exists
|
|
163
|
-
try {
|
|
164
|
-
await rm(dest, { force: true, recursive: true });
|
|
165
|
-
} catch {}
|
|
166
|
-
|
|
167
|
-
// Create symlink (use junctions on Windows for directories to avoid admin requirement)
|
|
168
|
-
try {
|
|
169
|
-
if (isWindows) {
|
|
170
|
-
const stats = lstatSync(src);
|
|
171
|
-
if (stats.isDirectory()) {
|
|
172
|
-
await symlink(src, dest, "junction");
|
|
173
|
-
} else {
|
|
174
|
-
await symlink(src, dest, "file");
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
await symlink(src, dest);
|
|
178
|
-
}
|
|
179
|
-
} catch (symlinkErr) {
|
|
180
|
-
const error = symlinkErr as NodeJS.ErrnoException;
|
|
181
|
-
if (isWindows && error.code === "EPERM") {
|
|
182
|
-
console.log(chalk.red(` Permission denied creating symlink.`));
|
|
183
|
-
console.log(chalk.dim(" On Windows, enable Developer Mode or run as Administrator."));
|
|
184
|
-
console.log(chalk.dim(" Settings > Update & Security > For developers > Developer Mode"));
|
|
185
|
-
}
|
|
186
|
-
throw symlinkErr;
|
|
187
|
-
}
|
|
188
|
-
result.created.push(entry.dest);
|
|
189
|
-
|
|
190
|
-
if (verbose) {
|
|
191
|
-
console.log(chalk.dim(` Linked: ${entry.dest} → ${entry.src}`));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
} catch (err) {
|
|
195
|
-
const error = err as NodeJS.ErrnoException;
|
|
196
|
-
const msg = `Failed to install ${entry.dest}: ${formatPermissionError(error, join(baseDir, entry.dest))}`;
|
|
197
|
-
result.errors.push(msg);
|
|
198
|
-
if (verbose) {
|
|
199
|
-
console.log(chalk.red(` ✗ ${msg}`));
|
|
200
|
-
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
201
|
-
console.log(chalk.dim(" Check directory permissions or run with appropriate privileges."));
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// If enabledFeatures provided and plugin has a runtime.json entry, update it
|
|
208
|
-
if (enabledFeatures !== undefined) {
|
|
209
|
-
const runtimeEntry = installEntries.find((e) => e.copy && e.dest.endsWith("runtime.json"));
|
|
210
|
-
if (runtimeEntry) {
|
|
211
|
-
const runtimePath = join(baseDir, runtimeEntry.dest);
|
|
212
|
-
await writeRuntimeConfig(runtimePath, { features: enabledFeatures }, verbose);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return result;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Read runtime.json config from a plugin's installed location
|
|
221
|
-
* Returns {} on failure so callers can detect missing/corrupt config and fall back to defaults
|
|
222
|
-
*/
|
|
223
|
-
export function readRuntimeConfig(runtimePath: string): PluginRuntimeConfig {
|
|
224
|
-
try {
|
|
225
|
-
const content = readFileSync(runtimePath, "utf-8");
|
|
226
|
-
return JSON.parse(content) as PluginRuntimeConfig;
|
|
227
|
-
} catch {
|
|
228
|
-
// Return empty object (not {features: []}) so callers detect missing config
|
|
229
|
-
// and can fall back to plugin defaults instead of treating as "all disabled"
|
|
230
|
-
return {};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Write runtime.json config to a plugin's installed location
|
|
236
|
-
*/
|
|
237
|
-
export async function writeRuntimeConfig(
|
|
238
|
-
runtimePath: string,
|
|
239
|
-
config: PluginRuntimeConfig,
|
|
240
|
-
verbose = false,
|
|
241
|
-
): Promise<void> {
|
|
242
|
-
try {
|
|
243
|
-
const existing = readRuntimeConfig(runtimePath);
|
|
244
|
-
const merged: PluginRuntimeConfig = {
|
|
245
|
-
features: config.features ?? existing.features ?? [],
|
|
246
|
-
options: { ...existing.options, ...config.options },
|
|
247
|
-
};
|
|
248
|
-
writeFileSync(runtimePath, JSON.stringify(merged, null, 2) + "\n");
|
|
249
|
-
if (verbose) {
|
|
250
|
-
console.log(chalk.dim(` Updated: ${runtimePath}`));
|
|
251
|
-
}
|
|
252
|
-
} catch (err) {
|
|
253
|
-
if (verbose) {
|
|
254
|
-
console.log(chalk.yellow(` ⚠ Failed to update runtime config: ${err}`));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Get the path to a plugin's runtime.json in the installed location
|
|
261
|
-
*/
|
|
262
|
-
export function getRuntimeConfigPath(pkgJson: PluginPackageJson, global = true): string | null {
|
|
263
|
-
const entries = getInstallEntries(pkgJson);
|
|
264
|
-
const runtimeEntry = entries.find((e) => e.copy && e.dest.endsWith("runtime.json"));
|
|
265
|
-
if (!runtimeEntry) return null;
|
|
266
|
-
return join(getBaseDir(global), runtimeEntry.dest);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Remove symlinks and copied files for a plugin's omp.install entries
|
|
271
|
-
*/
|
|
272
|
-
export async function removePluginSymlinks(
|
|
273
|
-
_pluginName: string,
|
|
274
|
-
pkgJson: PluginPackageJson,
|
|
275
|
-
global = true,
|
|
276
|
-
verbose = true,
|
|
277
|
-
): Promise<SymlinkRemovalResult> {
|
|
278
|
-
const result: SymlinkRemovalResult = { removed: [], errors: [], skippedNonSymlinks: [] };
|
|
279
|
-
|
|
280
|
-
const installEntries = getInstallEntries(pkgJson);
|
|
281
|
-
if (installEntries.length === 0) {
|
|
282
|
-
return result;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const baseDir = getBaseDir(global);
|
|
286
|
-
|
|
287
|
-
for (const entry of installEntries) {
|
|
288
|
-
// Validate dest path stays within base directory (prevents path traversal attacks)
|
|
289
|
-
if (!isPathWithinBase(baseDir, entry.dest)) {
|
|
290
|
-
const msg = `Path traversal blocked: ${entry.dest} escapes base directory`;
|
|
291
|
-
result.errors.push(msg);
|
|
292
|
-
if (verbose) {
|
|
293
|
-
console.log(chalk.red(` ✗ ${msg}`));
|
|
294
|
-
}
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const dest = join(baseDir, entry.dest);
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
if (existsSync(dest)) {
|
|
302
|
-
const stats = lstatSync(dest);
|
|
303
|
-
|
|
304
|
-
// For copy entries (like runtime.json), we can safely remove them
|
|
305
|
-
if (entry.copy) {
|
|
306
|
-
await rm(dest, { force: true });
|
|
307
|
-
result.removed.push(entry.dest);
|
|
308
|
-
if (verbose) {
|
|
309
|
-
console.log(chalk.dim(` Removed: ${entry.dest}`));
|
|
310
|
-
}
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// For symlinks, check they're actually symlinks
|
|
315
|
-
if (!stats.isSymbolicLink()) {
|
|
316
|
-
result.skippedNonSymlinks.push(dest);
|
|
317
|
-
if (verbose) {
|
|
318
|
-
console.log(chalk.yellow(` ⚠ Skipping ${entry.dest}: not a symlink (may contain user data)`));
|
|
319
|
-
}
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
await rm(dest, { force: true, recursive: true });
|
|
324
|
-
result.removed.push(entry.dest);
|
|
325
|
-
if (verbose) {
|
|
326
|
-
console.log(chalk.dim(` Removed: ${entry.dest}`));
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
} catch (err) {
|
|
330
|
-
const error = err as NodeJS.ErrnoException;
|
|
331
|
-
const msg = `Failed to remove ${entry.dest}: ${formatPermissionError(error, dest)}`;
|
|
332
|
-
result.errors.push(msg);
|
|
333
|
-
if (verbose) {
|
|
334
|
-
console.log(chalk.yellow(` ⚠ ${msg}`));
|
|
335
|
-
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
336
|
-
console.log(chalk.dim(" Check directory permissions or run with appropriate privileges."));
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return result;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Check symlink/file health for a plugin
|
|
347
|
-
*/
|
|
348
|
-
export async function checkPluginSymlinks(
|
|
349
|
-
pluginName: string,
|
|
350
|
-
pkgJson: PluginPackageJson,
|
|
351
|
-
global = true,
|
|
352
|
-
): Promise<{ valid: string[]; broken: string[]; missing: string[] }> {
|
|
353
|
-
const result = { valid: [] as string[], broken: [] as string[], missing: [] as string[] };
|
|
354
|
-
const sourceDir = getPluginSourceDir(pluginName, global);
|
|
355
|
-
const baseDir = getBaseDir(global);
|
|
356
|
-
|
|
357
|
-
const installEntries = getInstallEntries(pkgJson);
|
|
358
|
-
if (installEntries.length === 0) {
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
for (const entry of installEntries) {
|
|
363
|
-
// Skip entries with path traversal (treat as broken)
|
|
364
|
-
if (!isPathWithinBase(baseDir, entry.dest)) {
|
|
365
|
-
result.broken.push(entry.dest);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const src = join(sourceDir, entry.src);
|
|
370
|
-
const dest = join(baseDir, entry.dest);
|
|
371
|
-
|
|
372
|
-
if (!existsSync(dest)) {
|
|
373
|
-
result.missing.push(entry.dest);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
const stats = lstatSync(dest);
|
|
379
|
-
|
|
380
|
-
// For copy entries, just check the file exists
|
|
381
|
-
if (entry.copy) {
|
|
382
|
-
if (stats.isFile()) {
|
|
383
|
-
result.valid.push(entry.dest);
|
|
384
|
-
} else {
|
|
385
|
-
result.broken.push(entry.dest);
|
|
386
|
-
}
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// For symlinks, verify they point to valid sources
|
|
391
|
-
if (stats.isSymbolicLink()) {
|
|
392
|
-
const _target = await readlink(dest);
|
|
393
|
-
if (existsSync(src)) {
|
|
394
|
-
result.valid.push(entry.dest);
|
|
395
|
-
} else {
|
|
396
|
-
result.broken.push(entry.dest);
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
// Not a symlink, might be a file that was overwritten
|
|
400
|
-
result.broken.push(entry.dest);
|
|
401
|
-
}
|
|
402
|
-
} catch {
|
|
403
|
-
result.broken.push(entry.dest);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Get plugin name from an installed symlink destination
|
|
412
|
-
*/
|
|
413
|
-
export async function getPluginForSymlink(
|
|
414
|
-
dest: string,
|
|
415
|
-
installedPlugins: Map<string, PluginPackageJson>,
|
|
416
|
-
): Promise<string | null> {
|
|
417
|
-
for (const [name, pkgJson] of installedPlugins) {
|
|
418
|
-
if (pkgJson.omp?.install) {
|
|
419
|
-
for (const entry of pkgJson.omp.install) {
|
|
420
|
-
if (entry.dest === dest) {
|
|
421
|
-
return name;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return null;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Find all symlinks installed by plugins and trace them back
|
|
431
|
-
*/
|
|
432
|
-
export async function traceInstalledFile(
|
|
433
|
-
filePath: string,
|
|
434
|
-
installedPlugins: Map<string, PluginPackageJson>,
|
|
435
|
-
global = true,
|
|
436
|
-
): Promise<{ plugin: string; entry: OmpInstallEntry } | null> {
|
|
437
|
-
// Normalize the path relative to the base directory
|
|
438
|
-
const baseDir = getBaseDir(global);
|
|
439
|
-
let relativePath = filePath;
|
|
440
|
-
if (filePath.startsWith(baseDir)) {
|
|
441
|
-
relativePath = filePath.slice(baseDir.length + 1);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
for (const [name, pkgJson] of installedPlugins) {
|
|
445
|
-
if (pkgJson.omp?.install) {
|
|
446
|
-
for (const entry of pkgJson.omp.install) {
|
|
447
|
-
if (entry.dest === relativePath) {
|
|
448
|
-
return { plugin: name, entry };
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return null;
|
|
455
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"inlineSources": true,
|
|
14
|
-
"inlineSourceMap": false,
|
|
15
|
-
"moduleResolution": "bundler",
|
|
16
|
-
"resolveJsonModule": true,
|
|
17
|
-
"allowImportingTsExtensions": false,
|
|
18
|
-
"outDir": "./dist",
|
|
19
|
-
"rootDir": "./src",
|
|
20
|
-
"types": ["node", "bun-types"],
|
|
21
|
-
"baseUrl": ".",
|
|
22
|
-
"paths": {
|
|
23
|
-
"@omp/*": ["src/*"]
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"include": ["src/**/*"],
|
|
27
|
-
"exclude": ["node_modules", "dist"]
|
|
28
|
-
}
|