@pi-stef/catalog 0.3.5 → 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.
@@ -120,6 +120,7 @@ export async function syncCommand(
120
120
  // --- 2. Pull remote catalog (into memory only) ---------------------------
121
121
  let remoteCatalog = false;
122
122
  let pulledData: { catalog: CatalogYaml; lock: LockFile } | undefined;
123
+ ctx.ui.setWorkingMessage?.("Pulling remote catalog...");
123
124
  try {
124
125
  pulledData = await pullCatalog(profile, ctx.home);
125
126
  remoteCatalog = true;
@@ -129,6 +130,7 @@ export async function syncCommand(
129
130
  ctx.ui.notify(`Pull failed: ${message}`, "warning");
130
131
  summary.errors.push(message);
131
132
  }
133
+ ctx.ui.setWorkingMessage?.();
132
134
 
133
135
  // --- 3. Reconcile --------------------------------------------------------
134
136
  // Use pulled catalog if available, otherwise read from disk
@@ -172,6 +174,12 @@ export async function syncCommand(
172
174
  }
173
175
  }
174
176
 
177
+ // Track whether the rebuilt lock (from buildSyncedLock) differs from the
178
+ // remote lock. This catches the case where `pi update` bumped an installed
179
+ // version but the catalog source string hasn't changed, so the pre-pull
180
+ // lock comparison wouldn't detect it.
181
+ let rebuiltLockDiffers = false;
182
+
175
183
  const installed = scanInstalled(ctx.home);
176
184
 
177
185
  // Build catalog entries for reconcile
@@ -220,7 +228,9 @@ export async function syncCommand(
220
228
  writeLock(pulledData.lock, ctx.home);
221
229
  }
222
230
 
231
+ ctx.ui.setWorkingMessage?.("Executing actions...");
223
232
  const result = await executeActions(plan, { home: ctx.home });
233
+ ctx.ui.setWorkingMessage?.();
224
234
 
225
235
  for (const { error } of result.errors) {
226
236
  ctx.ui.notify(`Action error: ${error.message}`, "warning");
@@ -234,6 +244,28 @@ export async function syncCommand(
234
244
  // Always write a populated lock so "last sync" is accurate
235
245
  const syncedLock = buildSyncedLock(catalog, installed);
236
246
  writeLock(syncedLock, ctx.home);
247
+
248
+ // Compare rebuilt lock versions against remote to detect version drift
249
+ // from external `pi update` calls that bumped installed versions without
250
+ // changing the catalog source string.
251
+ if (pulledData) {
252
+ const remoteLock = pulledData.lock;
253
+ for (const [key, rebuiltEntry] of Object.entries(syncedLock.packages)) {
254
+ const remoteEntry = remoteLock.packages[key];
255
+ if (!remoteEntry || remoteEntry.version !== rebuiltEntry.version) {
256
+ rebuiltLockDiffers = true;
257
+ break;
258
+ }
259
+ }
260
+ if (!rebuiltLockDiffers) {
261
+ for (const key of Object.keys(remoteLock.packages)) {
262
+ if (!(key in syncedLock.packages)) {
263
+ rebuiltLockDiffers = true;
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ }
237
269
  }
238
270
 
239
271
  // --- 5. Push if changed --------------------------------------------------
@@ -251,7 +283,8 @@ export async function syncCommand(
251
283
  const hasGist = readCachedGistId(ctx.home) !== undefined;
252
284
  const localHasPackages = Object.keys(catalog.packages).length > 0;
253
285
 
254
- if (force || summary.actionCount > 0 || hasLocalOnlyPackages || hasLocalLockChanges || (!hasGist && localHasPackages)) {
286
+ if (force || summary.actionCount > 0 || hasLocalOnlyPackages || hasLocalLockChanges || rebuiltLockDiffers || (!hasGist && localHasPackages)) {
287
+ ctx.ui.setWorkingMessage?.("Pushing to gist...");
255
288
  try {
256
289
  const updatedCatalog = readCatalog(ctx.home);
257
290
  const updatedLock = readLock(ctx.home);
@@ -268,6 +301,7 @@ export async function syncCommand(
268
301
  ctx.ui.notify(`Push failed: ${message}`, "error");
269
302
  summary.errors.push(message);
270
303
  }
304
+ ctx.ui.setWorkingMessage?.();
271
305
  }
272
306
 
273
307
  // --- 6. Report summary ---------------------------------------------------
@@ -282,7 +316,7 @@ export async function syncCommand(
282
316
  }
283
317
  }
284
318
 
285
- if (summary.actionCount === 0 && summary.errors.length === 0 && !force && !hasLocalOnlyPackages && !hasLocalLockChanges) {
319
+ if (summary.actionCount === 0 && summary.errors.length === 0 && !force && !hasLocalOnlyPackages && !hasLocalLockChanges && !rebuiltLockDiffers) {
286
320
  ctx.ui.notify("Catalog already up to date.", "info");
287
321
  return;
288
322
  }
@@ -298,6 +332,9 @@ export async function syncCommand(
298
332
  if (hasLocalLockChanges) {
299
333
  parts.push("Pushed local version updates.");
300
334
  }
335
+ if (rebuiltLockDiffers) {
336
+ parts.push("Rebuilt lock (version drift detected).");
337
+ }
301
338
  if (plan.installs.length > 0) {
302
339
  parts.push(`${plan.installs.length} install(s): ${plan.installs.map((a) => a.key).join(", ")}`);
303
340
  }
@@ -1,12 +1,9 @@
1
1
  /**
2
2
  * `ct toggle`, `ct enable`, and `ct disable` subcommand implementations.
3
3
  *
4
- * - `ct toggle <name>` cycles a package's rating through the cycle:
5
- * core useful debatable disabled core
6
- * - `ct enable <name>` sets a disabled package back to its previous rating
7
- * (or "core" if no previous rating stored). No-op when already enabled.
8
- * - `ct disable <name>` sets rating to disabled, saves the previous rating,
9
- * and runs `pi uninstall` to remove the package.
4
+ * - `ct toggle <name>` toggles a package's enabled state (enabled ↔ disabled)
5
+ * - `ct enable <name>` enables a disabled package. No-op when already enabled.
6
+ * - `ct disable <name>` disables a package and runs `pi uninstall`.
10
7
  *
11
8
  * All commands read/write `cat.yaml` via `readCatalog` / `writeCatalog`
12
9
  * and provide user feedback through `ctx.ui.notify`.
@@ -31,7 +28,7 @@ export type ToggleCtx = CommandCtx;
31
28
  /**
32
29
  * Execute the `ct toggle` subcommand.
33
30
  *
34
- * Cycles the package's rating through: core useful → debatable → disabled → core.
31
+ * Toggles the package's enabled state: enabled disabled.
35
32
  */
36
33
  export async function toggleCommand(
37
34
  args: CommandArgs,
@@ -49,8 +46,9 @@ export async function toggleCommand(
49
46
  try {
50
47
  const updated = togglePackage(catalog, name);
51
48
  writeCatalog(updated, ctx.home);
49
+ const isEnabled = updated.packages[name].enabled !== false;
52
50
  ctx.ui.notify(
53
- `Toggled "${name}" to ${updated.packages[name].rating}`,
51
+ `Toggled "${name}" now ${isEnabled ? "enabled" : "disabled"}`,
54
52
  "info",
55
53
  );
56
54
  } catch (err: unknown) {
@@ -66,8 +64,7 @@ export async function toggleCommand(
66
64
  /**
67
65
  * Execute the `ct enable` subcommand.
68
66
  *
69
- * Restores a disabled package to its previous rating (or "core").
70
- * No-op when the package is already enabled.
67
+ * Enables a disabled package. No-op when the package is already enabled.
71
68
  */
72
69
  export async function enableCommand(
73
70
  args: CommandArgs,
@@ -92,10 +89,7 @@ export async function enableCommand(
92
89
  }
93
90
 
94
91
  writeCatalog(updated, ctx.home);
95
- ctx.ui.notify(
96
- `Enabled "${name}" (rating: ${updated.packages[name].rating})`,
97
- "info",
98
- );
92
+ ctx.ui.notify(`Enabled "${name}"`, "info");
99
93
  } catch (err: unknown) {
100
94
  const message = err instanceof Error ? err.message : String(err);
101
95
  ctx.ui.notify(message, "error");
@@ -109,8 +103,7 @@ export async function enableCommand(
109
103
  /**
110
104
  * Execute the `ct disable` subcommand.
111
105
  *
112
- * Sets the package rating to "disabled", saves the previous rating for later
113
- * restoration, and runs `pi uninstall` to remove the package.
106
+ * Disables a package and runs `pi uninstall` to remove it.
114
107
  */
115
108
  export async function disableCommand(
116
109
  args: CommandArgs,
@@ -136,6 +129,7 @@ export async function disableCommand(
136
129
  }
137
130
 
138
131
  // Run pi uninstall after disabling
132
+ ctx.ui.setWorkingMessage?.(`Uninstalling ${name}...`);
139
133
  try {
140
134
  await piUninstall(name);
141
135
  } catch {
@@ -144,4 +138,5 @@ export async function disableCommand(
144
138
  "warning",
145
139
  );
146
140
  }
141
+ ctx.ui.setWorkingMessage?.();
147
142
  }
@@ -32,6 +32,8 @@ export interface CommandArgs {
32
32
  export interface CommandCtx {
33
33
  ui: {
34
34
  notify: (msg: string, type?: "error" | "info" | "warning") => void;
35
+ /** Show a temporary working message (e.g. "Adding..."). Pass undefined or no arg to clear. */
36
+ setWorkingMessage?: (msg?: string) => void;
35
37
  };
36
38
  /** Home directory override (for testing). */
37
39
  home?: string;
@@ -0,0 +1,113 @@
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
+ import { checkSetupForSource, formatSetupStatus } from "../catalog/setup.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // updateCommand
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Execute the `ct update` subcommand.
24
+ *
25
+ * Reads the catalog, resolves the target package(s), and runs
26
+ * `pi update <source>` for each.
27
+ */
28
+ export async function updateCommand(
29
+ args: CommandArgs,
30
+ ctx: CommandCtx,
31
+ ): Promise<void> {
32
+ const { positional, flags } = args;
33
+ const updateAll = "all" in flags;
34
+ const name = positional[0];
35
+
36
+ if (!name && !updateAll) {
37
+ ctx.ui.notify("Usage: ct update <name> | ct update --all", "error");
38
+ return;
39
+ }
40
+
41
+ const catalog = readCatalog(ctx.home);
42
+ const packages = catalog.packages;
43
+
44
+ // --- Single package update ------------------------------------------------
45
+ if (name) {
46
+ const entry = packages[name];
47
+ if (!entry) {
48
+ ctx.ui.notify(`Package "${name}" not found in catalog`, "error");
49
+ return;
50
+ }
51
+
52
+ ctx.ui.setWorkingMessage?.(`Updating ${name}...`);
53
+ try {
54
+ await piUpdate(entry.source);
55
+ ctx.ui.notify(`Updated "${name}"`, "info");
56
+ } catch {
57
+ ctx.ui.notify(`Warning: update of "${name}" failed`, "warning");
58
+ }
59
+ ctx.ui.setWorkingMessage?.();
60
+
61
+ // Check setup requirements after update
62
+ const setup = checkSetupForSource(entry.source, ctx.home);
63
+ if (setup && !setup.ok) {
64
+ ctx.ui.notify(
65
+ `Setup incomplete for "${name}": ${formatSetupStatus(setup)}`,
66
+ "warning",
67
+ );
68
+ }
69
+ return;
70
+ }
71
+
72
+ // --- Update all -----------------------------------------------------------
73
+ const names = Object.keys(packages);
74
+ if (names.length === 0) {
75
+ ctx.ui.notify("Catalog is empty — nothing to update", "info");
76
+ return;
77
+ }
78
+
79
+ let updated = 0;
80
+ let failed = 0;
81
+ const setupWarnings: string[] = [];
82
+
83
+ for (const pkgName of names) {
84
+ const entry = packages[pkgName];
85
+ ctx.ui.setWorkingMessage?.(`Updating ${pkgName} (${updated + 1}/${names.length})...`);
86
+ try {
87
+ await piUpdate(entry.source);
88
+ updated++;
89
+
90
+ // Check setup after successful update
91
+ const setup = checkSetupForSource(entry.source, ctx.home);
92
+ if (setup && !setup.ok) {
93
+ setupWarnings.push(`${pkgName}: ${formatSetupStatus(setup)}`);
94
+ }
95
+ } catch {
96
+ ctx.ui.notify(`Warning: update of "${pkgName}" failed`, "warning");
97
+ failed++;
98
+ }
99
+ }
100
+ ctx.ui.setWorkingMessage?.();
101
+
102
+ const parts: string[] = [
103
+ `Updated ${updated}/${names.length} packages${failed > 0 ? ` (${failed} failed)` : ""}`,
104
+ ];
105
+ if (setupWarnings.length > 0) {
106
+ parts.push(`Setup incomplete:\n ${setupWarnings.join("\n ")}`);
107
+ }
108
+
109
+ ctx.ui.notify(
110
+ parts.join("\n"),
111
+ failed > 0 || setupWarnings.length > 0 ? "warning" : "info",
112
+ );
113
+ }
package/src/config/io.ts CHANGED
@@ -4,6 +4,7 @@ import yaml from "js-yaml";
4
4
  import { catalogFile, lockFile, ensureCatalogDir } from "./paths.js";
5
5
  import { CatalogYamlSchema, LockFileSchema } from "./schema.js";
6
6
  import type { CatalogYaml, LockFile } from "./schema.js";
7
+ import { migrateRatingToEnabledRaw } from "../catalog/migrate.js";
7
8
 
8
9
  // ---------------------------------------------------------------------------
9
10
  // Empty defaults
@@ -37,6 +38,8 @@ export function readCatalog(home?: string): CatalogYaml {
37
38
  }
38
39
 
39
40
  const parsed = yaml.load(raw);
41
+ // Migrate rating → enabled before Zod validation strips the unknown field
42
+ migrateRatingToEnabledRaw(parsed);
40
43
  return CatalogYamlSchema.parse(parsed);
41
44
  }
42
45
 
@@ -7,23 +7,16 @@ import { z } from "zod";
7
7
  /** Type discriminator for a catalog package entry. */
8
8
  export const PackageType = z.enum(["skill", "pi-native"]);
9
9
 
10
- /** Rating values for catalog packages. */
11
- export const Rating = z.enum(["core", "useful", "debatable", "disabled"]);
12
-
13
10
  /** A single package entry inside cat.yaml. */
14
11
  export const CatalogPackageSchema = z.object({
15
12
  /** Where to fetch the package from (URL, path, etc.). */
16
13
  source: z.string().min(1),
17
- /** User-assigned rating. */
18
- rating: Rating,
19
14
  /** Optional type discriminator. */
20
15
  type: PackageType.optional(),
21
16
  /** Optional profile name this package belongs to. */
22
17
  profile: z.string().optional(),
23
18
  /** Whether the package is active. Defaults to true when absent. */
24
19
  enabled: z.boolean().optional(),
25
- /** Previous rating before disable; used by enablePackage to restore. */
26
- previousRating: Rating.optional(),
27
20
  });
28
21
 
29
22
  /** The meta section at the top of cat.yaml. */
@@ -84,4 +77,3 @@ export type CatalogPackage = z.infer<typeof CatalogPackageSchema>;
84
77
  export type Profile = z.infer<typeof ProfileSchema>;
85
78
  export type LockFile = z.infer<typeof LockFileSchema>;
86
79
  export type LockPackage = z.infer<typeof LockPackageSchema>;
87
- export type RatingValue = z.infer<typeof Rating>;
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,25 @@ 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
- rating: Type.Optional(Type.String({ description: "Initial rating (core, useful, debatable)" })),
227
+ scope: Type.Optional(Type.String({ description: "Batch scope: '@pi-stef' to add all @pi-stef packages" })),
221
228
  }),
222
229
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
223
230
  try {
231
+ const flags: Record<string, true | string> = {};
232
+ if (params.scope) flags.scope = params.scope;
224
233
  const args: CommandArgs = {
225
- positional: [params.name, params.source],
226
- flags: params.rating ? { rating: params.rating } : {},
234
+ positional: params.scope ? [] : [params.source],
235
+ flags,
227
236
  };
228
237
  await addCommand(args, ctx as unknown as AddCtx);
229
- return { content: [{ type: "text" as const, text: `Added ${params.name}.` }], details: undefined as unknown };
238
+ return { content: [{ type: "text" as const, text: `Added ${params.source}.` }], details: undefined as unknown };
230
239
  } catch (err) {
231
240
  return { content: [{ type: "text" as const, text: `Add failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
232
241
  }
@@ -242,13 +251,20 @@ export function registerCatalog(pi: ExtensionAPI): void {
242
251
  "Use ct_remove when the user asks to remove or uninstall a package from their catalog.",
243
252
  ],
244
253
  parameters: Type.Object({
245
- name: Type.String({ description: "Package name to remove" }),
254
+ name: Type.Optional(Type.String({ description: "Package name to remove" })),
255
+ scope: Type.Optional(Type.String({ description: "Batch scope: '@pi-stef' to remove all @pi-stef packages" })),
246
256
  }),
247
257
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
248
258
  try {
249
- const args: CommandArgs = { positional: [params.name], flags: {} };
259
+ const flags: Record<string, true | string> = {};
260
+ if (params.scope) flags.scope = params.scope;
261
+ const args: CommandArgs = {
262
+ positional: params.name ? [params.name] : [],
263
+ flags,
264
+ };
250
265
  await removeCommand(args, ctx as unknown as RemoveCtx);
251
- return { content: [{ type: "text" as const, text: `Removed ${params.name}.` }], details: undefined as unknown };
266
+ const label = params.scope ? `Scope ${params.scope}` : `${params.name}`;
267
+ return { content: [{ type: "text" as const, text: `Removed ${label}.` }], details: undefined as unknown };
252
268
  } catch (err) {
253
269
  return { content: [{ type: "text" as const, text: `Remove failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
254
270
  }
@@ -259,10 +275,10 @@ export function registerCatalog(pi: ExtensionAPI): void {
259
275
  name: "ct_toggle",
260
276
  label: "Catalog Toggle",
261
277
  description:
262
- "Toggle a package's rating through the cycle: core → useful → debatable → disabled → core.",
263
- promptSnippet: "Toggle a package's catalog rating",
278
+ "Toggle a package's enabled state (enabled disabled).",
279
+ promptSnippet: "Toggle a package's enabled state",
264
280
  promptGuidelines: [
265
- "Use ct_toggle when the user wants to cycle a package's rating.",
281
+ "Use ct_toggle when the user wants to enable or disable a package.",
266
282
  ],
267
283
  parameters: Type.Object({
268
284
  name: Type.String({ description: "Package name to toggle" }),
@@ -278,6 +294,58 @@ export function registerCatalog(pi: ExtensionAPI): void {
278
294
  },
279
295
  });
280
296
 
297
+ pi.registerTool({
298
+ name: "ct_update",
299
+ label: "Catalog Update",
300
+ description:
301
+ "Update packages to their latest versions. Run `pi update` behind the scenes.",
302
+ promptSnippet: "Update catalog packages",
303
+ promptGuidelines: [
304
+ "Use ct_update when the user asks to update one or more packages in their catalog.",
305
+ ],
306
+ parameters: Type.Object({
307
+ name: Type.Optional(Type.String({ description: "Package name to update (omit for --all)" })),
308
+ all: Type.Optional(Type.Boolean({ description: "Update all packages" })),
309
+ }),
310
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
311
+ try {
312
+ const positional = params.name ? [params.name] : [];
313
+ const flags: Record<string, true | string> = {};
314
+ if (params.all) flags.all = true;
315
+ const args: CommandArgs = { positional, flags };
316
+ await updateCommand(args, ctx);
317
+ return { content: [{ type: "text" as const, text: "Update completed." }], details: undefined as unknown };
318
+ } catch (err) {
319
+ return { content: [{ type: "text" as const, text: `Update failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
320
+ }
321
+ },
322
+ });
323
+
324
+ pi.registerTool({
325
+ name: "ct_reset",
326
+ label: "Catalog Reset",
327
+ description:
328
+ "Full nuke: uninstall all @pi-stef packages, delete gist refs, delete config files.",
329
+ promptSnippet: "Reset catalog completely",
330
+ promptGuidelines: [
331
+ "Use ct_reset when the user wants to completely remove all @pi-stef packages and catalog config.",
332
+ ],
333
+ parameters: Type.Object({
334
+ yes: Type.Optional(Type.Boolean({ description: "Skip confirmation prompt" })),
335
+ }),
336
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
337
+ try {
338
+ const flags: Record<string, true | string> = {};
339
+ if (params.yes) flags.yes = true;
340
+ const args: CommandArgs = { positional: [], flags };
341
+ await resetCommand(args, ctx as unknown as ResetCtx);
342
+ return { content: [{ type: "text" as const, text: "Reset completed." }], details: undefined as unknown };
343
+ } catch (err) {
344
+ return { content: [{ type: "text" as const, text: `Reset failed: ${err instanceof Error ? err.message : String(err)}` }], details: undefined as unknown };
345
+ }
346
+ },
347
+ });
348
+
281
349
  pi.registerTool({
282
350
  name: "ct_status",
283
351
  label: "Catalog Status",
package/src/sync/pull.ts CHANGED
@@ -2,6 +2,7 @@ import yaml from "js-yaml";
2
2
 
3
3
  import { CatalogYamlSchema, LockFileSchema } from "../config/schema.js";
4
4
  import type { CatalogYaml, LockFile } from "../config/schema.js";
5
+ import { migrateRatingToEnabledRaw } from "../catalog/migrate.js";
5
6
  import { readGist, findGistByDescription } from "./gist.js";
6
7
  import { readCachedGistId, writeCachedGistId } from "./cache.js";
7
8
 
@@ -62,6 +63,7 @@ export async function pullCatalog(
62
63
  const lockJsonContent = gist.files["catalog.lock.json"]?.content ?? "";
63
64
 
64
65
  const parsedYaml = yaml.load(catYamlContent);
66
+ migrateRatingToEnabledRaw(parsedYaml);
65
67
  const catalog: CatalogYaml = CatalogYamlSchema.parse(parsedYaml);
66
68
 
67
69
  const parsedLock = JSON.parse(lockJsonContent);
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
+ }