@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.
Files changed (89) hide show
  1. package/.github/workflows/ci.yml +32 -0
  2. package/.github/workflows/publish.yml +42 -0
  3. package/CHECK.md +352 -0
  4. package/README.md +224 -0
  5. package/biome.json +29 -0
  6. package/bun.lock +50 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +3941 -0
  10. package/dist/commands/create.d.ts +9 -0
  11. package/dist/commands/create.d.ts.map +1 -0
  12. package/dist/commands/doctor.d.ts +10 -0
  13. package/dist/commands/doctor.d.ts.map +1 -0
  14. package/dist/commands/enable.d.ts +13 -0
  15. package/dist/commands/enable.d.ts.map +1 -0
  16. package/dist/commands/info.d.ts +9 -0
  17. package/dist/commands/info.d.ts.map +1 -0
  18. package/dist/commands/init.d.ts +8 -0
  19. package/dist/commands/init.d.ts.map +1 -0
  20. package/dist/commands/install.d.ts +13 -0
  21. package/dist/commands/install.d.ts.map +1 -0
  22. package/dist/commands/link.d.ts +10 -0
  23. package/dist/commands/link.d.ts.map +1 -0
  24. package/dist/commands/list.d.ts +9 -0
  25. package/dist/commands/list.d.ts.map +1 -0
  26. package/dist/commands/outdated.d.ts +9 -0
  27. package/dist/commands/outdated.d.ts.map +1 -0
  28. package/dist/commands/search.d.ts +9 -0
  29. package/dist/commands/search.d.ts.map +1 -0
  30. package/dist/commands/uninstall.d.ts +9 -0
  31. package/dist/commands/uninstall.d.ts.map +1 -0
  32. package/dist/commands/update.d.ts +9 -0
  33. package/dist/commands/update.d.ts.map +1 -0
  34. package/dist/commands/why.d.ts +9 -0
  35. package/dist/commands/why.d.ts.map +1 -0
  36. package/dist/conflicts.d.ts +21 -0
  37. package/dist/conflicts.d.ts.map +1 -0
  38. package/dist/index.d.ts +20 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/manifest.d.ts +81 -0
  41. package/dist/manifest.d.ts.map +1 -0
  42. package/dist/migrate.d.ts +9 -0
  43. package/dist/migrate.d.ts.map +1 -0
  44. package/dist/npm.d.ts +77 -0
  45. package/dist/npm.d.ts.map +1 -0
  46. package/dist/paths.d.ts +27 -0
  47. package/dist/paths.d.ts.map +1 -0
  48. package/dist/symlinks.d.ts +33 -0
  49. package/dist/symlinks.d.ts.map +1 -0
  50. package/package.json +36 -0
  51. package/plugins/metal-theme/README.md +13 -0
  52. package/plugins/metal-theme/omp.json +8 -0
  53. package/plugins/metal-theme/package.json +14 -0
  54. package/plugins/metal-theme/themes/metal.json +79 -0
  55. package/plugins/subagents/README.md +25 -0
  56. package/plugins/subagents/agents/explore.md +71 -0
  57. package/plugins/subagents/agents/planner.md +51 -0
  58. package/plugins/subagents/agents/reviewer.md +53 -0
  59. package/plugins/subagents/agents/task.md +46 -0
  60. package/plugins/subagents/commands/architect-plan.md +9 -0
  61. package/plugins/subagents/commands/implement-with-critic.md +10 -0
  62. package/plugins/subagents/commands/implement.md +10 -0
  63. package/plugins/subagents/omp.json +15 -0
  64. package/plugins/subagents/package.json +21 -0
  65. package/plugins/subagents/tools/task/index.ts +1019 -0
  66. package/scripts/bump-version.sh +52 -0
  67. package/scripts/publish.sh +35 -0
  68. package/src/cli.ts +167 -0
  69. package/src/commands/create.ts +153 -0
  70. package/src/commands/doctor.ts +217 -0
  71. package/src/commands/enable.ts +105 -0
  72. package/src/commands/info.ts +84 -0
  73. package/src/commands/init.ts +42 -0
  74. package/src/commands/install.ts +327 -0
  75. package/src/commands/link.ts +108 -0
  76. package/src/commands/list.ts +71 -0
  77. package/src/commands/outdated.ts +76 -0
  78. package/src/commands/search.ts +60 -0
  79. package/src/commands/uninstall.ts +73 -0
  80. package/src/commands/update.ts +112 -0
  81. package/src/commands/why.ts +105 -0
  82. package/src/conflicts.ts +84 -0
  83. package/src/index.ts +53 -0
  84. package/src/manifest.ts +212 -0
  85. package/src/migrate.ts +181 -0
  86. package/src/npm.ts +150 -0
  87. package/src/paths.ts +72 -0
  88. package/src/symlinks.ts +199 -0
  89. package/tsconfig.json +24 -0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # Bump version across all packages
5
+ # Usage: ./scripts/bump-version.sh <version>
6
+ # Example: ./scripts/bump-version.sh 1.0.0
7
+
8
+ if [[ -z "$1" ]]; then
9
+ echo "Usage: $0 <version>"
10
+ echo "Example: $0 1.0.0"
11
+ exit 1
12
+ fi
13
+
14
+ VERSION="$1"
15
+
16
+ echo "📦 Bumping all packages to v$VERSION..."
17
+
18
+ # Update root package.json
19
+ echo " Updating package.json..."
20
+ bun --eval "
21
+ const pkg = require('./package.json');
22
+ pkg.version = '$VERSION';
23
+ require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, '\t') + '\n');
24
+ "
25
+
26
+ # Update plugins/subagents/package.json
27
+ echo " Updating plugins/subagents/package.json..."
28
+ bun --eval "
29
+ const pkg = require('./plugins/subagents/package.json');
30
+ pkg.version = '$VERSION';
31
+ require('fs').writeFileSync('plugins/subagents/package.json', JSON.stringify(pkg, null, 2) + '\n');
32
+ "
33
+
34
+ # Update plugins/metal-theme/package.json
35
+ echo " Updating plugins/metal-theme/package.json..."
36
+ bun --eval "
37
+ const pkg = require('./plugins/metal-theme/package.json');
38
+ pkg.version = '$VERSION';
39
+ require('fs').writeFileSync('plugins/metal-theme/package.json', JSON.stringify(pkg, null, 2) + '\n');
40
+ "
41
+
42
+ # Update version in CLI
43
+ echo " Updating src/cli.ts version..."
44
+ sed -i "s/\.version(\"[^\"]*\")/.version(\"$VERSION\")/" src/cli.ts
45
+
46
+ echo ""
47
+ echo "✅ All packages bumped to v$VERSION"
48
+ echo ""
49
+ echo "Next steps:"
50
+ echo " 1. git add -A && git commit -m 'chore: bump version to $VERSION'"
51
+ echo " 2. git tag v$VERSION"
52
+ echo " 3. git push && git push --tags"
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # Publish all @oh-my-pi packages
5
+ # Usage: ./scripts/publish.sh [--dry-run]
6
+
7
+ DRY_RUN=""
8
+ if [[ "$1" == "--dry-run" ]]; then
9
+ DRY_RUN="--dry-run"
10
+ echo "🔍 Dry run mode - no packages will be published"
11
+ fi
12
+
13
+ echo "📦 Publishing @oh-my-pi packages..."
14
+ echo ""
15
+
16
+ # Build first
17
+ echo "🔨 Building CLI..."
18
+ bun run build
19
+
20
+ # Publish CLI
21
+ echo ""
22
+ echo "📤 Publishing @oh-my-pi/cli..."
23
+ npm publish --access public $DRY_RUN
24
+
25
+ # Publish plugins
26
+ echo ""
27
+ echo "📤 Publishing @oh-my-pi/subagents..."
28
+ cd plugins/subagents && npm publish --access public $DRY_RUN && cd ../..
29
+
30
+ echo ""
31
+ echo "📤 Publishing @oh-my-pi/metal-theme..."
32
+ cd plugins/metal-theme && npm publish --access public $DRY_RUN && cd ../..
33
+
34
+ echo ""
35
+ echo "✅ All packages published!"
package/src/cli.ts ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { program } from "commander";
4
+ import { createPlugin } from "./commands/create.js";
5
+ import { runDoctor } from "./commands/doctor.js";
6
+ import { disablePlugin, enablePlugin } from "./commands/enable.js";
7
+ import { showInfo } from "./commands/info.js";
8
+ import { initProject } from "./commands/init.js";
9
+ import { installPlugin } from "./commands/install.js";
10
+ import { linkPlugin } from "./commands/link.js";
11
+ import { listPlugins } from "./commands/list.js";
12
+ import { showOutdated } from "./commands/outdated.js";
13
+ import { searchPlugins } from "./commands/search.js";
14
+ import { uninstallPlugin } from "./commands/uninstall.js";
15
+ import { updatePlugin } from "./commands/update.js";
16
+ import { whyFile } from "./commands/why.js";
17
+ import { checkMigration, migrateToNpm } from "./migrate.js";
18
+
19
+ program.name("omp").description("Oh My Pi - Plugin manager for pi configuration").version("0.1.0");
20
+
21
+ // Check for migration on startup (only for commands that need it)
22
+ program.hook("preAction", async (thisCommand) => {
23
+ const migratingCommands = ["install", "uninstall", "update", "list", "link"];
24
+ if (migratingCommands.includes(thisCommand.name())) {
25
+ await checkMigration();
26
+ }
27
+ });
28
+
29
+ // ============================================================================
30
+ // Core Commands
31
+ // ============================================================================
32
+
33
+ program
34
+ .command("install [packages...]")
35
+ .alias("i")
36
+ .description("Install plugin(s). No args = install from plugins.json")
37
+ .addHelpText(
38
+ "after",
39
+ `
40
+ Examples:
41
+ $ omp install @oh-my-pi/subagents # Install from npm
42
+ $ omp install @oh-my-pi/subagents@^2.0.0 # Specific version range
43
+ $ omp install @myorg/cool-theme # Scoped package
44
+ $ omp install ./local/path # Local directory (copies)
45
+ $ omp install # Install all from plugins.json
46
+ `,
47
+ )
48
+ .option("-g, --global", "Install globally to ~/.pi (default)")
49
+ .option("-S, --save", "Add to plugins.json")
50
+ .option("-D, --save-dev", "Add as dev dependency")
51
+ .option("--force", "Overwrite conflicts without prompting")
52
+ .option("--json", "Output as JSON")
53
+ .action(installPlugin);
54
+
55
+ program
56
+ .command("uninstall <name>")
57
+ .alias("rm")
58
+ .description("Remove plugin and its symlinks")
59
+ .option("-g, --global", "Uninstall from ~/.pi (default)")
60
+ .option("--json", "Output as JSON")
61
+ .action(uninstallPlugin);
62
+
63
+ program
64
+ .command("update [name]")
65
+ .alias("up")
66
+ .description("Update to latest within semver range")
67
+ .option("-g, --global", "Update global plugins (default)")
68
+ .option("--json", "Output as JSON")
69
+ .action(updatePlugin);
70
+
71
+ program
72
+ .command("list")
73
+ .alias("ls")
74
+ .description("Show installed plugins")
75
+ .option("-g, --global", "List global plugins (default)")
76
+ .option("--json", "Output as JSON")
77
+ .action(listPlugins);
78
+
79
+ program
80
+ .command("link <path>")
81
+ .description("Symlink local plugin (dev mode)")
82
+ .addHelpText(
83
+ "after",
84
+ `
85
+ Unlike install, link creates a symlink to the original directory,
86
+ so changes are reflected immediately without reinstalling.
87
+ `,
88
+ )
89
+ .option("-n, --name <name>", "Custom name for the plugin")
90
+ .option("-g, --global", "Link globally (default)")
91
+ .action(linkPlugin);
92
+
93
+ // ============================================================================
94
+ // New Commands
95
+ // ============================================================================
96
+
97
+ program
98
+ .command("init")
99
+ .description("Create .pi/plugins.json in current project")
100
+ .option("--force", "Overwrite existing plugins.json")
101
+ .action(initProject);
102
+
103
+ program
104
+ .command("search <query>")
105
+ .description("Search npm for omp-plugin keyword")
106
+ .option("--json", "Output as JSON")
107
+ .option("--limit <n>", "Maximum results to show", "20")
108
+ .action((query, options) => searchPlugins(query, { ...options, limit: parseInt(options.limit, 10) }));
109
+
110
+ program
111
+ .command("info <package>")
112
+ .description("Show plugin details before install")
113
+ .option("--json", "Output as JSON")
114
+ .option("--versions", "Show available versions")
115
+ .action(showInfo);
116
+
117
+ program
118
+ .command("outdated")
119
+ .description("List plugins with newer versions")
120
+ .option("-g, --global", "Check global plugins (default)")
121
+ .option("--json", "Output as JSON")
122
+ .action(showOutdated);
123
+
124
+ program
125
+ .command("doctor")
126
+ .description("Check for broken symlinks, conflicts")
127
+ .option("-g, --global", "Check global plugins (default)")
128
+ .option("--fix", "Attempt to fix issues")
129
+ .option("--json", "Output as JSON")
130
+ .action(runDoctor);
131
+
132
+ program
133
+ .command("create <name>")
134
+ .description("Scaffold new plugin from template")
135
+ .option("-d, --description <desc>", "Plugin description")
136
+ .option("-a, --author <author>", "Plugin author")
137
+ .action(createPlugin);
138
+
139
+ program
140
+ .command("why <file>")
141
+ .description("Show which plugin installed a file")
142
+ .option("-g, --global", "Check global plugins (default)")
143
+ .option("--json", "Output as JSON")
144
+ .action(whyFile);
145
+
146
+ program
147
+ .command("enable <name>")
148
+ .description("Enable a disabled plugin")
149
+ .option("-g, --global", "Target global plugins (default)")
150
+ .option("--json", "Output as JSON")
151
+ .action(enablePlugin);
152
+
153
+ program
154
+ .command("disable <name>")
155
+ .description("Disable plugin without uninstalling")
156
+ .option("-g, --global", "Target global plugins (default)")
157
+ .option("--json", "Output as JSON")
158
+ .action(disablePlugin);
159
+
160
+ program
161
+ .command("migrate")
162
+ .description("Migrate from legacy manifest.json to npm-native format")
163
+ .action(async () => {
164
+ await migrateToNpm();
165
+ });
166
+
167
+ program.parse();
@@ -0,0 +1,153 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import chalk from "chalk";
5
+
6
+ export interface CreateOptions {
7
+ description?: string;
8
+ author?: string;
9
+ }
10
+
11
+ /**
12
+ * Scaffold a new plugin from template
13
+ */
14
+ export async function createPlugin(name: string, options: CreateOptions = {}): Promise<void> {
15
+ // Ensure name follows conventions
16
+ const pluginName = name.startsWith("omp-") ? name : `omp-${name}`;
17
+ const pluginDir = pluginName;
18
+
19
+ if (existsSync(pluginDir)) {
20
+ console.log(chalk.red(`Error: Directory ${pluginDir} already exists`));
21
+ return;
22
+ }
23
+
24
+ console.log(chalk.blue(`Creating plugin: ${pluginName}...`));
25
+
26
+ try {
27
+ // Create directory structure
28
+ await mkdir(pluginDir, { recursive: true });
29
+ await mkdir(join(pluginDir, "agents"), { recursive: true });
30
+ await mkdir(join(pluginDir, "tools"), { recursive: true });
31
+ await mkdir(join(pluginDir, "themes"), { recursive: true });
32
+ await mkdir(join(pluginDir, "commands"), { recursive: true });
33
+
34
+ // Create package.json
35
+ const packageJson = {
36
+ name: pluginName,
37
+ version: "0.1.0",
38
+ description: options.description || `A pi plugin`,
39
+ keywords: ["omp-plugin"],
40
+ author: options.author || "",
41
+ license: "MIT",
42
+ omp: {
43
+ install: [],
44
+ },
45
+ files: ["agents", "tools", "themes", "commands"],
46
+ };
47
+
48
+ await writeFile(join(pluginDir, "package.json"), JSON.stringify(packageJson, null, 2));
49
+
50
+ // Create README.md
51
+ const readme = `# ${pluginName}
52
+
53
+ ${options.description || "A pi plugin."}
54
+
55
+ ## Installation
56
+
57
+ \`\`\`bash
58
+ omp install ${pluginName}
59
+ \`\`\`
60
+
61
+ ## Contents
62
+
63
+ ### Agents
64
+
65
+ Add agent markdown files to \`agents/\` directory.
66
+
67
+ ### Tools
68
+
69
+ Add tool implementations to \`tools/\` directory.
70
+
71
+ ### Themes
72
+
73
+ Add theme JSON files to \`themes/\` directory.
74
+
75
+ ### Commands
76
+
77
+ Add command markdown files to \`commands/\` directory.
78
+
79
+ ## Configuration
80
+
81
+ Edit \`package.json\` to configure which files are installed:
82
+
83
+ \`\`\`json
84
+ {
85
+ "omp": {
86
+ "install": [
87
+ { "src": "agents/my-agent.md", "dest": "agent/agents/my-agent.md" },
88
+ { "src": "tools/my-tool/", "dest": "agent/tools/my-tool/" }
89
+ ]
90
+ }
91
+ }
92
+ \`\`\`
93
+
94
+ ## Publishing
95
+
96
+ 1. Update version in package.json
97
+ 2. Run \`npm publish\`
98
+
99
+ Users can then install with: \`omp install ${pluginName}\`
100
+
101
+ ## License
102
+
103
+ MIT
104
+ `;
105
+
106
+ await writeFile(join(pluginDir, "README.md"), readme);
107
+
108
+ // Create example agent
109
+ const exampleAgent = `# Example Agent
110
+
111
+ This is an example agent for ${pluginName}.
112
+
113
+ ## Description
114
+
115
+ Describe what this agent does.
116
+
117
+ ## Instructions
118
+
119
+ Provide instructions for the agent here.
120
+ `;
121
+
122
+ await writeFile(join(pluginDir, "agents", "example.md"), exampleAgent);
123
+
124
+ // Create .gitignore
125
+ const gitignore = `node_modules/
126
+ .DS_Store
127
+ *.log
128
+ `;
129
+ await writeFile(join(pluginDir, ".gitignore"), gitignore);
130
+
131
+ console.log(chalk.green(`\n✓ Created plugin at ${pluginDir}/`));
132
+ console.log();
133
+ console.log(chalk.dim("Directory structure:"));
134
+ console.log(chalk.dim(` ${pluginDir}/`));
135
+ console.log(chalk.dim(" ├── package.json"));
136
+ console.log(chalk.dim(" ├── README.md"));
137
+ console.log(chalk.dim(" ├── .gitignore"));
138
+ console.log(chalk.dim(" ├── agents/"));
139
+ console.log(chalk.dim(" │ └── example.md"));
140
+ console.log(chalk.dim(" ├── tools/"));
141
+ console.log(chalk.dim(" ├── themes/"));
142
+ console.log(chalk.dim(" └── commands/"));
143
+ console.log();
144
+ console.log(chalk.dim("Next steps:"));
145
+ console.log(chalk.dim(` 1. cd ${pluginDir}`));
146
+ console.log(chalk.dim(" 2. Add your agents, tools, themes, or commands"));
147
+ console.log(chalk.dim(" 3. Update omp.install in package.json"));
148
+ console.log(chalk.dim(" 4. Test locally: omp link ."));
149
+ console.log(chalk.dim(" 5. Publish: npm publish"));
150
+ } catch (err) {
151
+ console.log(chalk.red(`Error creating plugin: ${(err as Error).message}`));
152
+ }
153
+ }
@@ -0,0 +1,217 @@
1
+ import { existsSync } from "node:fs";
2
+ import chalk from "chalk";
3
+ import { detectAllConflicts, formatConflicts } from "../conflicts.js";
4
+ import { getInstalledPlugins, loadPluginsJson, readPluginPackageJson } from "../manifest.js";
5
+ import { GLOBAL_PACKAGE_JSON, NODE_MODULES_DIR, PLUGINS_DIR } from "../paths.js";
6
+ import { checkPluginSymlinks } from "../symlinks.js";
7
+
8
+ export interface DoctorOptions {
9
+ global?: boolean;
10
+ fix?: boolean;
11
+ json?: boolean;
12
+ }
13
+
14
+ interface DiagnosticResult {
15
+ check: string;
16
+ status: "ok" | "warning" | "error";
17
+ message: string;
18
+ fix?: string;
19
+ }
20
+
21
+ /**
22
+ * Run health checks on the plugin system
23
+ */
24
+ export async function runDoctor(options: DoctorOptions = {}): Promise<void> {
25
+ const isGlobal = options.global !== false;
26
+ const results: DiagnosticResult[] = [];
27
+
28
+ console.log(chalk.blue("Running health checks...\n"));
29
+
30
+ // 1. Check plugins directory exists
31
+ const pluginsDir = isGlobal ? PLUGINS_DIR : ".pi";
32
+ if (!existsSync(pluginsDir)) {
33
+ results.push({
34
+ check: "Plugins directory",
35
+ status: "warning",
36
+ message: `${pluginsDir} does not exist`,
37
+ fix: "Run: omp install <package>",
38
+ });
39
+ } else {
40
+ results.push({
41
+ check: "Plugins directory",
42
+ status: "ok",
43
+ message: pluginsDir,
44
+ });
45
+ }
46
+
47
+ // 2. Check package.json exists
48
+ const packageJsonPath = isGlobal ? GLOBAL_PACKAGE_JSON : ".pi/plugins.json";
49
+ if (!existsSync(packageJsonPath)) {
50
+ results.push({
51
+ check: "Package manifest",
52
+ status: "warning",
53
+ message: `${packageJsonPath} does not exist`,
54
+ fix: isGlobal ? "Run: omp install <package>" : "Run: omp init",
55
+ });
56
+ } else {
57
+ results.push({
58
+ check: "Package manifest",
59
+ status: "ok",
60
+ message: packageJsonPath,
61
+ });
62
+ }
63
+
64
+ // 3. Check node_modules exists
65
+ const nodeModules = isGlobal ? NODE_MODULES_DIR : ".pi/node_modules";
66
+ if (!existsSync(nodeModules)) {
67
+ results.push({
68
+ check: "Node modules",
69
+ status: "warning",
70
+ message: `${nodeModules} does not exist`,
71
+ });
72
+ } else {
73
+ results.push({
74
+ check: "Node modules",
75
+ status: "ok",
76
+ message: nodeModules,
77
+ });
78
+ }
79
+
80
+ // 4. Check each plugin's symlinks
81
+ const installedPlugins = await getInstalledPlugins(isGlobal);
82
+ const brokenSymlinks: string[] = [];
83
+ const missingSymlinks: string[] = [];
84
+
85
+ for (const [name, pkgJson] of installedPlugins) {
86
+ const symlinkStatus = await checkPluginSymlinks(name, pkgJson, isGlobal);
87
+
88
+ if (symlinkStatus.broken.length > 0) {
89
+ brokenSymlinks.push(...symlinkStatus.broken.map((s) => `${name}: ${s}`));
90
+ }
91
+ if (symlinkStatus.missing.length > 0) {
92
+ missingSymlinks.push(...symlinkStatus.missing.map((s) => `${name}: ${s}`));
93
+ }
94
+ }
95
+
96
+ if (brokenSymlinks.length > 0) {
97
+ results.push({
98
+ check: "Broken symlinks",
99
+ status: "error",
100
+ message: `${brokenSymlinks.length} broken symlink(s)`,
101
+ fix: "Run: omp update <plugin> to re-create symlinks",
102
+ });
103
+ } else {
104
+ results.push({
105
+ check: "Symlinks",
106
+ status: "ok",
107
+ message: "All symlinks valid",
108
+ });
109
+ }
110
+
111
+ if (missingSymlinks.length > 0) {
112
+ results.push({
113
+ check: "Missing symlinks",
114
+ status: "warning",
115
+ message: `${missingSymlinks.length} expected symlink(s) not found`,
116
+ fix: "Run: omp update <plugin> to re-create symlinks",
117
+ });
118
+ }
119
+
120
+ // 5. Check for conflicts
121
+ const conflicts = detectAllConflicts(installedPlugins);
122
+ if (conflicts.length > 0) {
123
+ results.push({
124
+ check: "Conflicts",
125
+ status: "warning",
126
+ message: formatConflicts(conflicts).join("; "),
127
+ });
128
+ } else {
129
+ results.push({
130
+ check: "Conflicts",
131
+ status: "ok",
132
+ message: "No conflicts detected",
133
+ });
134
+ }
135
+
136
+ // 6. Check for orphaned entries in package.json
137
+ const pluginsJson = await loadPluginsJson(isGlobal);
138
+ const orphaned: string[] = [];
139
+ for (const name of Object.keys(pluginsJson.plugins)) {
140
+ const pkgJson = await readPluginPackageJson(name, isGlobal);
141
+ if (!pkgJson) {
142
+ orphaned.push(name);
143
+ }
144
+ }
145
+
146
+ if (orphaned.length > 0) {
147
+ results.push({
148
+ check: "Orphaned entries",
149
+ status: "warning",
150
+ message: `${orphaned.length} plugin(s) in manifest but not in node_modules: ${orphaned.join(", ")}`,
151
+ fix: "Run: omp install (to reinstall) or remove from manifest",
152
+ });
153
+ }
154
+
155
+ // Output results
156
+ if (options.json) {
157
+ console.log(JSON.stringify({ results }, null, 2));
158
+ return;
159
+ }
160
+
161
+ for (const result of results) {
162
+ let icon: string;
163
+ let color: typeof chalk;
164
+
165
+ switch (result.status) {
166
+ case "ok":
167
+ icon = "✓";
168
+ color = chalk.green;
169
+ break;
170
+ case "warning":
171
+ icon = "⚠";
172
+ color = chalk.yellow;
173
+ break;
174
+ case "error":
175
+ icon = "✗";
176
+ color = chalk.red;
177
+ break;
178
+ }
179
+
180
+ console.log(color(`${icon} ${result.check}: `) + result.message);
181
+
182
+ if (result.fix && result.status !== "ok") {
183
+ console.log(chalk.dim(` ${result.fix}`));
184
+ }
185
+ }
186
+
187
+ // Summary
188
+ const errors = results.filter((r) => r.status === "error");
189
+ const warnings = results.filter((r) => r.status === "warning");
190
+
191
+ console.log();
192
+ if (errors.length === 0 && warnings.length === 0) {
193
+ console.log(chalk.green("✓ All checks passed!"));
194
+ } else {
195
+ if (errors.length > 0) {
196
+ console.log(chalk.red(`${errors.length} error(s) found`));
197
+ }
198
+ if (warnings.length > 0) {
199
+ console.log(chalk.yellow(`${warnings.length} warning(s) found`));
200
+ }
201
+ }
202
+
203
+ // Show broken symlinks details
204
+ if (brokenSymlinks.length > 0) {
205
+ console.log(chalk.red("\nBroken symlinks:"));
206
+ for (const s of brokenSymlinks) {
207
+ console.log(chalk.dim(` - ${s}`));
208
+ }
209
+ }
210
+
211
+ if (missingSymlinks.length > 0) {
212
+ console.log(chalk.yellow("\nMissing symlinks:"));
213
+ for (const s of missingSymlinks) {
214
+ console.log(chalk.dim(` - ${s}`));
215
+ }
216
+ }
217
+ }