@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,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
+ }