@danypops/pi-packed 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -10,7 +10,7 @@ inside the supervised Bun daemon.
10
10
 
11
11
  ```text
12
12
  ┌─ Pi extension (Node-compatible) ──────────────┐
13
- │ pkg_search · pkg_info · pkg_install · pkg_remove
13
+ │ pkg_search · pkg_info · pkg_install/update/remove
14
14
  │ /packages · /packed permission settings │
15
15
  │ operation-aware approval · no Bun/SQLite access │
16
16
  └──────────────────┬────────────────────────────┘
@@ -46,6 +46,7 @@ Packages execute arbitrary code and mutate Pi settings/install roots. One daemon
46
46
  | `packed search <q> [--offline] [--limit N] [--json]` | Search npm or the local mirror, scoped to `keywords:pi-package` |
47
47
  | `packed info <name> [--json]` | Show version, repository, Pi manifest, size, and license |
48
48
  | `packed updates [--json]` | Show drift from the local mirror |
49
+ | `packed update <source> [--approve] [--json]` | Update one configured source through `pi update --extension` |
49
50
  | `packed mirror [--json]` | Refresh the SQLite package index |
50
51
  | `packed installed [--json]` | Read Pi's installed package declarations |
51
52
  | `packed catalog [--json]` | Inspect the local package index |
@@ -81,6 +82,7 @@ The daemon listens on loopback only.
81
82
  | `GET` | `/updates` |
82
83
  | `GET` | `/catalog` |
83
84
  | `POST` | `/install` with `{ "source": "...", "approved": true }` |
85
+ | `POST` | `/update` with `{ "source": "...", "approved": true }` |
84
86
  | `POST` | `/remove` with `{ "name": "...", "approved": true }` |
85
87
 
86
88
  State defaults to `~/.cache/pi-packed/` and contains `token`, `port`,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * pi-packed — Pi extension seam.
3
3
  *
4
- * Thin by design: registers agent tools (pkg_search/pkg_info/pkg_install),
4
+ * Thin by design: registers agent tools (pkg_search/pkg_info/pkg_install/pkg_update/pkg_remove),
5
5
  * the /packages command, and a session_start update notification. ALL logic
6
6
  * lives in the Bun service (src/): registry access, caching, watcher,
7
7
  * catalog sync, install execution.
@@ -30,6 +30,7 @@ export interface Natives {
30
30
  setMutationApproval(value: MutationApproval, approved?: boolean): Promise<SecuritySettings>;
31
31
  install(source: string, approved?: boolean): Promise<string>;
32
32
  remove(name: string, approved?: boolean): Promise<string>;
33
+ update(source: string, approved?: boolean): Promise<string>;
33
34
  }
34
35
 
35
36
  export type PackageDaemonConnector = () => Promise<PackageDaemonPort>;
@@ -65,5 +66,6 @@ export async function createNatives(connect: PackageDaemonConnector = connectDef
65
66
  setMutationApproval: (value, approved) => call((daemon) => daemon.setMutationApproval(value, approved)),
66
67
  install: (source, approved) => call((daemon) => daemon.install(source, approved)),
67
68
  remove: (name, approved) => call((daemon) => daemon.remove(name, approved)),
69
+ update: (source, approved) => call((daemon) => daemon.update(source, approved)),
68
70
  };
69
71
  }
@@ -51,6 +51,21 @@ export async function installPackageWithPolicy(
51
51
  }
52
52
  }
53
53
 
54
+ export async function updatePackageWithPolicy(
55
+ source: string,
56
+ natives: Pick<Natives, "security" | "update">,
57
+ ctx: ApprovalContext,
58
+ ) {
59
+ try {
60
+ const approval = await approvePackageOperation("update", `pi update --extension ${source}`, natives, ctx);
61
+ if (!approval.allowed) return text(approval.message ?? "update denied");
62
+ const out = await natives.update(source, approval.approved);
63
+ return text(`${out || `Updated ${source}.`} Reload with /reload to activate.`);
64
+ } catch (error) {
65
+ return text(`update failed: ${error instanceof Error ? error.message : error}`);
66
+ }
67
+ }
68
+
54
69
  export async function removePackageWithPolicy(
55
70
  name: string,
56
71
  natives: Pick<Natives, "security" | "remove">,
@@ -121,6 +136,16 @@ export function registerTools(pi: ExtensionAPI, natives: Natives): void {
121
136
  },
122
137
  });
123
138
 
139
+ pi.registerTool({
140
+ name: "pkg_update",
141
+ label: "Pi Package Update",
142
+ description: "Update one configured Pi package through Pi's documented update command. Operation-aware approval is secure by default.",
143
+ parameters: Type.Object({ source: Type.String({ description: "configured npm:, git:, or https source" }) }),
144
+ async execute(_id, params, _signal, _onUpdate, ctx) {
145
+ return updatePackageWithPolicy(params.source, natives, ctx);
146
+ },
147
+ });
148
+
124
149
  pi.registerTool({
125
150
  name: "pkg_remove",
126
151
  label: "Pi Package Remove",
@@ -72,10 +72,10 @@ export async function showPackages(ctx: ExtensionCommandContext, natives: Native
72
72
  continue;
73
73
  }
74
74
  ctx.ui.notify(`Updating ${row.name}…`, "info");
75
- await natives.install(`npm:${row.name}@${row.latest}`, approval.approved);
76
- ctx.ui.notify(`Updated ${row.name} to ${row.latest} (takes effect after /reload)`, "info");
77
- row.version = row.latest ?? row.version;
78
- row.hasUpdate = false;
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
79
  } catch (e) {
80
80
  ctx.ui.notify(`update failed: ${e instanceof Error ? e.message : e}`, "error");
81
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Package service for the Pi agent: search/info/install/updates + /packages TUI, backed by a long-running Bun service",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/cli.ts CHANGED
@@ -35,6 +35,7 @@ usage:
35
35
  packed search <query> [--offline] [--json] search npm (or the local mirror with --offline)
36
36
  packed info <name> [--json] package details
37
37
  packed updates [--json] updates per the local mirror
38
+ packed update <source> [--approve] [--json] update one configured package through Pi
38
39
  packed mirror [--json] sync upstream into the local SQLite index
39
40
  packed installed [--json] installed pi packages
40
41
  packed catalog [--json] local package index (apt-cache stats)
@@ -98,6 +99,7 @@ const PACKAGE_COMMAND_OPERATIONS: Record<string, PackageOperation | undefined> =
98
99
  installed: "installed",
99
100
  catalog: "catalog",
100
101
  updates: "updates",
102
+ update: "update",
101
103
  mirror: "mirror",
102
104
  install: "install",
103
105
  remove: "remove",
@@ -237,8 +239,27 @@ const commands: Record<string, { usage: string; run: Command }> = {
237
239
  },
238
240
  },
239
241
 
242
+ update: {
243
+ usage: "packed update <configured-source> [--approve] [--json]",
244
+ async run(_rest, d, flags, pos) {
245
+ const source = pos[0] ?? "";
246
+ if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands["update"]!.usage}\n`);
247
+ try {
248
+ const output = await d.inst.update(source, { approved: flags.approved });
249
+ return flags.json
250
+ ? ok(`${JSON.stringify({ ok: true, source, output, reloadRequired: true })}\n`)
251
+ : ok(`${output}\nReload Pi with /reload to activate the updated package.\n`);
252
+ } catch (error) {
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ return flags.json
255
+ ? fail(`${JSON.stringify({ ok: false, source, error: message, reloadRequired: false })}\n`)
256
+ : fail(`${message}\n`);
257
+ }
258
+ },
259
+ },
260
+
240
261
  remove: {
241
- usage: "packed remove <name> [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
262
+ usage: "packed remove <name> [--approve] [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
242
263
  async run(_rest, d, flags, pos) {
243
264
  const name = pos[0] ?? "";
244
265
  if (!NAME_RE.test(name)) return usageErr(`usage: ${commands["remove"]!.usage}\n`);
@@ -299,7 +320,7 @@ export async function cliRun(args: string[], d: CliDeps): Promise<CliResult> {
299
320
  if (!cmd) return usageErr(`unknown command "${name}"\n${USAGE}`);
300
321
  const { flags, pos } = parseFlags(rest);
301
322
  try {
302
- const validMutationInput = name === "install"
323
+ const validMutationInput = name === "install" || name === "update"
303
324
  ? SOURCE_RE.test(pos[0] ?? "")
304
325
  : name === "remove" ? NAME_RE.test(pos[0] ?? "")
305
326
  : name === "security" ? (pos[0] === undefined || pos[0] === "always" || pos[0] === "never")
package/src/client.ts CHANGED
@@ -21,6 +21,7 @@ export interface PackageDaemonPort {
21
21
  setMutationApproval(value: MutationApproval, approved?: boolean): Promise<SecuritySettings>;
22
22
  install(source: string, approved?: boolean): Promise<string>;
23
23
  remove(name: string, approved?: boolean): Promise<string>;
24
+ update(source: string, approved?: boolean): Promise<string>;
24
25
  }
25
26
 
26
27
  interface MutationResponse {
@@ -113,6 +114,15 @@ export class PackageDaemonClient implements PackageDaemonPort {
113
114
  if (!result.ok) throw new PackageDaemonError(result.output || `failed to remove ${name}`, "remove");
114
115
  return result.output;
115
116
  }
117
+
118
+ async update(source: string, approved = false): Promise<string> {
119
+ const result = await this.request<MutationResponse>("/update", {
120
+ method: "POST",
121
+ body: JSON.stringify({ source, approved }),
122
+ });
123
+ if (!result.ok) throw new PackageDaemonError(result.output || `failed to update ${source}`, "update");
124
+ return result.output;
125
+ }
116
126
  }
117
127
 
118
128
  export class PackageDaemonInstaller implements Installer {
@@ -128,6 +138,10 @@ export class PackageDaemonInstaller implements Installer {
128
138
  }
129
139
  return this.client.remove(source.slice(4), options?.approved);
130
140
  }
141
+
142
+ update(source: string, options?: { approved?: boolean }): Promise<string> {
143
+ return this.client.update(source, options?.approved);
144
+ }
131
145
  }
132
146
 
133
147
  export class DaemonBackedSecurity {
@@ -150,6 +164,10 @@ export class DaemonBackedInstaller implements Installer {
150
164
  async remove(source: string, options?: { approved?: boolean }): Promise<string> {
151
165
  return new PackageDaemonInstaller(await connectPackageDaemon(this.stateDirectory)).remove(source, options);
152
166
  }
167
+
168
+ async update(source: string, options?: { approved?: boolean }): Promise<string> {
169
+ return (await connectPackageDaemon(this.stateDirectory)).update(source, options?.approved);
170
+ }
153
171
  }
154
172
 
155
173
  export class DaemonRegistry implements Registry {
package/src/install.ts CHANGED
@@ -30,4 +30,8 @@ export class ExecInstaller implements Installer {
30
30
  remove(source: string, _options?: { approved?: boolean }): Promise<string> {
31
31
  return this.run(["remove", source]);
32
32
  }
33
+
34
+ update(source: string, _options?: { approved?: boolean }): Promise<string> {
35
+ return this.run(["update", "--extension", source]);
36
+ }
33
37
  }
package/src/ports.ts CHANGED
@@ -41,6 +41,7 @@ export interface Registry {
41
41
  export interface Installer {
42
42
  install(source: string, options?: { approved?: boolean }): Promise<string>;
43
43
  remove(source: string, options?: { approved?: boolean }): Promise<string>;
44
+ update(source: string, options?: { approved?: boolean }): Promise<string>;
44
45
  }
45
46
 
46
47
  export interface InstalledPkg {
package/src/service.ts CHANGED
@@ -166,6 +166,29 @@ export function createApp(deps: Deps): { fetch: (req: Request) => Promise<Respon
166
166
  }
167
167
  }
168
168
 
169
+ if (path === "/update" && req.method === "POST") {
170
+ let source = "";
171
+ let approved = false;
172
+ try {
173
+ const body = (await req.json()) as { source?: unknown; approved?: unknown };
174
+ source = String(body.source ?? "");
175
+ approved = body.approved === true;
176
+ } catch {
177
+ /* fall through to validation */
178
+ }
179
+ if (!SOURCE_RE.test(source)) {
180
+ return err(400, "invalid source; want a configured npm:, git:, or https package source");
181
+ }
182
+ const denied = authorize("update", approved);
183
+ if (denied) return denied;
184
+ try {
185
+ const output = await deps.inst.update(source, { approved });
186
+ return json({ ok: true, source, output, reloadRequired: true });
187
+ } catch (error) {
188
+ return json({ ok: false, source, output: error instanceof Error ? error.message : String(error), reloadRequired: false });
189
+ }
190
+ }
191
+
169
192
  if (path === "/updates" && req.method === "GET") {
170
193
  const snap = await loadUpdates(deps.stateDir);
171
194
  return json(snap ?? { updates: [] });