@oh-my-pi/cli 0.1.0 → 0.3.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 (105) hide show
  1. package/.github/icon.png +0 -0
  2. package/.github/logo.png +0 -0
  3. package/.github/workflows/publish.yml +1 -1
  4. package/LICENSE +21 -0
  5. package/README.md +243 -138
  6. package/biome.json +1 -1
  7. package/bun.lock +59 -0
  8. package/dist/cli.js +6311 -2900
  9. package/dist/commands/config.d.ts +32 -0
  10. package/dist/commands/config.d.ts.map +1 -0
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/doctor.d.ts +1 -0
  13. package/dist/commands/doctor.d.ts.map +1 -1
  14. package/dist/commands/enable.d.ts +1 -0
  15. package/dist/commands/enable.d.ts.map +1 -1
  16. package/dist/commands/env.d.ts +14 -0
  17. package/dist/commands/env.d.ts.map +1 -0
  18. package/dist/commands/features.d.ts +25 -0
  19. package/dist/commands/features.d.ts.map +1 -0
  20. package/dist/commands/info.d.ts +1 -0
  21. package/dist/commands/info.d.ts.map +1 -1
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/install.d.ts +37 -0
  24. package/dist/commands/install.d.ts.map +1 -1
  25. package/dist/commands/link.d.ts +2 -0
  26. package/dist/commands/link.d.ts.map +1 -1
  27. package/dist/commands/list.d.ts +1 -0
  28. package/dist/commands/list.d.ts.map +1 -1
  29. package/dist/commands/outdated.d.ts +1 -0
  30. package/dist/commands/outdated.d.ts.map +1 -1
  31. package/dist/commands/search.d.ts.map +1 -1
  32. package/dist/commands/uninstall.d.ts +1 -0
  33. package/dist/commands/uninstall.d.ts.map +1 -1
  34. package/dist/commands/update.d.ts +1 -0
  35. package/dist/commands/update.d.ts.map +1 -1
  36. package/dist/commands/why.d.ts +1 -0
  37. package/dist/commands/why.d.ts.map +1 -1
  38. package/dist/conflicts.d.ts +9 -1
  39. package/dist/conflicts.d.ts.map +1 -1
  40. package/dist/errors.d.ts +8 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/index.d.ts +18 -19
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/lock.d.ts +3 -0
  45. package/dist/lock.d.ts.map +1 -0
  46. package/dist/lockfile.d.ts +52 -0
  47. package/dist/lockfile.d.ts.map +1 -0
  48. package/dist/manifest.d.ts +60 -25
  49. package/dist/manifest.d.ts.map +1 -1
  50. package/dist/npm.d.ts +14 -2
  51. package/dist/npm.d.ts.map +1 -1
  52. package/dist/paths.d.ts +34 -3
  53. package/dist/paths.d.ts.map +1 -1
  54. package/dist/runtime.d.ts +14 -0
  55. package/dist/runtime.d.ts.map +1 -0
  56. package/dist/symlinks.d.ts +43 -7
  57. package/dist/symlinks.d.ts.map +1 -1
  58. package/package.json +11 -5
  59. package/plugins/exa/README.md +153 -0
  60. package/plugins/exa/package.json +56 -0
  61. package/plugins/exa/tools/exa/company.ts +35 -0
  62. package/plugins/exa/tools/exa/index.ts +66 -0
  63. package/plugins/exa/tools/exa/linkedin.ts +35 -0
  64. package/plugins/exa/tools/exa/researcher.ts +40 -0
  65. package/plugins/exa/tools/exa/runtime.json +4 -0
  66. package/plugins/exa/tools/exa/search.ts +46 -0
  67. package/plugins/exa/tools/exa/shared.ts +230 -0
  68. package/plugins/exa/tools/exa/websets.ts +62 -0
  69. package/plugins/metal-theme/package.json +7 -2
  70. package/plugins/subagents/package.json +7 -2
  71. package/plugins/user-prompt/README.md +130 -0
  72. package/plugins/user-prompt/package.json +19 -0
  73. package/plugins/user-prompt/tools/user-prompt/index.ts +235 -0
  74. package/src/cli.ts +133 -58
  75. package/src/commands/config.ts +384 -0
  76. package/src/commands/create.ts +51 -1
  77. package/src/commands/doctor.ts +95 -7
  78. package/src/commands/enable.ts +25 -8
  79. package/src/commands/env.ts +38 -0
  80. package/src/commands/features.ts +295 -0
  81. package/src/commands/info.ts +41 -5
  82. package/src/commands/init.ts +20 -2
  83. package/src/commands/install.ts +453 -80
  84. package/src/commands/link.ts +60 -9
  85. package/src/commands/list.ts +122 -7
  86. package/src/commands/outdated.ts +17 -6
  87. package/src/commands/search.ts +20 -3
  88. package/src/commands/uninstall.ts +57 -6
  89. package/src/commands/update.ts +67 -9
  90. package/src/commands/why.ts +47 -16
  91. package/src/conflicts.ts +33 -1
  92. package/src/errors.ts +22 -0
  93. package/src/index.ts +18 -25
  94. package/src/lock.ts +46 -0
  95. package/src/lockfile.ts +132 -0
  96. package/src/manifest.ts +219 -71
  97. package/src/npm.ts +74 -18
  98. package/src/paths.ts +77 -12
  99. package/src/runtime.ts +116 -0
  100. package/src/symlinks.ts +291 -35
  101. package/tsconfig.json +7 -3
  102. package/CHECK.md +0 -352
  103. package/dist/migrate.d.ts +0 -9
  104. package/dist/migrate.d.ts.map +0 -1
  105. package/src/migrate.ts +0 -181
@@ -0,0 +1,235 @@
1
+ /**
2
+ * User Prompt Tool - Ask questions to the user during execution
3
+ *
4
+ * Use this tool when you need to ask the user questions during execution.
5
+ * This allows you to:
6
+ * 1. Gather user preferences or requirements
7
+ * 2. Clarify ambiguous instructions
8
+ * 3. Get decisions on implementation choices as you work
9
+ * 4. Offer choices to the user about what direction to take
10
+ *
11
+ * Usage notes:
12
+ * - Users will always be able to select "Other" to provide custom text input
13
+ * - Use multi: true to allow multiple answers to be selected for a question
14
+ * - If you recommend a specific option, make that the first option in the list
15
+ * and add "(Recommended)" at the end of the label
16
+ */
17
+
18
+ import { Type } from "@sinclair/typebox";
19
+ import { Text } from "@mariozechner/pi-tui";
20
+ import type { CustomAgentTool, CustomToolFactory, ToolAPI } from "@mariozechner/pi-coding-agent";
21
+
22
+ // =============================================================================
23
+ // Tool Definition
24
+ // =============================================================================
25
+
26
+ const OTHER_OPTION = "Other (type your own)";
27
+
28
+ const OptionItem = Type.Object({
29
+ label: Type.String({ description: "Display label for this option" }),
30
+ });
31
+
32
+ const UserPromptParams = Type.Object({
33
+ question: Type.String({ description: "The question to ask the user" }),
34
+ options: Type.Array(OptionItem, {
35
+ description: "Available options for the user to choose from.",
36
+ minItems: 1,
37
+ }),
38
+ multi: Type.Optional(Type.Boolean({
39
+ description: "Allow multiple options to be selected (default: false)",
40
+ default: false,
41
+ })),
42
+ });
43
+
44
+ interface UserPromptDetails {
45
+ question: string;
46
+ options: string[];
47
+ multi: boolean;
48
+ selectedOptions: string[];
49
+ customInput?: string;
50
+ }
51
+
52
+ const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
53
+ 1. Gather user preferences or requirements
54
+ 2. Clarify ambiguous instructions
55
+ 3. Get decisions on implementation choices as you work
56
+ 4. Offer choices to the user about what direction to take.
57
+
58
+ Usage notes:
59
+ - Users will always be able to select "Other" to provide custom text input
60
+ - Use multi: true to allow multiple answers to be selected for a question
61
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
62
+
63
+ Example usage:
64
+
65
+ <example>
66
+ assistant: Let me ask which features you want to include.
67
+ assistant: Uses the user_prompt tool:
68
+ {
69
+ "question": "Which features should I implement?",
70
+ "options": [
71
+ {"label": "Authentication"},
72
+ {"label": "API endpoints"},
73
+ {"label": "Database models"},
74
+ {"label": "Unit tests"},
75
+ {"label": "Documentation"}
76
+ ],
77
+ "multi": true
78
+ }
79
+ </example>`;
80
+
81
+ const factory: CustomToolFactory = (pi: ToolAPI) => {
82
+ const tool: CustomAgentTool<typeof UserPromptParams, UserPromptDetails> = {
83
+ name: "user_prompt",
84
+ label: "User Prompt",
85
+ description: DESCRIPTION,
86
+ parameters: UserPromptParams,
87
+
88
+ async execute(_toolCallId, params, _signal, _onUpdate) {
89
+ const { question, options, multi = false } = params;
90
+ const optionLabels = options.map((o) => o.label);
91
+
92
+ if (!pi.hasUI) {
93
+ return {
94
+ content: [{ type: "text", text: "Error: User prompt requires interactive mode" }],
95
+ details: { question, options: optionLabels, multi, selectedOptions: [] },
96
+ };
97
+ }
98
+
99
+ let selectedOptions: string[] = [];
100
+ let customInput: string | undefined;
101
+
102
+ if (multi) {
103
+ // Multi-select: show checkboxes in the label to indicate selection state
104
+ const DONE = "✓ Done selecting";
105
+ const selected = new Set<string>();
106
+
107
+ while (true) {
108
+ // Build options with checkbox indicators
109
+ const opts: string[] = [];
110
+
111
+ // Add "Done" option if any selected
112
+ if (selected.size > 0) {
113
+ opts.push(DONE);
114
+ }
115
+
116
+ // Add all options with [X] or [ ] prefix
117
+ for (const opt of optionLabels) {
118
+ const checkbox = selected.has(opt) ? "[X]" : "[ ]";
119
+ opts.push(`${checkbox} ${opt}`);
120
+ }
121
+
122
+ // Add "Other" option
123
+ opts.push(OTHER_OPTION);
124
+
125
+ const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
126
+ const choice = await pi.ui.select(`${prefix}${question}`, opts);
127
+
128
+ if (choice === null || choice === DONE) break;
129
+
130
+ if (choice === OTHER_OPTION) {
131
+ const input = await pi.ui.input("Enter your response:");
132
+ if (input) customInput = input;
133
+ break;
134
+ }
135
+
136
+ // Toggle selection - extract the actual option name
137
+ const optMatch = choice.match(/^\[.\] (.+)$/);
138
+ if (optMatch) {
139
+ const opt = optMatch[1];
140
+ if (selected.has(opt)) {
141
+ selected.delete(opt);
142
+ } else {
143
+ selected.add(opt);
144
+ }
145
+ }
146
+ }
147
+ selectedOptions = Array.from(selected);
148
+ } else {
149
+ // Single select with "Other" option
150
+ const choice = await pi.ui.select(question, [...optionLabels, OTHER_OPTION]);
151
+ if (choice === OTHER_OPTION) {
152
+ const input = await pi.ui.input("Enter your response:");
153
+ if (input) customInput = input;
154
+ } else if (choice) {
155
+ selectedOptions = [choice];
156
+ }
157
+ }
158
+
159
+ const details: UserPromptDetails = {
160
+ question,
161
+ options: optionLabels,
162
+ multi,
163
+ selectedOptions,
164
+ customInput,
165
+ };
166
+
167
+ let responseText: string;
168
+ if (customInput) {
169
+ responseText = `User provided custom input: ${customInput}`;
170
+ } else if (selectedOptions.length > 0) {
171
+ responseText = multi
172
+ ? `User selected: ${selectedOptions.join(", ")}`
173
+ : `User selected: ${selectedOptions[0]}`;
174
+ } else {
175
+ responseText = "User cancelled the selection";
176
+ }
177
+
178
+ return { content: [{ type: "text", text: responseText }], details };
179
+ },
180
+
181
+ renderCall(args, t) {
182
+ if (!args.question) {
183
+ return new Text(t.fg("error", "user_prompt: no question provided"), 0, 0);
184
+ }
185
+
186
+ const multiTag = args.multi ? t.fg("muted", " [multi-select]") : "";
187
+ let text = t.fg("toolTitle", "? ") + t.fg("accent", args.question) + multiTag;
188
+
189
+ if (args.options?.length) {
190
+ for (const opt of args.options) {
191
+ text += "\n" + t.fg("dim", " ○ ") + t.fg("muted", opt.label);
192
+ }
193
+ text += "\n" + t.fg("dim", " ○ ") + t.fg("muted", "Other (custom input)");
194
+ }
195
+
196
+ return new Text(text, 0, 0);
197
+ },
198
+
199
+ renderResult(result, { expanded }, t) {
200
+ const { details } = result;
201
+ if (!details) {
202
+ const txt = result.content[0];
203
+ return new Text(txt?.type === "text" ? txt.text : "", 0, 0);
204
+ }
205
+
206
+ let text = t.fg("toolTitle", "? ") + t.fg("accent", details.question);
207
+
208
+ if (details.customInput) {
209
+ // Custom input provided
210
+ text += "\n" + t.fg("dim", " ⎿ ") + t.fg("success", details.customInput);
211
+ } else if (details.selectedOptions.length > 0) {
212
+ // Show only selected options
213
+ const selected = details.selectedOptions;
214
+ if (selected.length === 1) {
215
+ text += "\n" + t.fg("dim", " ⎿ ") + t.fg("success", selected[0]);
216
+ } else {
217
+ // Multiple selections
218
+ for (let i = 0; i < selected.length; i++) {
219
+ const isLast = i === selected.length - 1;
220
+ const branch = isLast ? "└─" : "├─";
221
+ text += "\n" + t.fg("dim", ` ${branch} `) + t.fg("success", selected[i]);
222
+ }
223
+ }
224
+ } else {
225
+ text += "\n" + t.fg("dim", " ⎿ ") + t.fg("warning", "Cancelled");
226
+ }
227
+
228
+ return new Text(text, 0, 0);
229
+ },
230
+ };
231
+
232
+ return tool;
233
+ };
234
+
235
+ export default factory;
package/src/cli.ts CHANGED
@@ -1,30 +1,34 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { configCommand } from "@omp/commands/config";
4
+ import { createPlugin } from "@omp/commands/create";
5
+ import { runDoctor } from "@omp/commands/doctor";
6
+ import { disablePlugin, enablePlugin } from "@omp/commands/enable";
7
+ import { envCommand } from "@omp/commands/env";
8
+ import { featuresCommand } from "@omp/commands/features";
9
+ import { showInfo } from "@omp/commands/info";
10
+ import { initProject } from "@omp/commands/init";
11
+ import { installPlugin } from "@omp/commands/install";
12
+ import { linkPlugin } from "@omp/commands/link";
13
+ import { listPlugins } from "@omp/commands/list";
14
+ import { showOutdated } from "@omp/commands/outdated";
15
+ import { searchPlugins } from "@omp/commands/search";
16
+ import { uninstallPlugin } from "@omp/commands/uninstall";
17
+ import { updatePlugin } from "@omp/commands/update";
18
+ import { whyFile } from "@omp/commands/why";
19
+ import { withErrorHandling } from "@omp/errors";
20
+ import { checkNpmAvailable } from "@omp/npm";
21
+ import chalk from "chalk";
3
22
  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
- });
23
+
24
+ // Check npm availability at startup
25
+ const npmCheck = checkNpmAvailable();
26
+ if (!npmCheck.available) {
27
+ console.log(chalk.red(npmCheck.error));
28
+ process.exit(1);
29
+ }
30
+
31
+ program.name("omp").description("Oh My Pi - Plugin manager for pi configuration").version("0.2.0");
28
32
 
29
33
  // ============================================================================
30
34
  // Core Commands
@@ -38,43 +42,50 @@ program
38
42
  "after",
39
43
  `
40
44
  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
45
+ $ omp install @oh-my-pi/subagents # Install from npm (all features)
46
+ $ omp install @oh-my-pi/exa[search] # Install with specific features
47
+ $ omp install @oh-my-pi/exa[search,websets] # Multiple features
48
+ $ omp install @oh-my-pi/exa[*] # Explicitly all features
49
+ $ omp install @oh-my-pi/exa[] # No optional features (core only)
50
+ $ omp install @oh-my-pi/subagents@^2.0.0 # Specific version range
51
+ $ omp install ./local/path # Local directory (copies)
52
+ $ omp install # Install all from plugins.json
46
53
  `,
47
54
  )
48
- .option("-g, --global", "Install globally to ~/.pi (default)")
55
+ .option("-g, --global", "Install globally to ~/.pi")
56
+ .option("-l, --local", "Install to project-local .pi/")
49
57
  .option("-S, --save", "Add to plugins.json")
50
58
  .option("-D, --save-dev", "Add as dev dependency")
51
59
  .option("--force", "Overwrite conflicts without prompting")
52
60
  .option("--json", "Output as JSON")
53
- .action(installPlugin);
61
+ .action(withErrorHandling(installPlugin));
54
62
 
55
63
  program
56
64
  .command("uninstall <name>")
57
65
  .alias("rm")
58
66
  .description("Remove plugin and its symlinks")
59
- .option("-g, --global", "Uninstall from ~/.pi (default)")
67
+ .option("-g, --global", "Uninstall from ~/.pi")
68
+ .option("-l, --local", "Uninstall from project-local .pi/")
60
69
  .option("--json", "Output as JSON")
61
- .action(uninstallPlugin);
70
+ .action(withErrorHandling(uninstallPlugin));
62
71
 
63
72
  program
64
73
  .command("update [name]")
65
74
  .alias("up")
66
75
  .description("Update to latest within semver range")
67
- .option("-g, --global", "Update global plugins (default)")
76
+ .option("-g, --global", "Update global plugins")
77
+ .option("-l, --local", "Update project-local plugins")
68
78
  .option("--json", "Output as JSON")
69
- .action(updatePlugin);
79
+ .action(withErrorHandling(updatePlugin));
70
80
 
71
81
  program
72
82
  .command("list")
73
83
  .alias("ls")
74
84
  .description("Show installed plugins")
75
- .option("-g, --global", "List global plugins (default)")
85
+ .option("-g, --global", "List global plugins")
86
+ .option("-l, --local", "List project-local plugins")
76
87
  .option("--json", "Output as JSON")
77
- .action(listPlugins);
88
+ .action(withErrorHandling(listPlugins));
78
89
 
79
90
  program
80
91
  .command("link <path>")
@@ -87,8 +98,10 @@ so changes are reflected immediately without reinstalling.
87
98
  `,
88
99
  )
89
100
  .option("-n, --name <name>", "Custom name for the plugin")
90
- .option("-g, --global", "Link globally (default)")
91
- .action(linkPlugin);
101
+ .option("-g, --global", "Link globally")
102
+ .option("-l, --local", "Link to project-local .pi/")
103
+ .option("--force", "Overwrite existing npm-installed plugin")
104
+ .action(withErrorHandling(linkPlugin));
92
105
 
93
106
  // ============================================================================
94
107
  // New Commands
@@ -98,70 +111,132 @@ program
98
111
  .command("init")
99
112
  .description("Create .pi/plugins.json in current project")
100
113
  .option("--force", "Overwrite existing plugins.json")
101
- .action(initProject);
114
+ .action(withErrorHandling(initProject));
102
115
 
103
116
  program
104
117
  .command("search <query>")
105
118
  .description("Search npm for omp-plugin keyword")
106
119
  .option("--json", "Output as JSON")
107
120
  .option("--limit <n>", "Maximum results to show", "20")
108
- .action((query, options) => searchPlugins(query, { ...options, limit: parseInt(options.limit, 10) }));
121
+ .action(
122
+ withErrorHandling((query, options) => searchPlugins(query, { ...options, limit: parseInt(options.limit, 10) })),
123
+ );
109
124
 
110
125
  program
111
126
  .command("info <package>")
112
127
  .description("Show plugin details before install")
113
128
  .option("--json", "Output as JSON")
114
129
  .option("--versions", "Show available versions")
115
- .action(showInfo);
130
+ .option("--all-versions", "Show all published versions")
131
+ .action(withErrorHandling(showInfo));
116
132
 
117
133
  program
118
134
  .command("outdated")
119
135
  .description("List plugins with newer versions")
120
- .option("-g, --global", "Check global plugins (default)")
136
+ .option("-g, --global", "Check global plugins")
137
+ .option("-l, --local", "Check project-local plugins")
121
138
  .option("--json", "Output as JSON")
122
- .action(showOutdated);
139
+ .action(withErrorHandling(showOutdated));
123
140
 
124
141
  program
125
142
  .command("doctor")
126
143
  .description("Check for broken symlinks, conflicts")
127
- .option("-g, --global", "Check global plugins (default)")
144
+ .option("-g, --global", "Check global plugins")
145
+ .option("-l, --local", "Check project-local plugins")
128
146
  .option("--fix", "Attempt to fix issues")
129
147
  .option("--json", "Output as JSON")
130
- .action(runDoctor);
148
+ .action(withErrorHandling(runDoctor));
131
149
 
132
150
  program
133
151
  .command("create <name>")
134
152
  .description("Scaffold new plugin from template")
135
153
  .option("-d, --description <desc>", "Plugin description")
136
154
  .option("-a, --author <author>", "Plugin author")
137
- .action(createPlugin);
155
+ .action(withErrorHandling(createPlugin));
138
156
 
139
157
  program
140
158
  .command("why <file>")
141
159
  .description("Show which plugin installed a file")
142
- .option("-g, --global", "Check global plugins (default)")
160
+ .option("-g, --global", "Check global plugins")
161
+ .option("-l, --local", "Check project-local plugins")
143
162
  .option("--json", "Output as JSON")
144
- .action(whyFile);
163
+ .action(withErrorHandling(whyFile));
145
164
 
146
165
  program
147
166
  .command("enable <name>")
148
167
  .description("Enable a disabled plugin")
149
- .option("-g, --global", "Target global plugins (default)")
168
+ .option("-g, --global", "Target global plugins")
169
+ .option("-l, --local", "Target project-local plugins")
150
170
  .option("--json", "Output as JSON")
151
- .action(enablePlugin);
171
+ .action(withErrorHandling(enablePlugin));
152
172
 
153
173
  program
154
174
  .command("disable <name>")
155
175
  .description("Disable plugin without uninstalling")
156
- .option("-g, --global", "Target global plugins (default)")
176
+ .option("-g, --global", "Target global plugins")
177
+ .option("-l, --local", "Target project-local plugins")
178
+ .option("--json", "Output as JSON")
179
+ .action(withErrorHandling(disablePlugin));
180
+
181
+ program
182
+ .command("features <name>")
183
+ .description("List or configure plugin features")
184
+ .addHelpText(
185
+ "after",
186
+ `
187
+ Examples:
188
+ $ omp features @oh-my-pi/exa # List available features
189
+ $ omp features @oh-my-pi/exa --enable websets # Enable a feature
190
+ $ omp features @oh-my-pi/exa --disable search # Disable a feature
191
+ $ omp features @oh-my-pi/exa --set search,websets # Set exact features
192
+ $ omp features @oh-my-pi/exa --set '*' # Enable all features
193
+ $ omp features @oh-my-pi/exa --set '' # Disable all optional features
194
+ `,
195
+ )
196
+ .option("-g, --global", "Target global plugins")
197
+ .option("-l, --local", "Target project-local plugins")
198
+ .option("--enable <features...>", "Enable specific features")
199
+ .option("--disable <features...>", "Disable specific features")
200
+ .option("--set <features>", "Set exact feature list (comma-separated, '*' for all, '' for none)")
157
201
  .option("--json", "Output as JSON")
158
- .action(disablePlugin);
202
+ .action(withErrorHandling(featuresCommand));
159
203
 
160
204
  program
161
- .command("migrate")
162
- .description("Migrate from legacy manifest.json to npm-native format")
163
- .action(async () => {
164
- await migrateToNpm();
165
- });
205
+ .command("config <name> [key] [value]")
206
+ .description("Get or set plugin configuration variables")
207
+ .addHelpText(
208
+ "after",
209
+ `
210
+ Examples:
211
+ $ omp config @oh-my-pi/exa # List all variables
212
+ $ omp config @oh-my-pi/exa apiKey # Get value of apiKey
213
+ $ omp config @oh-my-pi/exa apiKey sk-xxx # Set apiKey to sk-xxx
214
+ $ omp config @oh-my-pi/exa apiKey --delete # Reset apiKey to default
215
+ `,
216
+ )
217
+ .option("-g, --global", "Target global plugins")
218
+ .option("-l, --local", "Target project-local plugins")
219
+ .option("--delete", "Delete/reset the variable to its default")
220
+ .option("--json", "Output as JSON")
221
+ .action(withErrorHandling(configCommand));
222
+
223
+ program
224
+ .command("env")
225
+ .description("Print plugin environment variables for shell eval")
226
+ .addHelpText(
227
+ "after",
228
+ `
229
+ Examples:
230
+ $ eval "$(omp env)" # Load env vars in current shell
231
+ $ omp env >> ~/.bashrc # Persist to shell config
232
+ $ omp env --fish | source # Fish shell syntax
233
+ $ omp env --json # JSON format for scripts
234
+ `,
235
+ )
236
+ .option("-g, --global", "Target global plugins")
237
+ .option("-l, --local", "Target project-local plugins")
238
+ .option("--fish", "Output fish shell syntax instead of POSIX")
239
+ .option("--json", "Output as JSON")
240
+ .action(withErrorHandling(envCommand));
166
241
 
167
242
  program.parse();