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 +73 -24
- package/package.json +6 -3
- package/src/commands/auto-update.ts +80 -0
- package/src/commands/cache.ts +22 -0
- package/src/commands/history.ts +264 -0
- package/src/commands/install.ts +83 -0
- package/src/commands/registry.ts +202 -0
- package/src/commands/types.ts +30 -0
- package/src/constants.ts +41 -0
- package/src/extensions/discovery.ts +120 -2
- package/src/index.ts +58 -311
- package/src/packages/discovery.ts +74 -35
- package/src/packages/install.ts +192 -54
- package/src/packages/management.ts +232 -28
- package/src/types/index.ts +0 -2
- package/src/ui/help.ts +5 -2
- package/src/ui/remote.ts +305 -98
- package/src/ui/theme.ts +0 -44
- package/src/ui/unified.ts +356 -111
- package/src/utils/auto-update.ts +37 -7
- package/src/utils/cache.ts +27 -19
- package/src/utils/command.ts +22 -0
- package/src/utils/format.ts +26 -0
- package/src/utils/history.ts +182 -50
- package/src/utils/mode.ts +0 -21
- package/src/utils/package-source.ts +18 -0
- package/src/utils/settings.ts +100 -45
- package/src/utils/status.ts +48 -0
- package/src/utils/ui-helpers.ts +2 -17
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/
|
|
3
|
+
<img width="2560" height="369" alt="image" src="https://i.imgur.com/bVM7ZcO.png" />
|
|
4
4
|
|
|
5
5
|
[](https://github.com/ayagmar/pi-extmgr/actions/workflows/ci.yml)
|
|
6
6
|
[](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
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
- **Unified
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- **
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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
|
|
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
|
|
57
|
-
/extensions
|
|
58
|
-
/extensions
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
38
|
-
"image": "https://i.imgur.com/
|
|
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
|
+
}
|