@pi-stef/catalog 0.3.4 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-stef/catalog",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Pi extension for managing skill/package catalogs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Hardcoded list of @pi-stef packages for scope-based batch operations.
3
+ *
4
+ * Used by `ct add --scope @pi-stef` and `ct remove --scope @pi-stef`
5
+ * to identify which packages belong to the @pi-stef ecosystem.
6
+ *
7
+ * NOTE: `@pi-stef/catalog` is intentionally excluded — it manages the
8
+ * other packages and should not be batch-operated on itself.
9
+ */
10
+
11
+ import { extractNpmName } from "./source.js";
12
+
13
+ /** The catalog package itself (excluded from batch operations). */
14
+ export const CATALOG_PACKAGE_NAME = "@pi-stef/catalog";
15
+
16
+ /**
17
+ * All @pi-stef packages except the catalog itself.
18
+ *
19
+ * This list is used for `--scope @pi-stef` batch operations.
20
+ */
21
+ export const PI_STEF_PACKAGES: readonly string[] = [
22
+ "@pi-stef/agent-workflows",
23
+ "@pi-stef/atlassian",
24
+ "@pi-stef/figma",
25
+ "@pi-stef/paths",
26
+ "@pi-stef/team",
27
+ "@pi-stef/web",
28
+ ] as const;
29
+
30
+ /**
31
+ * Returns true if the given package name is a @pi-stef package
32
+ * (excluding the catalog itself).
33
+ */
34
+ export function isPiStefPackage(name: string): boolean {
35
+ return PI_STEF_PACKAGES.includes(name);
36
+ }
37
+
38
+ /**
39
+ * Returns true if the source string refers to a @pi-stef package.
40
+ *
41
+ * Handles both `npm:@scope/pkg@version` and bare package name formats.
42
+ * Explicitly excludes `@pi-stef/catalog` — it manages the others
43
+ * and should not be included in batch scope operations.
44
+ */
45
+ export function isPiStefSource(source: string): boolean {
46
+ // npm: prefixed source — extract the package name
47
+ if (source.startsWith("npm:")) {
48
+ const pkgName = extractNpmName(source.slice(4));
49
+ // Never include the catalog package itself in batch operations
50
+ if (pkgName === CATALOG_PACKAGE_NAME) return false;
51
+ return PI_STEF_PACKAGES.includes(pkgName);
52
+ }
53
+
54
+ // Non-npm source — check if it's a bare package name
55
+ return PI_STEF_PACKAGES.includes(source);
56
+ }
@@ -13,6 +13,8 @@
13
13
  import type { RatingValue } from "../catalog/ratings.js";
14
14
  import type { CommandArgs, CommandCtx } from "./types.js";
15
15
  import { addPackage } from "../catalog/crud.js";
16
+ import { sourceToKey } from "../catalog/source.js";
17
+ import { PI_STEF_PACKAGES } from "../catalog/packages.js";
16
18
  import { readCatalog, writeCatalog } from "../config/io.js";
17
19
  import { piInstall } from "../util/exec.js";
18
20
 
@@ -75,18 +77,95 @@ function resolveType(
75
77
  /**
76
78
  * Execute the `ct add` subcommand.
77
79
  *
80
+ * New syntax (preferred): `ct add <source> [--rating ...] [--type ...]`
81
+ * — name is auto-derived from source via `sourceToKey()`.
82
+ *
83
+ * Legacy syntax (deprecated): `ct add <name> <source> [--rating ...] [--type ...]`
84
+ * — still accepted but emits a deprecation warning.
85
+ *
78
86
  * Reads the catalog, validates inputs, prompts for type if needed,
79
87
  * adds the package, writes the catalog, and runs `pi install`.
80
88
  */
81
89
  export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void> {
82
90
  const { positional, flags } = args;
83
- const name = positional[0];
84
- const source = positional[1];
85
91
 
86
- // --- Validate required args -----------------------------------------------
87
- if (!name || !source) {
92
+ // --- Handle --scope batch mode ---------------------------------------------
93
+ if ("scope" in flags) {
94
+ const scope = flags["scope"];
95
+ if (scope !== "@pi-stef") {
96
+ ctx.ui.notify(`Unsupported scope: "${scope}". Use --scope @pi-stef.`, "error");
97
+ return;
98
+ }
99
+
100
+ const catalog = readCatalog(ctx.home);
101
+ const rating = resolveRating(flags);
102
+ let added = 0;
103
+ let skipped = 0;
104
+ let currentCatalog = catalog;
105
+
106
+ for (const pkg of PI_STEF_PACKAGES) {
107
+ const npmSource = `npm:${pkg}`;
108
+
109
+ // Skip if already in catalog
110
+ if (currentCatalog.packages[pkg]) {
111
+ skipped++;
112
+ continue;
113
+ }
114
+
115
+ try {
116
+ currentCatalog = addPackage(currentCatalog, pkg, npmSource, rating);
117
+ added++;
118
+ } catch (err: unknown) {
119
+ // Unexpected validation error — warn but continue
120
+ ctx.ui.notify(
121
+ `Warning: failed to add "${pkg}": ${err instanceof Error ? err.message : String(err)}`,
122
+ "warning",
123
+ );
124
+ skipped++;
125
+ }
126
+ }
127
+
128
+ if (added > 0) {
129
+ writeCatalog(currentCatalog, ctx.home);
130
+ }
131
+
132
+ // Install all added packages
133
+ if (added > 0) {
134
+ for (const pkg of PI_STEF_PACKAGES) {
135
+ if (currentCatalog.packages[pkg]?.source === `npm:${pkg}`) {
136
+ try {
137
+ await piInstall(`npm:${pkg}`);
138
+ } catch {
139
+ ctx.ui.notify(`Warning: install of "${pkg}" failed`, "warning");
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ ctx.ui.notify(
146
+ `Scope @pi-stef: added ${added}, skipped ${skipped} (already in catalog)`,
147
+ "info",
148
+ );
149
+ return;
150
+ }
151
+
152
+ // --- Handle legacy 2-arg syntax: ct add <name> <source> -------------------
153
+ let name: string;
154
+ let source: string;
155
+
156
+ if (positional.length >= 2) {
157
+ name = positional[0];
158
+ source = positional[1];
159
+ ctx.ui.notify(
160
+ `"ct add <name> <source>" is legacy. Use "ct add <source>" — name is auto-derived.`,
161
+ "warning",
162
+ );
163
+ } else if (positional.length === 1) {
164
+ source = positional[0];
165
+ name = sourceToKey(source);
166
+ } else {
88
167
  ctx.ui.notify(
89
- "Usage: ct add <name> <source> [--rating <core|useful|debatable>] [--type <skill|pi-native>]",
168
+ "Usage: ct add <source> [--rating <core|useful|debatable>] [--type <skill|pi-native>]",
90
169
  "error",
91
170
  );
92
171
  return;
@@ -31,6 +31,7 @@ export const SUBCOMMAND_DEFS: readonly SubcommandDef[] = [
31
31
  { name: "add", aliases: ["a"], description: "Add a package to the catalog" },
32
32
  { name: "remove", aliases: ["rm"], description: "Remove a package from the catalog" },
33
33
  { name: "toggle", description: "Toggle a package's rating" },
34
+ { name: "update", aliases: ["up"], description: "Update packages to latest versions" },
34
35
  { name: "disable", description: "Disable a package" },
35
36
  { name: "enable", description: "Enable a package" },
36
37
  { name: "push", description: "Push catalog to remote gist" },
@@ -41,6 +42,7 @@ export const SUBCOMMAND_DEFS: readonly SubcommandDef[] = [
41
42
  { name: "verify", description: "Verify catalog integrity" },
42
43
  { name: "profiles", description: "List available profiles" },
43
44
  { name: "profile", description: "Show or switch active profile" },
45
+ { name: "reset", description: "Reset catalog: uninstall all @pi-stef packages and delete config" },
44
46
  ] as const;
45
47
 
46
48
  // ---------------------------------------------------------------------------
@@ -12,7 +12,8 @@
12
12
 
13
13
  import type { CommandArgs, CommandCtx } from "./types.js";
14
14
  import { removePackage } from "../catalog/crud.js";
15
- import { readCatalog, writeCatalog } from "../config/io.js";
15
+ import { isPiStefSource } from "../catalog/packages.js";
16
+ import { readCatalog, writeCatalog, readLock, writeLock } from "../config/io.js";
16
17
  import { piUninstall } from "../util/exec.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
@@ -41,6 +42,77 @@ export async function removeCommand(
41
42
  ctx: RemoveCtx,
42
43
  ): Promise<void> {
43
44
  const { positional, flags } = args;
45
+
46
+ // --- Handle --scope batch mode ---------------------------------------------
47
+ if ("scope" in flags) {
48
+ const scope = flags["scope"];
49
+ if (scope !== "@pi-stef") {
50
+ ctx.ui.notify(`Unsupported scope: "${scope}". Use --scope @pi-stef.`, "error");
51
+ return;
52
+ }
53
+
54
+ const catalog = readCatalog(ctx.home);
55
+ const lock = readLock(ctx.home);
56
+
57
+ // isPiStefSource returns false for @pi-stef/catalog by design (see packages.ts)
58
+ const piStefNames = Object.keys(catalog.packages).filter(
59
+ (name) => isPiStefSource(catalog.packages[name].source),
60
+ );
61
+
62
+ if (piStefNames.length === 0) {
63
+ ctx.ui.notify("No @pi-stef packages found in catalog", "info");
64
+ return;
65
+ }
66
+
67
+ // Confirmation
68
+ const skipConfirm = "yes" in flags || "y" in flags;
69
+ if (!skipConfirm && ctx.ui.confirm) {
70
+ const confirmed = await ctx.ui.confirm(
71
+ `Remove ${piStefNames.length} @pi-stef packages from catalog?`,
72
+ );
73
+ if (!confirmed) {
74
+ ctx.ui.notify("Removal cancelled", "info");
75
+ return;
76
+ }
77
+ }
78
+
79
+ // Capture sources before removing
80
+ const sources: Record<string, string> = {};
81
+ for (const name of piStefNames) {
82
+ sources[name] = catalog.packages[name].source;
83
+ }
84
+
85
+ // Remove all from catalog and lock file
86
+ for (const name of piStefNames) {
87
+ delete catalog.packages[name];
88
+ if (lock.packages[name]) {
89
+ delete lock.packages[name];
90
+ }
91
+ }
92
+
93
+ writeCatalog(catalog, ctx.home);
94
+ writeLock(lock, ctx.home);
95
+
96
+ // Uninstall all
97
+ let uninstalled = 0;
98
+ let failed = 0;
99
+ for (const name of piStefNames) {
100
+ try {
101
+ await piUninstall(sources[name]);
102
+ uninstalled++;
103
+ } catch {
104
+ ctx.ui.notify(`Warning: uninstall of "${name}" failed`, "warning");
105
+ failed++;
106
+ }
107
+ }
108
+
109
+ ctx.ui.notify(
110
+ `Scope @pi-stef: removed ${piStefNames.length}, uninstalled ${uninstalled}${failed > 0 ? ` (${failed} uninstall failed)` : ""}`,
111
+ failed > 0 ? "warning" : "info",
112
+ );
113
+ return;
114
+ }
115
+
44
116
  const name = positional[0];
45
117
 
46
118
  // --- Validate required args -----------------------------------------------
@@ -81,6 +153,13 @@ export async function removeCommand(
81
153
  const updated = removePackage(catalog, name);
82
154
  writeCatalog(updated, ctx.home);
83
155
 
156
+ // --- Remove from lock file ------------------------------------------------
157
+ const lock = readLock(ctx.home);
158
+ if (lock.packages[name]) {
159
+ delete lock.packages[name];
160
+ writeLock(lock, ctx.home);
161
+ }
162
+
84
163
  ctx.ui.notify(`Removed "${name}" from catalog`, "info");
85
164
 
86
165
  // --- Run pi uninstall -----------------------------------------------------
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `ct reset` subcommand implementation.
3
+ *
4
+ * Full nuke: uninstalls all @pi-stef packages, deletes gist refs,
5
+ * deletes config files (cat.yaml, catalog.lock.json, .gist),
6
+ * and removes the empty catalog directory.
7
+ *
8
+ * Usage:
9
+ * - `ct reset` — prompts for confirmation
10
+ * - `ct reset --yes` — skips confirmation
11
+ *
12
+ * This is a destructive operation. The catalog can be re-initialized
13
+ * with `ct init` after resetting.
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import type { CommandArgs, CommandCtx } from "./types.js";
18
+ import { isPiStefSource } from "../catalog/packages.js";
19
+ import { catalogDir, catalogFile, lockFile } from "../config/paths.js";
20
+ import { piUninstall } from "../util/exec.js";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Context for `resetCommand`, extending the base with `confirm`. */
27
+ export interface ResetCtx extends CommandCtx {
28
+ ui: CommandCtx["ui"] & {
29
+ confirm?: (message: string) => Promise<boolean>;
30
+ };
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // resetCommand
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Execute the `ct reset` subcommand.
39
+ *
40
+ * 1. Check cat.yaml exists
41
+ * 2. Confirm (skippable with --yes)
42
+ * 3. Find @pi-stef packages (isPiStefSource excludes catalog)
43
+ * 4. pi uninstall each
44
+ * 5. Delete config files with fs.rmSync({ recursive: true, force: true })
45
+ * 6. Remove empty catalog directory
46
+ */
47
+ export async function resetCommand(
48
+ args: CommandArgs,
49
+ ctx: ResetCtx,
50
+ ): Promise<void> {
51
+ const { flags } = args;
52
+
53
+ // --- Check cat.yaml exists ------------------------------------------------
54
+ const catPath = catalogFile(ctx.home);
55
+ if (!fs.existsSync(catPath)) {
56
+ ctx.ui.notify("No catalog found. Run `ct init` first.", "error");
57
+ return;
58
+ }
59
+
60
+ // --- Confirmation ----------------------------------------------------------
61
+ const skipConfirm = "yes" in flags || "y" in flags;
62
+ if (!skipConfirm && ctx.ui.confirm) {
63
+ const confirmed = await ctx.ui.confirm(
64
+ "This will uninstall all @pi-stef packages and delete all catalog config. Continue?",
65
+ );
66
+ if (!confirmed) {
67
+ ctx.ui.notify("Reset cancelled", "info");
68
+ return;
69
+ }
70
+ }
71
+
72
+ // --- Find @pi-stef packages -----------------------------------------------
73
+ // Read catalog directly (raw YAML) to avoid schema validation on corrupt files
74
+ let packages: Record<string, { source: string }> = {};
75
+ try {
76
+ const yaml = await import("js-yaml");
77
+ const content = fs.readFileSync(catPath, "utf8");
78
+ const parsed = yaml.load(content) as { packages?: Record<string, { source: string }> };
79
+ packages = parsed?.packages ?? {};
80
+ } catch {
81
+ // If YAML is corrupt, we still want to delete config files
82
+ ctx.ui.notify("Warning: could not parse cat.yaml — skipping uninstall step", "warning");
83
+ }
84
+
85
+ const piStefNames = Object.keys(packages).filter(
86
+ (name) => isPiStefSource(packages[name].source),
87
+ );
88
+
89
+ // --- Uninstall @pi-stef packages ------------------------------------------
90
+ let uninstalled = 0;
91
+ let failed = 0;
92
+
93
+ for (const name of piStefNames) {
94
+ try {
95
+ await piUninstall(packages[name].source);
96
+ uninstalled++;
97
+ } catch {
98
+ ctx.ui.notify(`Warning: uninstall of "${name}" failed`, "warning");
99
+ failed++;
100
+ }
101
+ }
102
+
103
+ // --- Delete config files --------------------------------------------------
104
+ const dir = catalogDir(ctx.home);
105
+ const gistPath = `${dir}/.gist`;
106
+
107
+ // Delete individual files first, then the directory
108
+ for (const filePath of [catPath, lockFile(ctx.home), gistPath]) {
109
+ try {
110
+ fs.rmSync(filePath, { force: true });
111
+ } catch {
112
+ // Ignore errors — file may not exist
113
+ }
114
+ }
115
+
116
+ // Remove the catalog directory itself
117
+ try {
118
+ fs.rmSync(dir, { recursive: true, force: true });
119
+ } catch {
120
+ // Ignore errors — directory may not exist or be non-empty
121
+ }
122
+
123
+ // --- Report ----------------------------------------------------------------
124
+ const parts: string[] = [];
125
+ if (piStefNames.length > 0) {
126
+ parts.push(`uninstalled ${uninstalled}/${piStefNames.length} packages`);
127
+ }
128
+ parts.push("deleted config files");
129
+
130
+ ctx.ui.notify(
131
+ `Reset complete: ${parts.join(", ")}${failed > 0 ? ` (${failed} uninstall failed)` : ""}`,
132
+ failed > 0 ? "warning" : "info",
133
+ );
134
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `ct update` subcommand implementation.
3
+ *
4
+ * Updates packages to their latest versions by running `pi update <source>`.
5
+ *
6
+ * Usage:
7
+ * - `ct update <name>` — update a single package by catalog name
8
+ * - `ct update --all` — update all packages in the catalog
9
+ *
10
+ * After updating, a `/ct sync` should be run to persist changes to the remote gist.
11
+ */
12
+
13
+ import type { CommandArgs, CommandCtx } from "./types.js";
14
+ import { readCatalog } from "../config/io.js";
15
+ import { piUpdate } from "../util/exec.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // updateCommand
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Execute the `ct update` subcommand.
23
+ *
24
+ * Reads the catalog, resolves the target package(s), and runs
25
+ * `pi update <source>` for each.
26
+ */
27
+ export async function updateCommand(
28
+ args: CommandArgs,
29
+ ctx: CommandCtx,
30
+ ): Promise<void> {
31
+ const { positional, flags } = args;
32
+ const updateAll = "all" in flags;
33
+ const name = positional[0];
34
+
35
+ if (!name && !updateAll) {
36
+ ctx.ui.notify("Usage: ct update <name> | ct update --all", "error");
37
+ return;
38
+ }
39
+
40
+ const catalog = readCatalog(ctx.home);
41
+ const packages = catalog.packages;
42
+
43
+ // --- Single package update ------------------------------------------------
44
+ if (name) {
45
+ const entry = packages[name];
46
+ if (!entry) {
47
+ ctx.ui.notify(`Package "${name}" not found in catalog`, "error");
48
+ return;
49
+ }
50
+
51
+ try {
52
+ await piUpdate(entry.source);
53
+ ctx.ui.notify(`Updated "${name}"`, "info");
54
+ } catch {
55
+ ctx.ui.notify(`Warning: update of "${name}" failed`, "warning");
56
+ }
57
+ return;
58
+ }
59
+
60
+ // --- Update all -----------------------------------------------------------
61
+ const names = Object.keys(packages);
62
+ if (names.length === 0) {
63
+ ctx.ui.notify("Catalog is empty — nothing to update", "info");
64
+ return;
65
+ }
66
+
67
+ let updated = 0;
68
+ let failed = 0;
69
+
70
+ for (const pkgName of names) {
71
+ const entry = packages[pkgName];
72
+ try {
73
+ await piUpdate(entry.source);
74
+ updated++;
75
+ } catch {
76
+ ctx.ui.notify(`Warning: update of "${pkgName}" failed`, "warning");
77
+ failed++;
78
+ }
79
+ }
80
+
81
+ ctx.ui.notify(
82
+ `Updated ${updated}/${names.length} packages${failed > 0 ? ` (${failed} failed)` : ""}`,
83
+ failed > 0 ? "warning" : "info",
84
+ );
85
+ }
package/src/register.ts CHANGED
@@ -15,6 +15,8 @@ import {
15
15
  import { addCommand, type AddCtx } from "./commands/add.js";
16
16
  import { initCommand, type InitContext } from "./commands/init.js";
17
17
  import { removeCommand, type RemoveCtx } from "./commands/remove.js";
18
+ import { updateCommand } from "./commands/update.js";
19
+ import { resetCommand, type ResetCtx } from "./commands/reset.js";
18
20
  import {
19
21
  toggleCommand,
20
22
  enableCommand,
@@ -109,6 +111,9 @@ async function handleSubcommand(
109
111
  case "toggle":
110
112
  await toggleCommand(parsed, ctx as ToggleCtx);
111
113
  break;
114
+ case "update":
115
+ await updateCommand(parsed, ctx);
116
+ break;
112
117
  case "enable":
113
118
  await enableCommand(parsed, ctx as ToggleCtx);
114
119
  break;
@@ -133,6 +138,9 @@ async function handleSubcommand(
133
138
  case "profile":
134
139
  await profileCommand(parsed, ctx as ProfilesCtx);
135
140
  break;
141
+ case "reset":
142
+ await resetCommand(parsed, ctx as ResetCtx);
143
+ break;
136
144
  default:
137
145
  ctx.ui.notify(`ct ${canonical}: not yet implemented`, "info");
138
146
  }
@@ -209,24 +217,27 @@ export function registerCatalog(pi: ExtensionAPI): void {
209
217
  name: "ct_add",
210
218
  label: "Catalog Add",
211
219
  description:
212
- "Add a package to the catalog by name and source. Source must start with 'npm:' or 'git:'.",
220
+ "Add a package to the catalog by source. Name is auto-derived from source. Source must start with 'npm:' or 'git:'.",
213
221
  promptSnippet: "Add a package to the catalog",
214
222
  promptGuidelines: [
215
223
  "Use ct_add when the user asks to add a new package or skill to their catalog.",
216
224
  ],
217
225
  parameters: Type.Object({
218
- name: Type.String({ description: "Package name" }),
219
226
  source: Type.String({ description: "Package source (npm:… or git:…)" }),
220
227
  rating: Type.Optional(Type.String({ description: "Initial rating (core, useful, debatable)" })),
228
+ scope: Type.Optional(Type.String({ description: "Batch scope: '@pi-stef' to add all @pi-stef packages" })),
221
229
  }),
222
230
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
223
231
  try {
232
+ const flags: Record<string, true | string> = {};
233
+ if (params.rating) flags.rating = params.rating;
234
+ if (params.scope) flags.scope = params.scope;
224
235
  const args: CommandArgs = {
225
- positional: [params.name, params.source],
226
- flags: params.rating ? { rating: params.rating } : {},
236
+ positional: params.scope ? [] : [params.source],
237
+ flags,
227
238
  };
228
239
  await addCommand(args, ctx as unknown as AddCtx);
229
- return { content: [{ type: "text" as const, text: `Added ${params.name}.` }], details: undefined as unknown };
240
+ return { content: [{ type: "text" as const, text: `Added ${params.source}.` }], details: undefined as unknown };
230
241
  } catch (err) {
231
242
  return { content: [{ type: "text" as const, text: `Add failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
232
243
  }
@@ -242,13 +253,20 @@ export function registerCatalog(pi: ExtensionAPI): void {
242
253
  "Use ct_remove when the user asks to remove or uninstall a package from their catalog.",
243
254
  ],
244
255
  parameters: Type.Object({
245
- name: Type.String({ description: "Package name to remove" }),
256
+ name: Type.Optional(Type.String({ description: "Package name to remove" })),
257
+ scope: Type.Optional(Type.String({ description: "Batch scope: '@pi-stef' to remove all @pi-stef packages" })),
246
258
  }),
247
259
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
248
260
  try {
249
- const args: CommandArgs = { positional: [params.name], flags: {} };
261
+ const flags: Record<string, true | string> = {};
262
+ if (params.scope) flags.scope = params.scope;
263
+ const args: CommandArgs = {
264
+ positional: params.name ? [params.name] : [],
265
+ flags,
266
+ };
250
267
  await removeCommand(args, ctx as unknown as RemoveCtx);
251
- return { content: [{ type: "text" as const, text: `Removed ${params.name}.` }], details: undefined as unknown };
268
+ const label = params.scope ? `Scope ${params.scope}` : `${params.name}`;
269
+ return { content: [{ type: "text" as const, text: `Removed ${label}.` }], details: undefined as unknown };
252
270
  } catch (err) {
253
271
  return { content: [{ type: "text" as const, text: `Remove failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
254
272
  }
@@ -278,6 +296,58 @@ export function registerCatalog(pi: ExtensionAPI): void {
278
296
  },
279
297
  });
280
298
 
299
+ pi.registerTool({
300
+ name: "ct_update",
301
+ label: "Catalog Update",
302
+ description:
303
+ "Update packages to their latest versions. Run `pi update` behind the scenes.",
304
+ promptSnippet: "Update catalog packages",
305
+ promptGuidelines: [
306
+ "Use ct_update when the user asks to update one or more packages in their catalog.",
307
+ ],
308
+ parameters: Type.Object({
309
+ name: Type.Optional(Type.String({ description: "Package name to update (omit for --all)" })),
310
+ all: Type.Optional(Type.Boolean({ description: "Update all packages" })),
311
+ }),
312
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
313
+ try {
314
+ const positional = params.name ? [params.name] : [];
315
+ const flags: Record<string, true | string> = {};
316
+ if (params.all) flags.all = true;
317
+ const args: CommandArgs = { positional, flags };
318
+ await updateCommand(args, ctx);
319
+ return { content: [{ type: "text" as const, text: "Update completed." }], details: undefined as unknown };
320
+ } catch (err) {
321
+ return { content: [{ type: "text" as const, text: `Update failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
322
+ }
323
+ },
324
+ });
325
+
326
+ pi.registerTool({
327
+ name: "ct_reset",
328
+ label: "Catalog Reset",
329
+ description:
330
+ "Full nuke: uninstall all @pi-stef packages, delete gist refs, delete config files.",
331
+ promptSnippet: "Reset catalog completely",
332
+ promptGuidelines: [
333
+ "Use ct_reset when the user wants to completely remove all @pi-stef packages and catalog config.",
334
+ ],
335
+ parameters: Type.Object({
336
+ yes: Type.Optional(Type.Boolean({ description: "Skip confirmation prompt" })),
337
+ }),
338
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
339
+ try {
340
+ const flags: Record<string, true | string> = {};
341
+ if (params.yes) flags.yes = true;
342
+ const args: CommandArgs = { positional: [], flags };
343
+ await resetCommand(args, ctx as unknown as ResetCtx);
344
+ return { content: [{ type: "text" as const, text: "Reset completed." }], details: undefined as unknown };
345
+ } catch (err) {
346
+ return { content: [{ type: "text" as const, text: `Reset failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
347
+ }
348
+ },
349
+ });
350
+
281
351
  pi.registerTool({
282
352
  name: "ct_status",
283
353
  label: "Catalog Status",
package/src/util/exec.ts CHANGED
@@ -158,3 +158,15 @@ export function piUninstall(
158
158
  ): Promise<ExecResult> {
159
159
  return execCommand("pi", ["uninstall", packageName], options);
160
160
  }
161
+
162
+ /**
163
+ * Update a pi package from the given source.
164
+ *
165
+ * Runs `pi update <source>`.
166
+ */
167
+ export function piUpdate(
168
+ source: string,
169
+ options?: PiExecOptions,
170
+ ): Promise<ExecResult> {
171
+ return execCommand("pi", ["update", source], options);
172
+ }