@oh-my-pi/cli 0.1.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/.github/workflows/ci.yml +32 -0
- package/.github/workflows/publish.yml +42 -0
- package/CHECK.md +352 -0
- package/README.md +224 -0
- package/biome.json +29 -0
- package/bun.lock +50 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +3941 -0
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/enable.d.ts +13 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/install.d.ts +13 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/link.d.ts +10 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/outdated.d.ts +9 -0
- package/dist/commands/outdated.d.ts.map +1 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/uninstall.d.ts +9 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/why.d.ts +9 -0
- package/dist/commands/why.d.ts.map +1 -0
- package/dist/conflicts.d.ts +21 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/manifest.d.ts +81 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/migrate.d.ts +9 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/npm.d.ts +77 -0
- package/dist/npm.d.ts.map +1 -0
- package/dist/paths.d.ts +27 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/symlinks.d.ts +33 -0
- package/dist/symlinks.d.ts.map +1 -0
- package/package.json +36 -0
- package/plugins/metal-theme/README.md +13 -0
- package/plugins/metal-theme/omp.json +8 -0
- package/plugins/metal-theme/package.json +14 -0
- package/plugins/metal-theme/themes/metal.json +79 -0
- package/plugins/subagents/README.md +25 -0
- package/plugins/subagents/agents/explore.md +71 -0
- package/plugins/subagents/agents/planner.md +51 -0
- package/plugins/subagents/agents/reviewer.md +53 -0
- package/plugins/subagents/agents/task.md +46 -0
- package/plugins/subagents/commands/architect-plan.md +9 -0
- package/plugins/subagents/commands/implement-with-critic.md +10 -0
- package/plugins/subagents/commands/implement.md +10 -0
- package/plugins/subagents/omp.json +15 -0
- package/plugins/subagents/package.json +21 -0
- package/plugins/subagents/tools/task/index.ts +1019 -0
- package/scripts/bump-version.sh +52 -0
- package/scripts/publish.sh +35 -0
- package/src/cli.ts +167 -0
- package/src/commands/create.ts +153 -0
- package/src/commands/doctor.ts +217 -0
- package/src/commands/enable.ts +105 -0
- package/src/commands/info.ts +84 -0
- package/src/commands/init.ts +42 -0
- package/src/commands/install.ts +327 -0
- package/src/commands/link.ts +108 -0
- package/src/commands/list.ts +71 -0
- package/src/commands/outdated.ts +76 -0
- package/src/commands/search.ts +60 -0
- package/src/commands/uninstall.ts +73 -0
- package/src/commands/update.ts +112 -0
- package/src/commands/why.ts +105 -0
- package/src/conflicts.ts +84 -0
- package/src/index.ts +53 -0
- package/src/manifest.ts +212 -0
- package/src/migrate.ts +181 -0
- package/src/npm.ts +150 -0
- package/src/paths.ts +72 -0
- package/src/symlinks.ts +199 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadPluginsJson, readPluginPackageJson, savePluginsJson } from "../manifest.js";
|
|
3
|
+
import { createPluginSymlinks, removePluginSymlinks } from "../symlinks.js";
|
|
4
|
+
|
|
5
|
+
export interface EnableDisableOptions {
|
|
6
|
+
global?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enable a disabled plugin (re-create symlinks)
|
|
12
|
+
*/
|
|
13
|
+
export async function enablePlugin(name: string, options: EnableDisableOptions = {}): Promise<void> {
|
|
14
|
+
const isGlobal = options.global !== false;
|
|
15
|
+
|
|
16
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
17
|
+
|
|
18
|
+
// Check if plugin exists
|
|
19
|
+
if (!pluginsJson.plugins[name]) {
|
|
20
|
+
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if already enabled
|
|
25
|
+
if (!pluginsJson.disabled?.includes(name)) {
|
|
26
|
+
console.log(chalk.yellow(`Plugin "${name}" is already enabled.`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Read package.json
|
|
32
|
+
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
33
|
+
if (!pkgJson) {
|
|
34
|
+
console.log(chalk.red(`Could not read package.json for ${name}`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Re-create symlinks
|
|
39
|
+
console.log(chalk.blue(`Enabling ${name}...`));
|
|
40
|
+
await createPluginSymlinks(name, pkgJson, isGlobal);
|
|
41
|
+
|
|
42
|
+
// Remove from disabled list
|
|
43
|
+
pluginsJson.disabled = pluginsJson.disabled.filter((n) => n !== name);
|
|
44
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
45
|
+
|
|
46
|
+
console.log(chalk.green(`✓ Enabled "${name}"`));
|
|
47
|
+
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(JSON.stringify({ name, enabled: true }, null, 2));
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.log(chalk.red(`Error enabling plugin: ${(err as Error).message}`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Disable a plugin (remove symlinks but keep installed)
|
|
58
|
+
*/
|
|
59
|
+
export async function disablePlugin(name: string, options: EnableDisableOptions = {}): Promise<void> {
|
|
60
|
+
const isGlobal = options.global !== false;
|
|
61
|
+
|
|
62
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
63
|
+
|
|
64
|
+
// Check if plugin exists
|
|
65
|
+
if (!pluginsJson.plugins[name]) {
|
|
66
|
+
console.log(chalk.yellow(`Plugin "${name}" is not installed.`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if already disabled
|
|
71
|
+
if (pluginsJson.disabled?.includes(name)) {
|
|
72
|
+
console.log(chalk.yellow(`Plugin "${name}" is already disabled.`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Read package.json
|
|
78
|
+
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
79
|
+
if (!pkgJson) {
|
|
80
|
+
console.log(chalk.red(`Could not read package.json for ${name}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Remove symlinks
|
|
85
|
+
console.log(chalk.blue(`Disabling ${name}...`));
|
|
86
|
+
await removePluginSymlinks(name, pkgJson);
|
|
87
|
+
|
|
88
|
+
// Add to disabled list
|
|
89
|
+
if (!pluginsJson.disabled) {
|
|
90
|
+
pluginsJson.disabled = [];
|
|
91
|
+
}
|
|
92
|
+
pluginsJson.disabled.push(name);
|
|
93
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
94
|
+
|
|
95
|
+
console.log(chalk.green(`✓ Disabled "${name}"`));
|
|
96
|
+
console.log(chalk.dim(" Plugin is still installed, symlinks removed"));
|
|
97
|
+
console.log(chalk.dim(` Re-enable with: omp enable ${name}`));
|
|
98
|
+
|
|
99
|
+
if (options.json) {
|
|
100
|
+
console.log(JSON.stringify({ name, enabled: false }, null, 2));
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.log(chalk.red(`Error disabling plugin: ${(err as Error).message}`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { npmInfo } from "../npm.js";
|
|
3
|
+
|
|
4
|
+
export interface InfoOptions {
|
|
5
|
+
json?: boolean;
|
|
6
|
+
versions?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Show detailed info about a package before install
|
|
11
|
+
*/
|
|
12
|
+
export async function showInfo(packageName: string, options: InfoOptions = {}): Promise<void> {
|
|
13
|
+
console.log(chalk.blue(`Fetching info for ${packageName}...`));
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const info = await npmInfo(packageName);
|
|
17
|
+
|
|
18
|
+
if (!info) {
|
|
19
|
+
console.log(chalk.red(`Package not found: ${packageName}`));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify(info, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log();
|
|
29
|
+
console.log(chalk.bold.green(info.name) + chalk.dim(` v${info.version}`));
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
if (info.description) {
|
|
33
|
+
console.log(chalk.white(info.description));
|
|
34
|
+
console.log();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Author
|
|
38
|
+
if (info.author) {
|
|
39
|
+
const authorStr =
|
|
40
|
+
typeof info.author === "string"
|
|
41
|
+
? info.author
|
|
42
|
+
: `${info.author.name}${info.author.email ? ` <${info.author.email}>` : ""}`;
|
|
43
|
+
console.log(chalk.dim("author: ") + authorStr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Homepage
|
|
47
|
+
if (info.homepage) {
|
|
48
|
+
console.log(chalk.dim("homepage: ") + info.homepage);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Repository
|
|
52
|
+
if (info.repository) {
|
|
53
|
+
const repoUrl = typeof info.repository === "string" ? info.repository : info.repository.url;
|
|
54
|
+
console.log(chalk.dim("repo: ") + repoUrl);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Keywords
|
|
58
|
+
if (info.keywords?.length) {
|
|
59
|
+
console.log(chalk.dim("keywords: ") + info.keywords.join(", "));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Is it an omp plugin?
|
|
63
|
+
const isOmpPlugin = info.keywords?.includes("omp-plugin");
|
|
64
|
+
if (isOmpPlugin) {
|
|
65
|
+
console.log(chalk.green("\n✓ This is an omp plugin"));
|
|
66
|
+
} else {
|
|
67
|
+
console.log(chalk.yellow("\n⚠ This package does not have the omp-plugin keyword"));
|
|
68
|
+
console.log(chalk.dim(" It may work, but might not have omp.install configuration"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Versions
|
|
72
|
+
if (options.versions && info["dist-tags"]) {
|
|
73
|
+
console.log(chalk.dim("\ndist-tags:"));
|
|
74
|
+
for (const [tag, version] of Object.entries(info["dist-tags"])) {
|
|
75
|
+
console.log(chalk.dim(` ${tag}: `) + version);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log();
|
|
80
|
+
console.log(chalk.dim(`Install with: omp install ${packageName}`));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.log(chalk.red(`Error fetching info: ${(err as Error).message}`));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { PROJECT_PI_DIR, PROJECT_PLUGINS_JSON } from "../paths.js";
|
|
5
|
+
|
|
6
|
+
export interface InitOptions {
|
|
7
|
+
force?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize .pi/plugins.json in current project
|
|
12
|
+
*/
|
|
13
|
+
export async function initProject(options: InitOptions = {}): Promise<void> {
|
|
14
|
+
// Check if already exists
|
|
15
|
+
if (existsSync(PROJECT_PLUGINS_JSON) && !options.force) {
|
|
16
|
+
console.log(chalk.yellow(`${PROJECT_PLUGINS_JSON} already exists.`));
|
|
17
|
+
console.log(chalk.dim("Use --force to overwrite"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Create .pi directory
|
|
23
|
+
await mkdir(PROJECT_PI_DIR, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Create plugins.json
|
|
26
|
+
const pluginsJson = {
|
|
27
|
+
plugins: {},
|
|
28
|
+
disabled: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await writeFile(PROJECT_PLUGINS_JSON, JSON.stringify(pluginsJson, null, 2));
|
|
32
|
+
|
|
33
|
+
console.log(chalk.green(`✓ Created ${PROJECT_PLUGINS_JSON}`));
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(chalk.dim("Next steps:"));
|
|
36
|
+
console.log(chalk.dim(" 1. Add plugins: omp install <package> --save"));
|
|
37
|
+
console.log(chalk.dim(" 2. Or edit plugins.json directly"));
|
|
38
|
+
console.log(chalk.dim(" 3. Run: omp install (to install all)"));
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.log(chalk.red(`Error initializing project: ${(err as Error).message}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { cp, mkdir, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { type Conflict, detectConflicts, formatConflicts } from "../conflicts.js";
|
|
8
|
+
import {
|
|
9
|
+
getInstalledPlugins,
|
|
10
|
+
initGlobalPlugins,
|
|
11
|
+
loadPluginsJson,
|
|
12
|
+
type PluginPackageJson,
|
|
13
|
+
readPluginPackageJson,
|
|
14
|
+
savePluginsJson,
|
|
15
|
+
} from "../manifest.js";
|
|
16
|
+
import { npmInfo, npmInstall } from "../npm.js";
|
|
17
|
+
import { NODE_MODULES_DIR, PLUGINS_DIR, PROJECT_NODE_MODULES, PROJECT_PLUGINS_JSON } from "../paths.js";
|
|
18
|
+
import { createPluginSymlinks } from "../symlinks.js";
|
|
19
|
+
|
|
20
|
+
export interface InstallOptions {
|
|
21
|
+
global?: boolean;
|
|
22
|
+
save?: boolean;
|
|
23
|
+
saveDev?: boolean;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prompt user to choose when there's a conflict
|
|
30
|
+
*/
|
|
31
|
+
async function promptConflictResolution(conflict: Conflict): Promise<number | null> {
|
|
32
|
+
const rl = createInterface({
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
console.log(chalk.yellow(`\n⚠ Conflict: ${formatConflicts([conflict])[0]}`));
|
|
39
|
+
conflict.plugins.forEach((p, i) => {
|
|
40
|
+
console.log(` [${i + 1}] ${p.name}`);
|
|
41
|
+
});
|
|
42
|
+
console.log(` [${conflict.plugins.length + 1}] abort`);
|
|
43
|
+
|
|
44
|
+
rl.question(" Choose: ", (answer) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
const choice = parseInt(answer, 10);
|
|
47
|
+
if (choice > 0 && choice <= conflict.plugins.length) {
|
|
48
|
+
resolve(choice - 1);
|
|
49
|
+
} else {
|
|
50
|
+
resolve(null);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse package specifier into name and version
|
|
58
|
+
*/
|
|
59
|
+
function parsePackageSpec(spec: string): { name: string; version: string } {
|
|
60
|
+
// Handle scoped packages: @scope/name@version
|
|
61
|
+
if (spec.startsWith("@")) {
|
|
62
|
+
const lastAt = spec.lastIndexOf("@");
|
|
63
|
+
if (lastAt > 0) {
|
|
64
|
+
return {
|
|
65
|
+
name: spec.slice(0, lastAt),
|
|
66
|
+
version: spec.slice(lastAt + 1),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { name: spec, version: "latest" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle regular packages: name@version
|
|
73
|
+
const atIndex = spec.indexOf("@");
|
|
74
|
+
if (atIndex > 0) {
|
|
75
|
+
return {
|
|
76
|
+
name: spec.slice(0, atIndex),
|
|
77
|
+
version: spec.slice(atIndex + 1),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { name: spec, version: "latest" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a path looks like a local path
|
|
86
|
+
*/
|
|
87
|
+
function isLocalPath(spec: string): boolean {
|
|
88
|
+
return spec.startsWith("/") || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("~");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Install plugins from package specifiers
|
|
93
|
+
* omp install [pkg...]
|
|
94
|
+
*/
|
|
95
|
+
export async function installPlugin(packages?: string[], options: InstallOptions = {}): Promise<void> {
|
|
96
|
+
const isGlobal = options.global !== false; // Default to global
|
|
97
|
+
const prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
98
|
+
const _nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
99
|
+
|
|
100
|
+
// Initialize plugins directory if needed
|
|
101
|
+
if (isGlobal) {
|
|
102
|
+
await initGlobalPlugins();
|
|
103
|
+
} else {
|
|
104
|
+
// Ensure project .pi directory exists
|
|
105
|
+
await mkdir(prefix, { recursive: true });
|
|
106
|
+
// Initialize plugins.json if it doesn't exist
|
|
107
|
+
if (!existsSync(PROJECT_PLUGINS_JSON)) {
|
|
108
|
+
await savePluginsJson({ plugins: {} }, false);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If no packages specified, install from plugins.json
|
|
113
|
+
if (!packages || packages.length === 0) {
|
|
114
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
115
|
+
packages = Object.entries(pluginsJson.plugins).map(([name, version]) => `${name}@${version}`);
|
|
116
|
+
|
|
117
|
+
if (packages.length === 0) {
|
|
118
|
+
console.log(chalk.yellow("No plugins to install."));
|
|
119
|
+
console.log(
|
|
120
|
+
chalk.dim(isGlobal ? "Add plugins with: omp install <package>" : "Add plugins to .pi/plugins.json"),
|
|
121
|
+
);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(
|
|
126
|
+
chalk.blue(`Installing ${packages.length} plugin(s) from ${isGlobal ? "package.json" : "plugins.json"}...`),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get existing plugins for conflict detection
|
|
131
|
+
const existingPlugins = await getInstalledPlugins(isGlobal);
|
|
132
|
+
|
|
133
|
+
const results: Array<{ name: string; version: string; success: boolean; error?: string }> = [];
|
|
134
|
+
|
|
135
|
+
for (const spec of packages) {
|
|
136
|
+
// Check if it's a local path
|
|
137
|
+
if (isLocalPath(spec)) {
|
|
138
|
+
const result = await installLocalPlugin(spec, isGlobal, options);
|
|
139
|
+
results.push(result);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { name, version } = parsePackageSpec(spec);
|
|
144
|
+
const pkgSpec = version === "latest" ? name : `${name}@${version}`;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
console.log(chalk.blue(`\nInstalling ${pkgSpec}...`));
|
|
148
|
+
|
|
149
|
+
// 1. Resolve version from npm registry
|
|
150
|
+
const info = await npmInfo(pkgSpec);
|
|
151
|
+
if (!info) {
|
|
152
|
+
console.log(chalk.red(` ✗ Package not found: ${name}`));
|
|
153
|
+
results.push({ name, version, success: false, error: "Package not found" });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Check for conflicts before installing
|
|
158
|
+
// We need to fetch the package.json to check omp.install
|
|
159
|
+
// For now, we'll check after npm install and rollback if needed
|
|
160
|
+
|
|
161
|
+
// 3. npm install
|
|
162
|
+
console.log(chalk.dim(` Fetching from npm...`));
|
|
163
|
+
await npmInstall([pkgSpec], prefix, { save: options.save || isGlobal });
|
|
164
|
+
|
|
165
|
+
// 4. Read package.json from installed package
|
|
166
|
+
const pkgJson = await readPluginPackageJson(name, isGlobal);
|
|
167
|
+
if (!pkgJson) {
|
|
168
|
+
console.log(chalk.yellow(` ⚠ Installed but no package.json found`));
|
|
169
|
+
results.push({ name, version: info.version, success: true });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 5. Check for conflicts
|
|
174
|
+
const conflicts = detectConflicts(name, pkgJson, existingPlugins);
|
|
175
|
+
|
|
176
|
+
if (conflicts.length > 0 && !options.force) {
|
|
177
|
+
// Handle conflicts
|
|
178
|
+
let abort = false;
|
|
179
|
+
for (const conflict of conflicts) {
|
|
180
|
+
const choice = await promptConflictResolution(conflict);
|
|
181
|
+
if (choice === null) {
|
|
182
|
+
abort = true;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
// If user chose the new plugin, we continue
|
|
186
|
+
// If user chose existing plugin, we skip this destination
|
|
187
|
+
// For now, simplify: if not aborted, force overwrite
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (abort) {
|
|
191
|
+
console.log(chalk.yellow(` Aborted due to conflicts`));
|
|
192
|
+
// Rollback: uninstall the package
|
|
193
|
+
execSync(`npm uninstall --prefix ${prefix} ${name}`, { stdio: "pipe" });
|
|
194
|
+
results.push({ name, version: info.version, success: false, error: "Conflicts" });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 6. Create symlinks for omp.install entries
|
|
200
|
+
const _symlinkResult = await createPluginSymlinks(name, pkgJson, isGlobal);
|
|
201
|
+
|
|
202
|
+
// 7. Process dependencies with omp field
|
|
203
|
+
if (pkgJson.dependencies) {
|
|
204
|
+
for (const depName of Object.keys(pkgJson.dependencies)) {
|
|
205
|
+
const depPkgJson = await readPluginPackageJson(depName, isGlobal);
|
|
206
|
+
if (depPkgJson?.omp?.install) {
|
|
207
|
+
console.log(chalk.dim(` Processing dependency: ${depName}`));
|
|
208
|
+
await createPluginSymlinks(depName, depPkgJson, isGlobal);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Add to installed plugins map for subsequent conflict detection
|
|
214
|
+
existingPlugins.set(name, pkgJson);
|
|
215
|
+
|
|
216
|
+
console.log(chalk.green(`✓ Installed ${name}@${info.version}`));
|
|
217
|
+
results.push({ name, version: info.version, success: true });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const errorMsg = (err as Error).message;
|
|
220
|
+
console.log(chalk.red(` ✗ Failed to install ${name}: ${errorMsg}`));
|
|
221
|
+
results.push({ name, version, success: false, error: errorMsg });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Summary
|
|
226
|
+
const successful = results.filter((r) => r.success);
|
|
227
|
+
const failed = results.filter((r) => !r.success);
|
|
228
|
+
|
|
229
|
+
console.log();
|
|
230
|
+
if (successful.length > 0) {
|
|
231
|
+
console.log(chalk.green(`✓ Installed ${successful.length} plugin(s)`));
|
|
232
|
+
}
|
|
233
|
+
if (failed.length > 0) {
|
|
234
|
+
console.log(chalk.red(`✗ Failed to install ${failed.length} plugin(s)`));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (options.json) {
|
|
238
|
+
console.log(JSON.stringify({ results }, null, 2));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Install a local plugin (copy or link based on path type)
|
|
244
|
+
*/
|
|
245
|
+
async function installLocalPlugin(
|
|
246
|
+
localPath: string,
|
|
247
|
+
isGlobal: boolean,
|
|
248
|
+
_options: InstallOptions,
|
|
249
|
+
): Promise<{ name: string; version: string; success: boolean; error?: string }> {
|
|
250
|
+
// Expand ~ to home directory
|
|
251
|
+
if (localPath.startsWith("~")) {
|
|
252
|
+
localPath = join(process.env.HOME || "", localPath.slice(1));
|
|
253
|
+
}
|
|
254
|
+
localPath = resolve(localPath);
|
|
255
|
+
|
|
256
|
+
if (!existsSync(localPath)) {
|
|
257
|
+
console.log(chalk.red(`Error: Path does not exist: ${localPath}`));
|
|
258
|
+
return { name: basename(localPath), version: "local", success: false, error: "Path not found" };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const _prefix = isGlobal ? PLUGINS_DIR : ".pi";
|
|
262
|
+
const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Read package.json from local path
|
|
266
|
+
const localPkgJsonPath = join(localPath, "package.json");
|
|
267
|
+
let pkgJson: PluginPackageJson;
|
|
268
|
+
|
|
269
|
+
if (existsSync(localPkgJsonPath)) {
|
|
270
|
+
pkgJson = JSON.parse(await readFile(localPkgJsonPath, "utf-8"));
|
|
271
|
+
} else {
|
|
272
|
+
// Check for omp.json (legacy format)
|
|
273
|
+
const ompJsonPath = join(localPath, "omp.json");
|
|
274
|
+
if (existsSync(ompJsonPath)) {
|
|
275
|
+
const ompJson = JSON.parse(await readFile(ompJsonPath, "utf-8"));
|
|
276
|
+
// Convert omp.json to package.json format
|
|
277
|
+
pkgJson = {
|
|
278
|
+
name: ompJson.name || basename(localPath),
|
|
279
|
+
version: ompJson.version || "0.0.0",
|
|
280
|
+
description: ompJson.description,
|
|
281
|
+
keywords: ["omp-plugin"],
|
|
282
|
+
omp: {
|
|
283
|
+
install: ompJson.install,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
pkgJson = {
|
|
288
|
+
name: basename(localPath),
|
|
289
|
+
version: "0.0.0",
|
|
290
|
+
keywords: ["omp-plugin"],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const pluginName = pkgJson.name;
|
|
296
|
+
const pluginDir = join(nodeModules, pluginName);
|
|
297
|
+
|
|
298
|
+
console.log(chalk.blue(`\nInstalling ${pluginName} from ${localPath}...`));
|
|
299
|
+
|
|
300
|
+
// Create node_modules directory
|
|
301
|
+
await mkdir(nodeModules, { recursive: true });
|
|
302
|
+
|
|
303
|
+
// Remove existing if present
|
|
304
|
+
if (existsSync(pluginDir)) {
|
|
305
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Copy the plugin
|
|
309
|
+
await cp(localPath, pluginDir, { recursive: true });
|
|
310
|
+
console.log(chalk.dim(` Copied to ${pluginDir}`));
|
|
311
|
+
|
|
312
|
+
// Update plugins.json/package.json
|
|
313
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
314
|
+
pluginsJson.plugins[pluginName] = `file:${localPath}`;
|
|
315
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
316
|
+
|
|
317
|
+
// Create symlinks
|
|
318
|
+
await createPluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
319
|
+
|
|
320
|
+
console.log(chalk.green(`✓ Installed ${pluginName}@${pkgJson.version}`));
|
|
321
|
+
return { name: pluginName, version: pkgJson.version, success: true };
|
|
322
|
+
} catch (err) {
|
|
323
|
+
const errorMsg = (err as Error).message;
|
|
324
|
+
console.log(chalk.red(` ✗ Failed: ${errorMsg}`));
|
|
325
|
+
return { name: basename(localPath), version: "local", success: false, error: errorMsg };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { loadPluginsJson, type PluginPackageJson, savePluginsJson } from "../manifest.js";
|
|
6
|
+
import { NODE_MODULES_DIR, PROJECT_NODE_MODULES } from "../paths.js";
|
|
7
|
+
import { createPluginSymlinks } from "../symlinks.js";
|
|
8
|
+
|
|
9
|
+
export interface LinkOptions {
|
|
10
|
+
name?: string;
|
|
11
|
+
global?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Link a local plugin directory for development
|
|
16
|
+
* Creates a symlink in node_modules pointing to the local directory
|
|
17
|
+
*/
|
|
18
|
+
export async function linkPlugin(localPath: string, options: LinkOptions = {}): Promise<void> {
|
|
19
|
+
const isGlobal = options.global !== false;
|
|
20
|
+
const nodeModules = isGlobal ? NODE_MODULES_DIR : PROJECT_NODE_MODULES;
|
|
21
|
+
|
|
22
|
+
// Expand ~ to home directory
|
|
23
|
+
if (localPath.startsWith("~")) {
|
|
24
|
+
localPath = join(process.env.HOME || "", localPath.slice(1));
|
|
25
|
+
}
|
|
26
|
+
localPath = resolve(localPath);
|
|
27
|
+
|
|
28
|
+
// Verify the path exists
|
|
29
|
+
if (!existsSync(localPath)) {
|
|
30
|
+
console.log(chalk.red(`Error: Path does not exist: ${localPath}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Read package.json from local path
|
|
35
|
+
let pkgJson: PluginPackageJson;
|
|
36
|
+
const localPkgJsonPath = join(localPath, "package.json");
|
|
37
|
+
const localOmpJsonPath = join(localPath, "omp.json");
|
|
38
|
+
|
|
39
|
+
if (existsSync(localPkgJsonPath)) {
|
|
40
|
+
pkgJson = JSON.parse(await readFile(localPkgJsonPath, "utf-8"));
|
|
41
|
+
} else if (existsSync(localOmpJsonPath)) {
|
|
42
|
+
// Convert legacy omp.json to package.json format
|
|
43
|
+
const ompJson = JSON.parse(await readFile(localOmpJsonPath, "utf-8"));
|
|
44
|
+
pkgJson = {
|
|
45
|
+
name: ompJson.name || options.name || basename(localPath),
|
|
46
|
+
version: ompJson.version || "0.0.0-dev",
|
|
47
|
+
description: ompJson.description,
|
|
48
|
+
keywords: ["omp-plugin"],
|
|
49
|
+
omp: {
|
|
50
|
+
install: ompJson.install,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
pkgJson = {
|
|
55
|
+
name: options.name || basename(localPath),
|
|
56
|
+
version: "0.0.0-dev",
|
|
57
|
+
keywords: ["omp-plugin"],
|
|
58
|
+
};
|
|
59
|
+
console.log(chalk.yellow(" Warning: No package.json or omp.json found"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pluginName = options.name || pkgJson.name;
|
|
63
|
+
const pluginDir = join(nodeModules, pluginName);
|
|
64
|
+
|
|
65
|
+
// Check if already installed
|
|
66
|
+
const pluginsJson = await loadPluginsJson(isGlobal);
|
|
67
|
+
if (pluginsJson.plugins[pluginName]) {
|
|
68
|
+
console.log(chalk.yellow(`Plugin "${pluginName}" is already installed.`));
|
|
69
|
+
console.log(chalk.dim("Use omp uninstall first, or specify a different name with -n"));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
console.log(chalk.blue(`Linking ${localPath}...`));
|
|
75
|
+
|
|
76
|
+
// Create parent directory (handles scoped packages like @org/name)
|
|
77
|
+
await mkdir(dirname(pluginDir), { recursive: true });
|
|
78
|
+
|
|
79
|
+
// Remove existing if present
|
|
80
|
+
if (existsSync(pluginDir)) {
|
|
81
|
+
await rm(pluginDir, { force: true, recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create symlink to the plugin directory
|
|
85
|
+
await symlink(localPath, pluginDir);
|
|
86
|
+
console.log(chalk.dim(` Symlinked: ${pluginDir} → ${localPath}`));
|
|
87
|
+
|
|
88
|
+
// Update plugins.json with file: protocol
|
|
89
|
+
pluginsJson.plugins[pluginName] = `file:${localPath}`;
|
|
90
|
+
await savePluginsJson(pluginsJson, isGlobal);
|
|
91
|
+
|
|
92
|
+
// Create symlinks for omp.install entries
|
|
93
|
+
if (pkgJson.omp?.install?.length) {
|
|
94
|
+
await createPluginSymlinks(pluginName, pkgJson, isGlobal);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(
|
|
98
|
+
chalk.green(`\n✓ Linked "${pluginName}"${pkgJson.version ? ` v${pkgJson.version}` : ""} (development mode)`),
|
|
99
|
+
);
|
|
100
|
+
console.log(chalk.dim(" Changes to the source will be reflected immediately"));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.log(chalk.red(`Error linking plugin: ${(err as Error).message}`));
|
|
103
|
+
// Cleanup on failure
|
|
104
|
+
try {
|
|
105
|
+
await rm(pluginDir, { force: true });
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
}
|