@danypops/pi-packed 0.5.3 → 0.6.1
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 +9 -117
- package/extension/src/constants.ts +8 -0
- package/extension/src/index.ts +8 -2
- package/extension/src/packed.ts +25 -14
- package/extension/src/permission.ts +9 -0
- package/extension/src/profile.ts +280 -0
- package/extension/src/reload.ts +16 -0
- package/extension/src/resource-config.ts +230 -0
- package/extension/src/security-tui.ts +2 -1
- package/extension/src/setup-command.ts +50 -0
- package/extension/src/tool-output.ts +25 -6
- package/extension/src/tools.ts +38 -6
- package/extension/src/tui.ts +59 -29
- package/package.json +15 -28
- package/src/cache.ts +0 -21
- package/src/catalog.ts +0 -64
- package/src/cli.ts +0 -374
- package/src/client.ts +0 -247
- package/src/constants.ts +0 -60
- package/src/daemon.ts +0 -71
- package/src/db.ts +0 -164
- package/src/install.ts +0 -52
- package/src/installed.ts +0 -88
- package/src/log.ts +0 -42
- package/src/ports.ts +0 -96
- package/src/registry.ts +0 -141
- package/src/security.ts +0 -95
- package/src/service.ts +0 -233
- package/src/state.ts +0 -50
- package/src/version.ts +0 -16
- package/src/watcher.ts +0 -72
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resource-config.ts — /packed config overlay: enable/disable individual
|
|
3
|
+
* extensions, skills, prompt templates, and themes declared by installed
|
|
4
|
+
* Pi packages, matching pi's own `pi config` interaction model (group by
|
|
5
|
+
* package, toggle with space/enter, Tab to switch global/project scope).
|
|
6
|
+
* Pi's own settings mutation lives in its internal SettingsManager, which
|
|
7
|
+
* extensions cannot reach -- this reads and writes the same documented
|
|
8
|
+
* settings.json filter arrays (docs/packages.md, "Enable and Disable
|
|
9
|
+
* Resources") entirely through Packed's authenticated daemon.
|
|
10
|
+
*/
|
|
11
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { DynamicBorder, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
14
|
+
import type { PackageResources, ResourceField } from "./packed.js";
|
|
15
|
+
import type { Natives } from "./packed.js";
|
|
16
|
+
import { packagePermissionDecision } from "./permission.js";
|
|
17
|
+
|
|
18
|
+
type Scope = "global" | "project";
|
|
19
|
+
const RESOURCE_FIELDS = ["extensions", "skills", "prompts", "themes"] as const satisfies readonly ResourceField[];
|
|
20
|
+
|
|
21
|
+
interface FlatItem {
|
|
22
|
+
scope: Scope;
|
|
23
|
+
source: string;
|
|
24
|
+
packageName: string;
|
|
25
|
+
field: ResourceField;
|
|
26
|
+
path: string;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function flatten(groups: PackageResources[], scope: Scope): FlatItem[] {
|
|
31
|
+
const items: FlatItem[] = [];
|
|
32
|
+
for (const group of groups) {
|
|
33
|
+
for (const field of RESOURCE_FIELDS) {
|
|
34
|
+
for (const item of group[field]) items.push({ scope, source: group.source, packageName: group.name, field, path: item.path, enabled: item.enabled });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return items.sort((a, b) => a.packageName.localeCompare(b.packageName) || a.field.localeCompare(b.field) || a.path.localeCompare(b.path));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function filterItems(items: FlatItem[], query: string): FlatItem[] {
|
|
41
|
+
const q = query.trim().toLowerCase();
|
|
42
|
+
if (!q) return items;
|
|
43
|
+
return items.filter((item) => `${item.packageName} ${item.field} ${item.path}`.toLowerCase().includes(q));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type { FlatItem };
|
|
47
|
+
|
|
48
|
+
interface PanelAction {
|
|
49
|
+
type: "toggle" | "switch" | "refresh" | "close";
|
|
50
|
+
item?: FlatItem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function approveToggle(
|
|
54
|
+
natives: Pick<Natives, "security">,
|
|
55
|
+
ctx: Pick<ExtensionCommandContext, "hasUI" | "ui">,
|
|
56
|
+
item: FlatItem,
|
|
57
|
+
nextEnabled: boolean,
|
|
58
|
+
): Promise<boolean> {
|
|
59
|
+
const settings = await natives.security();
|
|
60
|
+
if (!packagePermissionDecision(settings, "toggle").approvalRequired) return true;
|
|
61
|
+
if (!ctx.hasUI) return false;
|
|
62
|
+
const reloadNote = item.field === "extensions" ? " This will require a Pi reload (/reload) to take effect." : "";
|
|
63
|
+
return ctx.ui.confirm(
|
|
64
|
+
`${nextEnabled ? "Enable" : "Disable"} ${item.field.slice(0, -1)}`,
|
|
65
|
+
`${item.packageName} \u00b7 ${item.path}.${reloadNote} Continue?`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ToggleOutcome = "toggled" | "cancelled" | "failed";
|
|
70
|
+
|
|
71
|
+
/** Warns inline (in the pre-toggle confirm dialog, not a footer note),
|
|
72
|
+
* requires the same approval every other settings-mutating operation
|
|
73
|
+
* requires, and reports -- never silently assumes -- whether the toggle
|
|
74
|
+
* actually reached the daemon. Extracted from the render loop so this
|
|
75
|
+
* approve-then-mutate decision is directly testable without faking a full
|
|
76
|
+
* ctx.ui.custom interaction. */
|
|
77
|
+
export async function applyResourceToggle(item: FlatItem, natives: Pick<Natives, "security" | "toggleResource">, ctx: ExtensionCommandContext): Promise<ToggleOutcome> {
|
|
78
|
+
const nextEnabled = !item.enabled;
|
|
79
|
+
const approved = await approveToggle(natives, ctx, item, nextEnabled);
|
|
80
|
+
if (!approved) { ctx.ui.notify("toggle cancelled", "warning"); return "cancelled"; }
|
|
81
|
+
try {
|
|
82
|
+
await natives.toggleResource(item.source, item.field, item.path, nextEnabled, item.scope === "project" ? ctx.cwd : undefined, true);
|
|
83
|
+
return "toggled";
|
|
84
|
+
} catch (error) {
|
|
85
|
+
ctx.ui.notify(`toggle failed: ${error instanceof Error ? error.message : error}`, "error");
|
|
86
|
+
return "failed";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `/packed config` dispatch, matching handleSetupCommand's own
|
|
91
|
+
* returns-true-if-handled shape so index.ts can chain both checks. */
|
|
92
|
+
export async function handleResourceConfigCommand(args: string, ctx: ExtensionCommandContext, natives: Natives): Promise<boolean> {
|
|
93
|
+
if (args.trim() !== "config") return false;
|
|
94
|
+
await showResourceConfig(ctx, natives);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function showResourceConfig(ctx: ExtensionCommandContext, natives: Natives): Promise<void> {
|
|
99
|
+
if (!ctx.hasUI) {
|
|
100
|
+
ctx.ui.notify("/packed config requires interactive mode", "warning");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let data: { global: PackageResources[]; project: PackageResources[] };
|
|
104
|
+
try { data = await natives.listResources(ctx.cwd); }
|
|
105
|
+
catch (error) {
|
|
106
|
+
ctx.ui.notify(`packed unavailable: ${error instanceof Error ? error.message : error}`, "error");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let scope: Scope = "global";
|
|
111
|
+
let pendingReload = false;
|
|
112
|
+
|
|
113
|
+
for (;;) {
|
|
114
|
+
const action = await renderConfig(ctx, data, scope);
|
|
115
|
+
if (action.type === "close") break;
|
|
116
|
+
if (action.type === "switch") { scope = scope === "global" ? "project" : "global"; continue; }
|
|
117
|
+
if (action.type === "refresh") {
|
|
118
|
+
try { data = await natives.listResources(ctx.cwd); }
|
|
119
|
+
catch (error) { ctx.ui.notify(`refresh failed: ${error instanceof Error ? error.message : error}`, "error"); }
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const item = action.item;
|
|
123
|
+
if (!item) continue;
|
|
124
|
+
const outcome = await applyResourceToggle(item, natives, ctx);
|
|
125
|
+
if (outcome !== "toggled") continue;
|
|
126
|
+
const group = (item.scope === "global" ? data.global : data.project).find((candidate) => candidate.source === item.source);
|
|
127
|
+
const entry = group?.[item.field].find((candidate) => candidate.path === item.path);
|
|
128
|
+
if (entry) entry.enabled = !item.enabled;
|
|
129
|
+
if (item.field === "extensions") pendingReload = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!pendingReload) return;
|
|
133
|
+
const confirmed = await ctx.ui.confirm("Reload Pi now?", "Extension changes only take effect after a reload.");
|
|
134
|
+
if (confirmed) await ctx.reload();
|
|
135
|
+
else ctx.ui.notify("Extension changes pending -- run /reload when ready.", "warning");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderConfig(ctx: ExtensionCommandContext, data: { global: PackageResources[]; project: PackageResources[] }, scope: Scope): Promise<PanelAction> {
|
|
139
|
+
return ctx.ui.custom<PanelAction>((tui, theme, _kb, done) => {
|
|
140
|
+
const searchInput = new Input();
|
|
141
|
+
let searchActive = false;
|
|
142
|
+
const items = flatten(scope === "global" ? data.global : data.project, scope);
|
|
143
|
+
let filtered = items;
|
|
144
|
+
let selectedIndex = 0;
|
|
145
|
+
const maxVisible = 20;
|
|
146
|
+
|
|
147
|
+
function applyFilter(): void {
|
|
148
|
+
filtered = filterItems(items, searchInput.getValue());
|
|
149
|
+
selectedIndex = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const header = {
|
|
153
|
+
invalidate() {},
|
|
154
|
+
render(width: number): string[] {
|
|
155
|
+
const title = theme.bold(scope === "project" ? "Project Resources" : "Global Resources");
|
|
156
|
+
const hint = searchActive
|
|
157
|
+
? rawKeyHint("esc", "clear")
|
|
158
|
+
: rawKeyHint("space", "toggle") + theme.fg("muted", " \u00b7 ") + rawKeyHint("/", "filter") + theme.fg("muted", " \u00b7 ") + rawKeyHint("tab", "scope") + theme.fg("muted", " \u00b7 ") + rawKeyHint("esc", "close");
|
|
159
|
+
const spacing = Math.max(1, width - visibleWidth(title) - visibleWidth(hint));
|
|
160
|
+
const line1 = truncateToWidth(`${title}${" ".repeat(spacing)}${hint}`, width, "");
|
|
161
|
+
const line2 = truncateToWidth(theme.fg("muted", `${items.length} resource(s) \u00b7 extensions need /reload after a change`), width, "");
|
|
162
|
+
return [line1, line2];
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const list = {
|
|
167
|
+
invalidate() {},
|
|
168
|
+
render(width: number): string[] {
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
if (searchActive) lines.push(...searchInput.render(width));
|
|
171
|
+
lines.push("");
|
|
172
|
+
if (filtered.length === 0) { lines.push(theme.fg("muted", " No resources found")); return lines; }
|
|
173
|
+
const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
174
|
+
const end = Math.min(start + maxVisible, filtered.length);
|
|
175
|
+
for (let index = start; index < end; index++) {
|
|
176
|
+
const item = filtered[index]!;
|
|
177
|
+
const isSelected = index === selectedIndex;
|
|
178
|
+
const cursor = isSelected ? theme.fg("accent", "\u276f") : " ";
|
|
179
|
+
const box = item.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
180
|
+
const reloadMark = item.field === "extensions" ? theme.fg("warning", " \u27f3") : "";
|
|
181
|
+
const label = `${item.packageName} \u00b7 ${item.field} \u00b7 ${item.path}${reloadMark}`;
|
|
182
|
+
lines.push(truncateToWidth(`${cursor} ${box} ${isSelected ? theme.bold(label) : label}`, width, "..."));
|
|
183
|
+
}
|
|
184
|
+
const hasScroll = start > 0 || end < filtered.length;
|
|
185
|
+
if (hasScroll) lines.push(theme.fg("dim", ` ${selectedIndex + 1}/${filtered.length}`));
|
|
186
|
+
return lines;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const container = new Container();
|
|
191
|
+
container.addChild(new Spacer(1));
|
|
192
|
+
container.addChild(new DynamicBorder());
|
|
193
|
+
container.addChild(new Spacer(1));
|
|
194
|
+
container.addChild(header);
|
|
195
|
+
container.addChild(new Spacer(1));
|
|
196
|
+
container.addChild(list);
|
|
197
|
+
container.addChild(new Spacer(1));
|
|
198
|
+
container.addChild(new DynamicBorder());
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
render: (width: number) => container.render(width),
|
|
202
|
+
invalidate: () => container.invalidate(),
|
|
203
|
+
handleInput(raw: string) {
|
|
204
|
+
if (searchActive) {
|
|
205
|
+
if (raw === "\x1b") { searchActive = false; searchInput.setValue?.(""); applyFilter(); }
|
|
206
|
+
else if (raw === "\r") searchActive = false;
|
|
207
|
+
else { searchInput.handleInput(raw); applyFilter(); }
|
|
208
|
+
tui.requestRender();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
switch (raw) {
|
|
212
|
+
case "\x1b[A": selectedIndex = (selectedIndex - 1 + filtered.length) % Math.max(filtered.length, 1); break;
|
|
213
|
+
case "\x1b[B": selectedIndex = (selectedIndex + 1) % Math.max(filtered.length, 1); break;
|
|
214
|
+
case "\t": done({ type: "switch" }); return;
|
|
215
|
+
case "/": searchActive = true; break;
|
|
216
|
+
case "r": done({ type: "refresh" }); return;
|
|
217
|
+
case " ":
|
|
218
|
+
case "\r": {
|
|
219
|
+
const item = filtered[selectedIndex];
|
|
220
|
+
if (item) done({ type: "toggle", item });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
case "\x1b": done({ type: "close" }); return;
|
|
224
|
+
default: return;
|
|
225
|
+
}
|
|
226
|
+
tui.requestRender();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SecuritySettings } from "@danypops/packed/protocol";
|
|
3
|
+
type MutationApproval = SecuritySettings["mutationApproval"];
|
|
3
4
|
import type { Natives } from "./packed.ts";
|
|
4
5
|
|
|
5
6
|
const OPTIONS: Array<{ value: MutationApproval; label: string }> = [
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Natives } from "./packed.js";
|
|
4
|
+
|
|
5
|
+
interface SetupCommandArgs { action: "plan" | "apply"; path: string; prune: boolean; }
|
|
6
|
+
|
|
7
|
+
function parseSetupCommand(args: string, cwd: string): SetupCommandArgs | undefined {
|
|
8
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
9
|
+
if (parts[0] !== "setup" || (parts[1] !== "plan" && parts[1] !== "apply")) return undefined;
|
|
10
|
+
const prune = parts.includes("--prune");
|
|
11
|
+
const path = parts.slice(2).find((part) => !part.startsWith("--")) ?? "pi-setup.json";
|
|
12
|
+
return { action: parts[1], path: resolve(cwd, path), prune };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleSetupCommand(args: string, ctx: ExtensionCommandContext, natives: Pick<Natives, "setupPlan" | "setupApply">): Promise<boolean> {
|
|
16
|
+
const parsed = parseSetupCommand(args, ctx.cwd);
|
|
17
|
+
if (!parsed) return false;
|
|
18
|
+
if (!ctx.hasUI) return true;
|
|
19
|
+
const plan = await natives.setupPlan(parsed.path, parsed.prune);
|
|
20
|
+
if (!plan.ok) {
|
|
21
|
+
ctx.ui.notify(plan.diagnostics[0]?.message ?? "Setup manifest is invalid", "error");
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (parsed.action === "plan") {
|
|
25
|
+
const suffix = parsed.prune ? " with prune" : "";
|
|
26
|
+
ctx.ui.notify(`${plan.operations.length} setup operation(s)${suffix}`, "info");
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (plan.operations.length === 0) {
|
|
30
|
+
ctx.ui.notify("Setup already matches", "info");
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
const packageChanges = plan.operations.filter((operation) => operation.kind.includes("package")).length;
|
|
34
|
+
const confirmed = await ctx.ui.confirm(
|
|
35
|
+
"Apply Pi setup?",
|
|
36
|
+
`Apply ${plan.operations.length} operation(s)${parsed.prune ? ", including prune removals" : ""}. ${packageChanges} package operation(s) may execute package code.`,
|
|
37
|
+
);
|
|
38
|
+
if (!confirmed) return true;
|
|
39
|
+
const result = await natives.setupApply(parsed.path, true, parsed.prune);
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
ctx.ui.notify(result.diagnostics[0]?.message ?? "Setup apply failed", "error");
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (result.reloadRequired) {
|
|
45
|
+
await ctx.reload();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
ctx.ui.notify(`Applied ${result.operations.length} setup operation(s)`, "info");
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
@@ -9,17 +9,21 @@ import {
|
|
|
9
9
|
TOOL_DETAILS_MAX_PACKAGES,
|
|
10
10
|
TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
|
|
11
11
|
TOOL_MODEL_CONTENT_MAX_CHARACTERS,
|
|
12
|
-
} from "
|
|
13
|
-
import type {
|
|
12
|
+
} from "./constants.js";
|
|
13
|
+
import type { PackageInfo as PkgInfo, PackageSummary as Pkg } from "@danypops/packed/protocol";
|
|
14
14
|
|
|
15
|
-
const DETAILS_VERSION =
|
|
15
|
+
const DETAILS_VERSION = 2 as const;
|
|
16
16
|
const MUTATION_OPERATIONS = new Set(["install", "update", "remove"]);
|
|
17
17
|
const MUTATION_STATUSES = new Set(["succeeded", "cancelled", "denied"]);
|
|
18
|
+
const PACKAGE_SHAPES = new Set(["keyword-only", "manifest", "conventional"]);
|
|
19
|
+
const TRUSTED_PUBLISHER_STATES = new Set(["verified", "not-verified", "unknown"]);
|
|
18
20
|
|
|
19
21
|
export interface PackageSummaryDetails {
|
|
20
22
|
name: string;
|
|
21
23
|
version: string;
|
|
22
24
|
description: string;
|
|
25
|
+
shape: "keyword-only" | "manifest" | "conventional";
|
|
26
|
+
verified: boolean;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export interface SearchToolDetails {
|
|
@@ -42,6 +46,8 @@ export interface InfoToolDetails {
|
|
|
42
46
|
license?: string;
|
|
43
47
|
keywords: string[];
|
|
44
48
|
capabilities: string[];
|
|
49
|
+
provenance?: string;
|
|
50
|
+
trustedPublisher: "verified" | "not-verified" | "unknown";
|
|
45
51
|
};
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -116,6 +122,8 @@ function packageSummary(pkg: Pkg): PackageSummaryDetails {
|
|
|
116
122
|
name: bounded(pkg.name, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS),
|
|
117
123
|
version: bounded(pkg.version, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS),
|
|
118
124
|
description: bounded(pkg.description, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS),
|
|
125
|
+
shape: pkg.packageEvidence?.shape ?? "keyword-only",
|
|
126
|
+
verified: pkg.packageEvidence?.verified === true,
|
|
119
127
|
};
|
|
120
128
|
}
|
|
121
129
|
|
|
@@ -143,6 +151,8 @@ export function createInfoDetails(info: PkgInfo): InfoToolDetails {
|
|
|
143
151
|
...(info.license ? { license: bounded(info.license, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS) } : {}),
|
|
144
152
|
keywords: (info.keywords ?? []).slice(0, TOOL_DETAILS_MAX_KEYWORDS).map((keyword) => bounded(keyword, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS)),
|
|
145
153
|
capabilities: Object.keys(info.pi ?? {}).slice(0, TOOL_DETAILS_MAX_CAPABILITIES).map((name) => bounded(name, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS)),
|
|
154
|
+
...(info.publication?.provenanceUrl ? { provenance: safePackageTarget(info.publication.provenanceUrl) } : {}),
|
|
155
|
+
trustedPublisher: info.publication?.trustedPublisher ?? "unknown",
|
|
146
156
|
},
|
|
147
157
|
};
|
|
148
158
|
}
|
|
@@ -191,9 +201,10 @@ export function parsePackageToolDetails(value: unknown): PackageToolDetails | un
|
|
|
191
201
|
const pkg = candidate.package as Record<string, unknown>;
|
|
192
202
|
if (!isPackageSummary(pkg) || !Array.isArray(pkg.keywords) || pkg.keywords.length > TOOL_DETAILS_MAX_KEYWORDS || !pkg.keywords.every((item) => isShortString(item, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS))) return undefined;
|
|
193
203
|
if (!Array.isArray(pkg.capabilities) || pkg.capabilities.length > TOOL_DETAILS_MAX_CAPABILITIES || !pkg.capabilities.every((item) => isShortString(item, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS))) return undefined;
|
|
194
|
-
for (const field of ["homepage", "repository", "license"] as const) {
|
|
204
|
+
for (const field of ["homepage", "repository", "license", "provenance"] as const) {
|
|
195
205
|
if (pkg[field] !== undefined && !isShortString(pkg[field])) return undefined;
|
|
196
206
|
}
|
|
207
|
+
if (typeof pkg.trustedPublisher !== "string" || !TRUSTED_PUBLISHER_STATES.has(pkg.trustedPublisher)) return undefined;
|
|
197
208
|
return value as InfoToolDetails;
|
|
198
209
|
}
|
|
199
210
|
if (candidate.kind === "mutation") {
|
|
@@ -213,7 +224,9 @@ function isPackageSummary(value: unknown): boolean {
|
|
|
213
224
|
const item = value as Record<string, unknown>;
|
|
214
225
|
return isShortString(item.name, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS)
|
|
215
226
|
&& isShortString(item.version, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS)
|
|
216
|
-
&& isShortString(item.description, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS)
|
|
227
|
+
&& isShortString(item.description, TOOL_DETAILS_MAX_DESCRIPTION_CHARACTERS)
|
|
228
|
+
&& typeof item.shape === "string" && PACKAGE_SHAPES.has(item.shape)
|
|
229
|
+
&& typeof item.verified === "boolean";
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
function contentFallback(result: AgentToolResult<unknown>): string {
|
|
@@ -240,7 +253,10 @@ export function renderPackageToolResult(
|
|
|
240
253
|
if (details.kind === "search") {
|
|
241
254
|
const shown = options.expanded ? details.items : details.items.slice(0, TOOL_COLLAPSED_PACKAGE_PREVIEW);
|
|
242
255
|
const heading = `${theme.bold(String(details.total))} packages · showing ${details.items.length}${details.truncated ? " · bounded" : ""}`;
|
|
243
|
-
const rows = shown.map((pkg) =>
|
|
256
|
+
const rows = shown.map((pkg) => {
|
|
257
|
+
const evidence = pkg.verified ? ` [verified ${pkg.shape}]` : " [keyword candidate]";
|
|
258
|
+
return truncateToWidth(`${theme.fg("accent", `${pkg.name}@${pkg.version}`)}${theme.fg("muted", evidence)}${options.expanded && pkg.description ? ` — ${pkg.description}` : ""}`, safeWidth);
|
|
259
|
+
});
|
|
244
260
|
if (!options.expanded && details.items.length > shown.length) rows.push(theme.fg("muted", `… ${details.items.length - shown.length} more`));
|
|
245
261
|
return [truncateToWidth(heading, safeWidth), ...rows];
|
|
246
262
|
}
|
|
@@ -251,6 +267,9 @@ export function renderPackageToolResult(
|
|
|
251
267
|
if (pkg.license) lines.push(`license: ${pkg.license}`);
|
|
252
268
|
if (pkg.repository) lines.push(`repository: ${pkg.repository}`);
|
|
253
269
|
if (pkg.homepage) lines.push(`homepage: ${pkg.homepage}`);
|
|
270
|
+
lines.push(`shape: ${pkg.shape}${pkg.verified ? " (tarball verified)" : " (metadata only)"}`);
|
|
271
|
+
if (pkg.provenance) lines.push(`provenance: ${pkg.provenance}`);
|
|
272
|
+
lines.push(`trusted publisher: ${pkg.trustedPublisher}`);
|
|
254
273
|
if (pkg.capabilities.length) lines.push(`provides: ${pkg.capabilities.join(", ")}`);
|
|
255
274
|
if (pkg.keywords.length) lines.push(`keywords: ${pkg.keywords.join(", ")}`);
|
|
256
275
|
}
|
package/extension/src/tools.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Agent-facing package tools over the authenticated daemon. */
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
|
-
import { packagePermissionDecision, type PackageOperation } from "
|
|
5
|
-
import type
|
|
4
|
+
import { packagePermissionDecision, type PackageOperation } from "./permission.js";
|
|
5
|
+
import { InstallServiceError, PI_COMMAND_NAME, type Natives } from "./packed.js";
|
|
6
|
+
import { reloadWarning } from "./reload.js";
|
|
6
7
|
import {
|
|
7
8
|
createInfoDetails,
|
|
8
9
|
createModelContent,
|
|
@@ -41,14 +42,14 @@ export async function approvePackageOperation(
|
|
|
41
42
|
}
|
|
42
43
|
const approved = await ctx.ui.confirm(
|
|
43
44
|
`${operation[0]!.toUpperCase()}${operation.slice(1)} Pi package`,
|
|
44
|
-
`Run: ${command}\n\nThis operation can execute package code or mutate Pi settings/install roots. Continue?`,
|
|
45
|
+
`Run: ${command}\n\nThis operation can execute package code or mutate Pi settings/install roots. ${reloadWarning(operation)} Continue?`,
|
|
45
46
|
);
|
|
46
47
|
return approved ? { allowed: true, approved: true } : { allowed: false, approved: false, reason: "cancelled", message: `${operation} cancelled by user.` };
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export async function installPackageWithPolicy(
|
|
50
51
|
source: string,
|
|
51
|
-
natives: Pick<Natives, "security" | "install">,
|
|
52
|
+
natives: Pick<Natives, "security" | "install" | "installService">,
|
|
52
53
|
ctx: ApprovalContext,
|
|
53
54
|
) {
|
|
54
55
|
const approval = await approvePackageOperation("install", `pi install ${source}`, natives, ctx);
|
|
@@ -56,7 +57,23 @@ export async function installPackageWithPolicy(
|
|
|
56
57
|
const output = approval.message ?? "install denied";
|
|
57
58
|
return text(output, createMutationDetails("install", source, approval.reason ?? "denied", output));
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
let output = await natives.install(source, approval.approved) || `Installed ${source}. Reload with /reload to activate.`;
|
|
61
|
+
// Piggybacks on the same approval already granted for install -- both are
|
|
62
|
+
// the same code-execution mutation tier, not a new consent surface. Silent
|
|
63
|
+
// for the overwhelmingly common case (most Pi packages aren't daemons at
|
|
64
|
+
// all); a genuine failure is reported without failing the install itself,
|
|
65
|
+
// which already succeeded.
|
|
66
|
+
if (source.startsWith("npm:")) {
|
|
67
|
+
try {
|
|
68
|
+
const svc = await natives.installService(source, approval.approved);
|
|
69
|
+
output += `\n${svc.output}`;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (!(e instanceof InstallServiceError) || !e.notADaemon) {
|
|
72
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
73
|
+
output += `\nnote: detected a persistent-service daemon but could not register it: ${message}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
return text(output, createMutationDetails("install", source, "succeeded", output));
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -125,7 +142,7 @@ export function registerTools(pi: ExtensionAPI, natives: Natives): void {
|
|
|
125
142
|
pi.registerTool({
|
|
126
143
|
name: "pkg_info",
|
|
127
144
|
label: "Pi Package Info",
|
|
128
|
-
description:
|
|
145
|
+
description: `Show bounded metadata and declared Pi resources for one package. Special case: querying ${PI_COMMAND_NAME} (Pi itself) also reports the locally running version against the latest published release -- a different question from, and in addition to, the generic npm registry metadata every other package gets.`,
|
|
129
146
|
parameters: Type.Object({ name: Type.String({ description: "npm package name" }) }),
|
|
130
147
|
renderCall(args, theme) { return renderPackageToolCall("Package info", args, theme); },
|
|
131
148
|
renderResult(result, options, theme, context) { return renderPackageToolResult(result, options, theme, context); },
|
|
@@ -141,6 +158,21 @@ export function registerTools(pi: ExtensionAPI, natives: Natives): void {
|
|
|
141
158
|
info.unpackedSize ? `size: ${(info.unpackedSize / 1024).toFixed(0)} KB` : "",
|
|
142
159
|
info.modified ? `modified: ${info.modified}` : "",
|
|
143
160
|
].filter(Boolean);
|
|
161
|
+
// Special-cased by package identity, not by any change in risk
|
|
162
|
+
// profile -- still a pure read, just answering a second, genuinely
|
|
163
|
+
// different question (what's actually running here, not what npm
|
|
164
|
+
// last published) that the generic registry lookup above cannot.
|
|
165
|
+
if (params.name === PI_COMMAND_NAME) {
|
|
166
|
+
const status = await natives.piStatus().catch(() => undefined);
|
|
167
|
+
if (status?.current) {
|
|
168
|
+
const comparison = status.latest === undefined
|
|
169
|
+
? "latest release unknown"
|
|
170
|
+
: status.upToDate === false
|
|
171
|
+
? `behind -- latest is ${status.latest}, run pi update --self`
|
|
172
|
+
: `up to date with the latest ${status.latest}`;
|
|
173
|
+
lines.push(`--- Pi runtime (not npm registry data) ---`, `currently running: ${status.current}`, comparison);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
144
176
|
return text(lines.join("\n"), createInfoDetails(info));
|
|
145
177
|
} catch (error) {
|
|
146
178
|
throw new Error(`pkg_info failed: ${error instanceof Error ? error.message : error}`);
|
package/extension/src/tui.ts
CHANGED
|
@@ -17,6 +17,63 @@ interface PanelAction {
|
|
|
17
17
|
row?: Row;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Outcome of a confirmed row action, resolved after any real mutation and
|
|
21
|
+
* reload decision -- "changed" means the daemon state changed and Pi has
|
|
22
|
+
* already been reloaded (the caller should stop showing the stale panel);
|
|
23
|
+
* "unchanged"/"cancelled" mean the panel keeps running as-is. */
|
|
24
|
+
export type PackageChoiceOutcome = "changed" | "unchanged" | "cancelled";
|
|
25
|
+
|
|
26
|
+
export async function applyPackageChoice(
|
|
27
|
+
choice: string | undefined,
|
|
28
|
+
row: Row,
|
|
29
|
+
natives: Natives,
|
|
30
|
+
ctx: ExtensionCommandContext,
|
|
31
|
+
): Promise<PackageChoiceOutcome> {
|
|
32
|
+
if (choice?.startsWith("Update")) {
|
|
33
|
+
try {
|
|
34
|
+
const approval = await approvePackageOperation("update", `pi update --extension npm:${row.name}`, natives, ctx);
|
|
35
|
+
if (!approval.allowed) {
|
|
36
|
+
ctx.ui.notify(approval.message ?? "update denied", "warning");
|
|
37
|
+
return "cancelled";
|
|
38
|
+
}
|
|
39
|
+
ctx.ui.notify(`Updating ${row.name}…`, "info");
|
|
40
|
+
const outcome = await natives.update(`npm:${row.name}`, approval.approved);
|
|
41
|
+
if (!outcome.reloadRequired) {
|
|
42
|
+
const version = outcome.currentVersion ?? outcome.previousVersion;
|
|
43
|
+
const reason = outcome.pinned
|
|
44
|
+
? `pinned to ${version ?? "an exact version"} -- pi update intentionally leaves pinned packages unchanged`
|
|
45
|
+
: `already up to date${version ? ` at ${version}` : ""}`;
|
|
46
|
+
ctx.ui.notify(`${row.name} is ${reason}.`, "info");
|
|
47
|
+
return "unchanged";
|
|
48
|
+
}
|
|
49
|
+
const transition = outcome.previousVersion && outcome.currentVersion ? ` (${outcome.previousVersion} → ${outcome.currentVersion})` : "";
|
|
50
|
+
ctx.ui.notify(`Updated ${row.name}${transition}; reloading Pi resources.`, "info");
|
|
51
|
+
await ctx.reload();
|
|
52
|
+
return "changed";
|
|
53
|
+
} catch (e) {
|
|
54
|
+
ctx.ui.notify(`update failed: ${e instanceof Error ? e.message : e}`, "error");
|
|
55
|
+
return "cancelled";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (choice === "Remove") {
|
|
59
|
+
try {
|
|
60
|
+
const approval = await approvePackageOperation("remove", `pi remove npm:${row.name}`, natives, ctx);
|
|
61
|
+
if (!approval.allowed) {
|
|
62
|
+
ctx.ui.notify(approval.message ?? "remove denied", "warning");
|
|
63
|
+
return "cancelled";
|
|
64
|
+
}
|
|
65
|
+
await natives.remove(row.name, approval.approved);
|
|
66
|
+
ctx.ui.notify(`Removed ${row.name}; reloading Pi resources.`, "info");
|
|
67
|
+
await ctx.reload();
|
|
68
|
+
return "changed";
|
|
69
|
+
} catch (e) {
|
|
70
|
+
ctx.ui.notify(`remove failed: ${e instanceof Error ? e.message : e}`, "error");
|
|
71
|
+
return "cancelled";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return "cancelled";
|
|
75
|
+
}
|
|
76
|
+
|
|
20
77
|
async function loadRows(natives: Natives): Promise<{ rows: Row[]; error?: string }> {
|
|
21
78
|
try {
|
|
22
79
|
const [installed, updates] = await Promise.all([
|
|
@@ -64,35 +121,8 @@ export async function showPackages(ctx: ExtensionCommandContext, natives: Native
|
|
|
64
121
|
],
|
|
65
122
|
);
|
|
66
123
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const approval = await approvePackageOperation("update", `pi update --extension npm:${row.name}`, natives, ctx);
|
|
70
|
-
if (!approval.allowed) {
|
|
71
|
-
ctx.ui.notify(approval.message ?? "update denied", "warning");
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
ctx.ui.notify(`Updating ${row.name}…`, "info");
|
|
75
|
-
await natives.update(`npm:${row.name}`, approval.approved);
|
|
76
|
-
ctx.ui.notify(`Updated ${row.name} to ${row.latest}; reloading Pi resources`, "info");
|
|
77
|
-
await ctx.reload();
|
|
78
|
-
return;
|
|
79
|
-
} catch (e) {
|
|
80
|
-
ctx.ui.notify(`update failed: ${e instanceof Error ? e.message : e}`, "error");
|
|
81
|
-
}
|
|
82
|
-
} else if (choice === "Remove") {
|
|
83
|
-
try {
|
|
84
|
-
const approval = await approvePackageOperation("remove", `pi remove npm:${row.name}`, natives, ctx);
|
|
85
|
-
if (approval.allowed) {
|
|
86
|
-
await natives.remove(row.name, approval.approved);
|
|
87
|
-
ctx.ui.notify(`Removed ${row.name}`, "info");
|
|
88
|
-
rows = rows.filter((r) => r.name !== row.name);
|
|
89
|
-
} else {
|
|
90
|
-
ctx.ui.notify(approval.message ?? "remove denied", "warning");
|
|
91
|
-
}
|
|
92
|
-
} catch (e) {
|
|
93
|
-
ctx.ui.notify(`remove failed: ${e instanceof Error ? e.message : e}`, "error");
|
|
94
|
-
}
|
|
95
|
-
}
|
|
124
|
+
const outcome = await applyPackageChoice(choice, row, natives, ctx);
|
|
125
|
+
if (outcome === "changed") return; // ctx.reload() already replaced the session
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
128
|
|
package/package.json
CHANGED
|
@@ -1,43 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Pi package tools, commands, profiles, and TUI for the Packed daemon",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
|
|
6
|
+
"keywords": ["pi-package"],
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": ["extension/src/index.ts"]
|
|
8
9
|
},
|
|
9
|
-
"keywords": [
|
|
10
|
-
"pi-package"
|
|
11
|
-
],
|
|
12
10
|
"scripts": {
|
|
13
|
-
"test": "bun test",
|
|
14
|
-
"typecheck": "
|
|
15
|
-
"cli": "bun src/cli.ts",
|
|
16
|
-
"serve": "bun src/cli.ts serve"
|
|
17
|
-
},
|
|
18
|
-
"pi": {
|
|
19
|
-
"extensions": [
|
|
20
|
-
"extension/src/index.ts"
|
|
21
|
-
]
|
|
11
|
+
"test": "bun test test",
|
|
12
|
+
"typecheck": "bunx tsc --noEmit"
|
|
22
13
|
},
|
|
23
14
|
"dependencies": {
|
|
24
|
-
"@danypops/
|
|
15
|
+
"@danypops/packed": "^0.2.0",
|
|
16
|
+
"@danypops/vehicle-client": "^0.1.1",
|
|
17
|
+
"@danypops/vehicle-client-pi": "^0.1.5"
|
|
25
18
|
},
|
|
26
19
|
"peerDependencies": {
|
|
27
20
|
"@earendil-works/pi-coding-agent": "*",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"bun-types": "latest"
|
|
21
|
+
"typebox": "*",
|
|
22
|
+
"@earendil-works/pi-tui": "*"
|
|
33
23
|
},
|
|
34
24
|
"repository": {
|
|
35
25
|
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/DanyPops/
|
|
26
|
+
"url": "git+https://github.com/DanyPops/packed.git",
|
|
27
|
+
"directory": "packages/pi-packed"
|
|
37
28
|
},
|
|
38
|
-
"files": [
|
|
39
|
-
"src",
|
|
40
|
-
"extension",
|
|
41
|
-
"README.md"
|
|
42
|
-
]
|
|
29
|
+
"files": ["extension", "setup", "README.md"]
|
|
43
30
|
}
|
package/src/cache.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** TTLCache — the smart-proxy concern, nothing more. */
|
|
2
|
-
import { CACHE_TTL_MS } from "./constants.ts";
|
|
3
|
-
|
|
4
|
-
export class TTLCache {
|
|
5
|
-
private m = new Map<string, { body: string; expires: number }>();
|
|
6
|
-
constructor(private ttlMs = CACHE_TTL_MS) {}
|
|
7
|
-
|
|
8
|
-
get(key: string): string | undefined {
|
|
9
|
-
const e = this.m.get(key);
|
|
10
|
-
if (!e) return undefined;
|
|
11
|
-
if (Date.now() > e.expires) {
|
|
12
|
-
this.m.delete(key);
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|
|
15
|
-
return e.body;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
set(key: string, body: string): void {
|
|
19
|
-
this.m.set(key, { body, expires: Date.now() + this.ttlMs });
|
|
20
|
-
}
|
|
21
|
-
}
|