pi-extmgr 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-extmgr
2
2
 
3
- <img width="2560" height="369" alt="image" src="https://i.imgur.com/7ifGp5x.png" />
3
+ <img width="2560" height="369" alt="image" src="https://i.imgur.com/bVM7ZcO.png" />
4
4
 
5
5
  [![CI](https://github.com/ayagmar/pi-extmgr/actions/workflows/ci.yml/badge.svg)](https://github.com/ayagmar/pi-extmgr/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -13,16 +13,34 @@ A better way to manage Pi extensions. Browse, install, enable/disable, and remov
13
13
  pi install npm:pi-extmgr
14
14
  ```
15
15
 
16
- Then reload Pi with `/reload`.
17
-
18
- ## What it does
19
-
20
- - **Unified view**: See all your local extensions and installed npm/git packages in one list
21
- - **Toggle extensions**: Enable/disable local extensions with Space/Enter, save with `S`
22
- - **Package actions**: Update or remove installed packages with `A`
23
- - **Browse community**: Search and install from npm (`R` to browse)
24
- - **History tracking**: See what you've changed with `/extensions history`
25
- - **Smart reload prompt**: After applying extension state changes, extmgr can trigger reload directly from the UI
16
+ Then reload Pi.
17
+
18
+ ## Features
19
+
20
+ - **Unified manager UI**
21
+ - Local extensions (`~/.pi/agent/extensions`, `.pi/extensions`) and installed packages in one list
22
+ - Scope indicators (global/project), status indicators, update badges
23
+ - **Safe staged local extension toggles**
24
+ - Toggle with `Space/Enter`, apply with `S`
25
+ - Unsaved-change guard when leaving (save/discard/stay)
26
+ - **Package management**
27
+ - Install, update, remove from UI and command line
28
+ - Quick actions (`A`, `u`, `X`) and bulk update (`U`)
29
+ - **Remote discovery and install**
30
+ - npm search/browse with pagination
31
+ - Install by source (`npm:`, `git:`, URL, local path)
32
+ - Supports direct GitHub `.ts` installs and local standalone install mode
33
+ - **Auto-update**
34
+ - Interactive wizard (`t` in manager, or `/extensions auto-update`)
35
+ - Persistent schedule restored on startup and session switch
36
+ - Background checks + status bar updates
37
+ - **Operational visibility**
38
+ - Session history (`/extensions history`)
39
+ - Cache controls (`/extensions clear-cache`)
40
+ - Status line summary (`pkg count • auto-update • known updates`)
41
+ - **Interactive + non-interactive support**
42
+ - Works in TUI and non-UI modes
43
+ - Non-interactive commands for list/install/remove/update/auto-update
26
44
 
27
45
  ## Usage
28
46
 
@@ -41,11 +59,12 @@ Open the manager:
41
59
  | `S` | Save changes |
42
60
  | `Enter` / `A` | Actions on selected package (update/remove/view) |
43
61
  | `u` | Update selected package directly |
44
- | `X` | Remove selected package directly |
62
+ | `X` | Remove selected item (package/local extension) |
45
63
  | `i` | Quick install by source |
46
64
  | `f` | Quick search |
47
65
  | `U` | Update all packages |
48
66
  | `t` | Auto-update wizard |
67
+ | `P` / `M` | Quick actions palette |
49
68
  | `R` | Browse remote packages |
50
69
  | `?` / `H` | Help |
51
70
  | `Esc` | Exit |
@@ -53,17 +72,51 @@ Open the manager:
53
72
  ### Commands
54
73
 
55
74
  ```bash
56
- /extensions # Open the manager
57
- /extensions search <query> # Search npm
58
- /extensions install <source> # Install package
75
+ /extensions # Open interactive manager (default)
76
+ /extensions local # Alias: open interactive manager
77
+ /extensions list # List local extensions
78
+ /extensions remote # Open remote package browser
79
+ /extensions packages # Alias: remote browser
80
+ /extensions installed # Installed packages view (legacy alias to unified flow)
81
+ /extensions search <query> # Search npm packages
82
+ /extensions install <source> [--project|--global] # Install package
59
83
  /extensions remove [source] # Remove package
84
+ /extensions uninstall [source] # Alias: remove
60
85
  /extensions update [source] # Update one package (or all when omitted)
61
- /extensions auto-update [every] # No arg opens wizard; accepts 1d, 1w, never, etc.
62
- /extensions history # View change history
63
- /extensions stats # View statistics
86
+ /extensions auto-update [every] # No arg opens wizard in UI; accepts 1d, 1w, never, etc.
87
+ /extensions history [options] # View change history (supports filters)
64
88
  /extensions clear-cache # Clear metadata cache
65
89
  ```
66
90
 
91
+ ### Non-interactive mode
92
+
93
+ When Pi is running without UI, extmgr still supports command-driven workflows:
94
+
95
+ - `/extensions list`
96
+ - `/extensions installed`
97
+ - `/extensions install <source> [--project|--global]`
98
+ - `/extensions remove <source>`
99
+ - `/extensions update [source]`
100
+ - `/extensions history [options]`
101
+ - `/extensions auto-update <duration>`
102
+
103
+ Remote browsing/search menus require interactive mode.
104
+
105
+ History options (works in non-interactive mode too):
106
+
107
+ - `--limit <n>`
108
+ - `--action <extension_toggle|package_install|package_update|package_remove|cache_clear>`
109
+ - `--success` / `--failed`
110
+ - `--package <query>`
111
+ - `--since <duration>` (e.g. `30m`, `24h`, `7d`, `1mo`)
112
+ - `--global` (non-interactive mode only; reads all persisted sessions)
113
+
114
+ Examples:
115
+
116
+ - `/extensions history --failed --limit 50`
117
+ - `/extensions history --action package_update --since 7d`
118
+ - `/extensions history --global --package extmgr --since 24h`
119
+
67
120
  ### Install sources
68
121
 
69
122
  ```bash
@@ -81,13 +134,9 @@ Open the manager:
81
134
  - **Two install modes**:
82
135
  - **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache
83
136
  - **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`, supports multi-file extensions
84
- - **Auto-update schedule is persistent**: `/extensions auto-update 1d` stays active across future Pi sessions.
137
+ - **Auto-update schedule is persistent**: `/extensions auto-update 1d` stays active across future Pi sessions and is restored when switching sessions.
85
138
  - **Reload is built-in**: When extmgr asks to reload, it calls `ctx.reload()` directly.
86
- - **Remove requires restart**: After removing a package, you need to fully restart Pi (not just `/reload`) for it to be completely unloaded.
87
-
88
- ## Keyboard shortcut
89
-
90
- Press `Ctrl+Shift+E` anywhere to open the extension manager.
139
+ - **Remove requires restart**: After removing a package, you need to fully restart Pi (not just a reload) for it to be completely unloaded.
91
140
 
92
141
  ## License
93
142
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extmgr",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Enhanced UX for managing local Pi extensions and community packages",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -34,8 +34,8 @@
34
34
  "extensions": [
35
35
  "./src/index.ts"
36
36
  ],
37
- "video": "https://github.com/ayagmar/pi-extmgr/releases/download/v0.0.2/Screencast_20260207_013142.mp4",
38
- "image": "https://i.imgur.com/7ifGp5x.png"
37
+ "video": "https://github.com/ayagmar/pi-extmgr/releases/download/demo/demo.mp4",
38
+ "image": "https://i.imgur.com/bVM7ZcO.png"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@mariozechner/pi-coding-agent": "*",
@@ -56,6 +56,9 @@
56
56
  },
57
57
  "author": "ayagmar",
58
58
  "license": "MIT",
59
+ "engines": {
60
+ "node": ">=22"
61
+ },
59
62
  "repository": {
60
63
  "type": "git",
61
64
  "url": "https://github.com/ayagmar/pi-extmgr.git"
@@ -0,0 +1,80 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ ExtensionContext,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import {
7
+ disableAutoUpdate,
8
+ enableAutoUpdate,
9
+ getAutoUpdateStatus,
10
+ promptAutoUpdateWizard,
11
+ } from "../utils/auto-update.js";
12
+ import { notify } from "../utils/notify.js";
13
+ import { parseDuration } from "../utils/settings.js";
14
+ import { updateExtmgrStatus } from "../utils/status.js";
15
+
16
+ function onUpdateAvailable(
17
+ ctx: ExtensionCommandContext | ExtensionContext,
18
+ packages: string[]
19
+ ): void {
20
+ notify(
21
+ ctx,
22
+ `Updates available for ${packages.length} package(s): ${packages.join(", ")}`,
23
+ "info"
24
+ );
25
+ }
26
+
27
+ export async function handleAutoUpdateSubcommand(
28
+ tokens: string[],
29
+ ctx: ExtensionCommandContext | ExtensionContext,
30
+ pi: ExtensionAPI
31
+ ): Promise<void> {
32
+ const trimmed = tokens.join(" ").trim();
33
+
34
+ if (!trimmed && ctx.hasUI) {
35
+ await promptAutoUpdateWizard(pi, ctx, (packages) => onUpdateAvailable(ctx, packages));
36
+ void updateExtmgrStatus(ctx, pi);
37
+ return;
38
+ }
39
+
40
+ const duration = parseDuration(trimmed);
41
+
42
+ if (!duration) {
43
+ const status = getAutoUpdateStatus(ctx);
44
+ notify(ctx, `Auto-update: ${status}`, "info");
45
+
46
+ const usage = [
47
+ "Usage: /extensions auto-update <duration>",
48
+ "",
49
+ "Duration examples:",
50
+ " never - Disable auto-updates",
51
+ " 1h - Check every hour",
52
+ " 2h - Check every 2 hours",
53
+ " 1d - Check daily",
54
+ " 3d - Check every 3 days",
55
+ " 1w - Check weekly",
56
+ " 2w - Check every 2 weeks",
57
+ " 1m - Check monthly",
58
+ " daily - Check daily (alias)",
59
+ " weekly - Check weekly (alias)",
60
+ ];
61
+ notify(ctx, usage.join("\n"), "info");
62
+ return;
63
+ }
64
+
65
+ if (duration.ms === 0) {
66
+ disableAutoUpdate(pi, ctx);
67
+ } else {
68
+ enableAutoUpdate(pi, ctx, duration.ms, duration.display, (packages) =>
69
+ onUpdateAvailable(ctx, packages)
70
+ );
71
+ }
72
+
73
+ void updateExtmgrStatus(ctx, pi);
74
+ }
75
+
76
+ export function createAutoUpdateNotificationHandler(
77
+ ctx: ExtensionCommandContext | ExtensionContext
78
+ ): (packages: string[]) => void {
79
+ return (packages) => onUpdateAvailable(ctx, packages);
80
+ }
@@ -0,0 +1,22 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { clearCache } from "../utils/cache.js";
3
+ import { logCacheClear } from "../utils/history.js";
4
+ import { notify } from "../utils/notify.js";
5
+ import { updateExtmgrStatus } from "../utils/status.js";
6
+
7
+ export async function clearMetadataCacheCommand(
8
+ ctx: ExtensionCommandContext,
9
+ pi: ExtensionAPI
10
+ ): Promise<void> {
11
+ try {
12
+ await clearCache();
13
+ logCacheClear(pi, true);
14
+ notify(ctx, "Metadata cache cleared.", "info");
15
+ } catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ logCacheClear(pi, false, message);
18
+ notify(ctx, `Failed to clear metadata cache: ${message}`, "error");
19
+ }
20
+
21
+ void updateExtmgrStatus(ctx, pi);
22
+ }
@@ -0,0 +1,264 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import {
3
+ formatChangeEntry,
4
+ queryGlobalHistory,
5
+ querySessionChanges,
6
+ type ChangeAction,
7
+ type HistoryFilters,
8
+ } from "../utils/history.js";
9
+ import { notify } from "../utils/notify.js";
10
+ import { formatListOutput } from "../utils/ui-helpers.js";
11
+
12
+ const HISTORY_ACTIONS: ChangeAction[] = [
13
+ "extension_toggle",
14
+ "package_install",
15
+ "package_update",
16
+ "package_remove",
17
+ "cache_clear",
18
+ ];
19
+
20
+ interface ParsedHistoryArgs {
21
+ filters: HistoryFilters;
22
+ global: boolean;
23
+ showHelp: boolean;
24
+ errors: string[];
25
+ }
26
+
27
+ interface HistoryParseState {
28
+ filters: HistoryFilters;
29
+ global: boolean;
30
+ showHelp: boolean;
31
+ errors: string[];
32
+ }
33
+
34
+ type HistoryOptionHandler = (tokens: string[], index: number, state: HistoryParseState) => number;
35
+
36
+ const HISTORY_ACTION_SET = new Set<ChangeAction>(HISTORY_ACTIONS);
37
+
38
+ function parseHistorySinceDuration(input: string): number | undefined {
39
+ const normalized = input.toLowerCase().trim();
40
+ const match = normalized.match(
41
+ /^(\d+)\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days|w|wk|wks|week|weeks|mo|mos|month|months)$/
42
+ );
43
+ if (!match) return undefined;
44
+
45
+ const value = Number.parseInt(match[1] ?? "", 10);
46
+ if (!Number.isFinite(value) || value <= 0) return undefined;
47
+
48
+ const unit = match[2] ?? "";
49
+ if (unit.startsWith("m") && !unit.startsWith("mo")) {
50
+ return value * 60 * 1000;
51
+ }
52
+ if (unit.startsWith("h")) {
53
+ return value * 60 * 60 * 1000;
54
+ }
55
+ if (unit.startsWith("d")) {
56
+ return value * 24 * 60 * 60 * 1000;
57
+ }
58
+ if (unit.startsWith("w")) {
59
+ return value * 7 * 24 * 60 * 60 * 1000;
60
+ }
61
+ if (unit.startsWith("mo")) {
62
+ return value * 30 * 24 * 60 * 60 * 1000;
63
+ }
64
+
65
+ return undefined;
66
+ }
67
+
68
+ const HISTORY_OPTION_HANDLERS: Record<string, HistoryOptionHandler> = {
69
+ "--help": (_tokens, _index, state) => {
70
+ state.showHelp = true;
71
+ return 0;
72
+ },
73
+ "-h": (_tokens, _index, state) => {
74
+ state.showHelp = true;
75
+ return 0;
76
+ },
77
+ "--global": (_tokens, _index, state) => {
78
+ state.global = true;
79
+ return 0;
80
+ },
81
+ "--limit": (tokens, index, state) => {
82
+ const value = tokens[index + 1];
83
+ if (!value) {
84
+ state.errors.push("--limit requires a number");
85
+ return 0;
86
+ }
87
+
88
+ const parsed = Number.parseInt(value, 10);
89
+ if (!Number.isFinite(parsed) || parsed <= 0) {
90
+ state.errors.push(`Invalid --limit value: ${value}`);
91
+ } else {
92
+ state.filters.limit = parsed;
93
+ }
94
+
95
+ return 1;
96
+ },
97
+ "--action": (tokens, index, state) => {
98
+ const value = tokens[index + 1] as ChangeAction | undefined;
99
+ if (!value) {
100
+ state.errors.push("--action requires a value");
101
+ return 0;
102
+ }
103
+
104
+ if (!HISTORY_ACTION_SET.has(value)) {
105
+ state.errors.push(`Invalid --action value: ${value}`);
106
+ } else {
107
+ state.filters.action = value;
108
+ }
109
+
110
+ return 1;
111
+ },
112
+ "--failed": (_tokens, _index, state) => {
113
+ if (state.filters.success === true) {
114
+ state.errors.push("Use either --success or --failed, not both");
115
+ }
116
+ state.filters.success = false;
117
+ return 0;
118
+ },
119
+ "--success": (_tokens, _index, state) => {
120
+ if (state.filters.success === false) {
121
+ state.errors.push("Use either --success or --failed, not both");
122
+ }
123
+ state.filters.success = true;
124
+ return 0;
125
+ },
126
+ "--package": (tokens, index, state) => {
127
+ const value = tokens[index + 1];
128
+ if (!value) {
129
+ state.errors.push("--package requires a value");
130
+ return 0;
131
+ }
132
+
133
+ state.filters.packageQuery = value;
134
+ return 1;
135
+ },
136
+ "--since": (tokens, index, state) => {
137
+ const value = tokens[index + 1];
138
+ if (!value) {
139
+ state.errors.push("--since requires a duration (e.g. 30m, 7d, 24h)");
140
+ return 0;
141
+ }
142
+
143
+ const ms = parseHistorySinceDuration(value);
144
+ if (!ms) {
145
+ state.errors.push(`Invalid --since duration: ${value}`);
146
+ } else {
147
+ state.filters.sinceTimestamp = Date.now() - ms;
148
+ }
149
+
150
+ return 1;
151
+ },
152
+ };
153
+
154
+ function parseHistoryArgs(tokens: string[]): ParsedHistoryArgs {
155
+ const state: HistoryParseState = {
156
+ filters: { limit: 20 },
157
+ global: false,
158
+ showHelp: false,
159
+ errors: [],
160
+ };
161
+
162
+ for (let i = 0; i < tokens.length; i++) {
163
+ const token = tokens[i] ?? "";
164
+ const handler = HISTORY_OPTION_HANDLERS[token];
165
+
166
+ if (!handler) {
167
+ state.errors.push(`Unknown history option: ${token}`);
168
+ continue;
169
+ }
170
+
171
+ const consumed = handler(tokens, i, state);
172
+ i += consumed;
173
+ }
174
+
175
+ return {
176
+ filters: state.filters,
177
+ global: state.global,
178
+ showHelp: state.showHelp,
179
+ errors: state.errors,
180
+ };
181
+ }
182
+
183
+ function showHistoryHelp(ctx: ExtensionCommandContext): void {
184
+ const lines = [
185
+ "Usage: /extensions history [options]",
186
+ "",
187
+ "Options:",
188
+ " --limit <n> Maximum entries to show (default: 20)",
189
+ " --action <type> Filter by action",
190
+ " extension_toggle | package_install | package_update | package_remove | cache_clear",
191
+ " --success Show only successful entries",
192
+ " --failed Show only failed entries",
193
+ " --package <q> Filter by package/source/extension id",
194
+ " --since <d> Show only entries newer than duration (e.g. 30m, 24h, 7d, 1mo)",
195
+ " --global Read all persisted sessions (non-interactive mode only)",
196
+ "",
197
+ "Examples:",
198
+ " /extensions history --failed --limit 50",
199
+ " /extensions history --action package_update --since 7d",
200
+ " /extensions history --package extmgr --since 30m",
201
+ " /extensions history --global --failed --since 14d",
202
+ ];
203
+
204
+ notify(ctx, lines.join("\n"), "info");
205
+ }
206
+
207
+ function formatSessionSuffix(sessionFile: string): string {
208
+ const marker = "/.pi/agent/sessions/";
209
+ const normalized = sessionFile.replace(/\\/g, "/");
210
+ const index = normalized.indexOf(marker);
211
+ if (index >= 0) {
212
+ return normalized.slice(index + marker.length);
213
+ }
214
+ return sessionFile;
215
+ }
216
+
217
+ export async function handleHistorySubcommand(
218
+ ctx: ExtensionCommandContext,
219
+ _pi: ExtensionAPI,
220
+ tokens: string[],
221
+ allowGlobal: boolean
222
+ ): Promise<void> {
223
+ const parsed = parseHistoryArgs(tokens);
224
+
225
+ if (parsed.showHelp) {
226
+ showHistoryHelp(ctx);
227
+ return;
228
+ }
229
+
230
+ if (parsed.errors.length > 0) {
231
+ notify(ctx, parsed.errors.join("\n"), "warning");
232
+ showHistoryHelp(ctx);
233
+ return;
234
+ }
235
+
236
+ if (parsed.global && !allowGlobal) {
237
+ notify(ctx, "--global is only available in non-interactive mode.", "warning");
238
+ return;
239
+ }
240
+
241
+ if (parsed.global) {
242
+ const changes = await queryGlobalHistory(parsed.filters);
243
+ if (changes.length === 0) {
244
+ notify(ctx, "No matching extension changes found across persisted sessions.", "info");
245
+ return;
246
+ }
247
+
248
+ const lines = changes.map(
249
+ ({ change, sessionFile }) =>
250
+ `${formatChangeEntry(change)} [${formatSessionSuffix(sessionFile)}]`
251
+ );
252
+ formatListOutput(ctx, `Extension Change History (global, recent ${changes.length})`, lines);
253
+ return;
254
+ }
255
+
256
+ const changes = querySessionChanges(ctx, parsed.filters);
257
+ if (changes.length === 0) {
258
+ notify(ctx, "No matching extension changes found in this session.", "info");
259
+ return;
260
+ }
261
+
262
+ const lines = changes.map(formatChangeEntry);
263
+ formatListOutput(ctx, `Extension Change History (recent ${changes.length})`, lines);
264
+ }
@@ -0,0 +1,83 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { installPackage, type InstallScope } from "../packages/install.js";
3
+ import { notify } from "../utils/notify.js";
4
+
5
+ export const INSTALL_USAGE = "Usage: /extensions install <source> [--project|--global]";
6
+
7
+ interface ParsedInstallArgs {
8
+ source: string;
9
+ scope?: InstallScope;
10
+ errors: string[];
11
+ }
12
+
13
+ interface InstallParseState {
14
+ sourceParts: string[];
15
+ scope?: InstallScope;
16
+ errors: string[];
17
+ }
18
+
19
+ type InstallOptionHandler = (state: InstallParseState) => void;
20
+
21
+ const INSTALL_OPTION_HANDLERS: Record<string, InstallOptionHandler> = {
22
+ "--project": (state) => {
23
+ if (state.scope === "global") {
24
+ state.errors.push("Use either --project or --global, not both");
25
+ }
26
+ state.scope = "project";
27
+ },
28
+ "-l": (state) => {
29
+ if (state.scope === "global") {
30
+ state.errors.push("Use either --project or --global, not both");
31
+ }
32
+ state.scope = "project";
33
+ },
34
+ "--global": (state) => {
35
+ if (state.scope === "project") {
36
+ state.errors.push("Use either --project or --global, not both");
37
+ }
38
+ state.scope = "global";
39
+ },
40
+ };
41
+
42
+ function parseInstallArgs(tokens: string[]): ParsedInstallArgs {
43
+ const state: InstallParseState = {
44
+ sourceParts: [],
45
+ errors: [],
46
+ };
47
+
48
+ for (const token of tokens) {
49
+ const optionHandler = INSTALL_OPTION_HANDLERS[token];
50
+ if (optionHandler) {
51
+ optionHandler(state);
52
+ } else {
53
+ state.sourceParts.push(token);
54
+ }
55
+ }
56
+
57
+ return {
58
+ source: state.sourceParts.join(" ").trim(),
59
+ ...(state.scope ? { scope: state.scope } : {}),
60
+ errors: state.errors,
61
+ };
62
+ }
63
+
64
+ export async function handleInstallSubcommand(
65
+ tokens: string[],
66
+ ctx: ExtensionCommandContext,
67
+ pi: ExtensionAPI
68
+ ): Promise<void> {
69
+ const parsed = parseInstallArgs(tokens);
70
+
71
+ if (parsed.errors.length > 0) {
72
+ notify(ctx, parsed.errors.join("\n"), "warning");
73
+ notify(ctx, INSTALL_USAGE, "info");
74
+ return;
75
+ }
76
+
77
+ if (!parsed.source) {
78
+ notify(ctx, INSTALL_USAGE, "info");
79
+ return;
80
+ }
81
+
82
+ await installPackage(parsed.source, ctx, pi, parsed.scope ? { scope: parsed.scope } : undefined);
83
+ }