@danypops/pi-packed 0.4.0 → 0.5.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 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,8 +1,11 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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
+ "bin": {
7
+ "packed": "src/cli.ts"
8
+ },
6
9
  "keywords": [
7
10
  "pi-package"
8
11
  ],
package/src/cli.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env bun
1
2
  /**
2
3
  * cli.ts — second driving adapter (humans drive the same hexagon ports).
3
4
  * cliRun is pure: ({code, out}) in, no I/O — the entry point prints.
@@ -35,6 +36,7 @@ usage:
35
36
  packed search <query> [--offline] [--json] search npm (or the local mirror with --offline)
36
37
  packed info <name> [--json] package details
37
38
  packed updates [--json] updates per the local mirror
39
+ packed update <source> [--approve] [--json] update one configured package through Pi
38
40
  packed mirror [--json] sync upstream into the local SQLite index
39
41
  packed installed [--json] installed pi packages
40
42
  packed catalog [--json] local package index (apt-cache stats)
@@ -98,6 +100,7 @@ const PACKAGE_COMMAND_OPERATIONS: Record<string, PackageOperation | undefined> =
98
100
  installed: "installed",
99
101
  catalog: "catalog",
100
102
  updates: "updates",
103
+ update: "update",
101
104
  mirror: "mirror",
102
105
  install: "install",
103
106
  remove: "remove",
@@ -237,8 +240,27 @@ const commands: Record<string, { usage: string; run: Command }> = {
237
240
  },
238
241
  },
239
242
 
243
+ update: {
244
+ usage: "packed update <configured-source> [--approve] [--json]",
245
+ async run(_rest, d, flags, pos) {
246
+ const source = pos[0] ?? "";
247
+ if (!SOURCE_RE.test(source)) return usageErr(`usage: ${commands["update"]!.usage}\n`);
248
+ try {
249
+ const output = await d.inst.update(source, { approved: flags.approved });
250
+ return flags.json
251
+ ? ok(`${JSON.stringify({ ok: true, source, output, reloadRequired: true })}\n`)
252
+ : ok(`${output}\nReload Pi with /reload to activate the updated package.\n`);
253
+ } catch (error) {
254
+ const message = error instanceof Error ? error.message : String(error);
255
+ return flags.json
256
+ ? fail(`${JSON.stringify({ ok: false, source, error: message, reloadRequired: false })}\n`)
257
+ : fail(`${message}\n`);
258
+ }
259
+ },
260
+ },
261
+
240
262
  remove: {
241
- usage: "packed remove <name> [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
263
+ usage: "packed remove <name> [--approve] [--json] (bare npm name, e.g. pi-lsp or @scope/pkg)",
242
264
  async run(_rest, d, flags, pos) {
243
265
  const name = pos[0] ?? "";
244
266
  if (!NAME_RE.test(name)) return usageErr(`usage: ${commands["remove"]!.usage}\n`);
@@ -299,7 +321,7 @@ export async function cliRun(args: string[], d: CliDeps): Promise<CliResult> {
299
321
  if (!cmd) return usageErr(`unknown command "${name}"\n${USAGE}`);
300
322
  const { flags, pos } = parseFlags(rest);
301
323
  try {
302
- const validMutationInput = name === "install"
324
+ const validMutationInput = name === "install" || name === "update"
303
325
  ? SOURCE_RE.test(pos[0] ?? "")
304
326
  : name === "remove" ? NAME_RE.test(pos[0] ?? "")
305
327
  : 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: [] });