@pi-stef/catalog 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 +34 -39
- package/package.json +1 -1
- package/src/catalog/crud.ts +9 -18
- package/src/catalog/install.ts +7 -0
- package/src/catalog/migrate.ts +59 -0
- package/src/catalog/ratings.ts +0 -16
- package/src/catalog/setup.ts +169 -0
- package/src/commands/add.ts +36 -30
- package/src/commands/definitions.ts +1 -1
- package/src/commands/init.ts +3 -2
- package/src/commands/remove.ts +4 -0
- package/src/commands/reset.ts +2 -0
- package/src/commands/status.ts +71 -36
- package/src/commands/sync.ts +39 -2
- package/src/commands/toggle.ts +11 -16
- package/src/commands/types.ts +2 -0
- package/src/commands/update.ts +30 -2
- package/src/config/io.ts +3 -0
- package/src/config/schema.ts +0 -8
- package/src/register.ts +3 -5
- package/src/sync/pull.ts +2 -0
package/README.md
CHANGED
|
@@ -4,14 +4,6 @@ Declarative package manager for the [pi](https://pi.dev) coding agent. Manage yo
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
From the monorepo root (while developing):
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pi install packages/catalog
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Once published to npm:
|
|
14
|
-
|
|
15
7
|
```bash
|
|
16
8
|
pi install npm:@pi-stef/catalog
|
|
17
9
|
```
|
|
@@ -40,37 +32,48 @@ All commands are invoked as `/ct <subcommand>` inside pi, or via the shorthand `
|
|
|
40
32
|
|---|---|---|---|
|
|
41
33
|
| `sync` | — | Full sync cycle: pull → reconcile → execute → push | `--dry-run`, `--force`, `--no-push`, `--profile=<name>` |
|
|
42
34
|
| `init` | — | Initialize catalog from installed packages or a gist | `--from-gist=<id>` |
|
|
43
|
-
| `add` | `a` | Add a package to the catalog and install it | `--
|
|
44
|
-
| `remove` | `rm` | Remove a package from the catalog |
|
|
45
|
-
| `toggle` | — |
|
|
46
|
-
| `enable` | — |
|
|
47
|
-
| `disable` | — | Disable a package
|
|
35
|
+
| `add` | `a` | Add a package to the catalog and install it | `--type=<t>`, `-s <t>`, `--scope=@pi-stef` |
|
|
36
|
+
| `remove` | `rm` | Remove a package from the catalog | `--yes`, `--scope=@pi-stef` |
|
|
37
|
+
| `toggle` | — | Toggle a package's enabled state (enabled ↔ disabled) | — |
|
|
38
|
+
| `enable` | — | Enable a disabled package | — |
|
|
39
|
+
| `disable` | — | Disable a package and uninstall it | — |
|
|
40
|
+
| `update` | `up` | Update packages to latest versions | `--all` |
|
|
48
41
|
| `push` | — | Push local catalog + lock to GitHub Gist | `--dry-run`, `--profile=<name>` |
|
|
49
42
|
| `pull` | — | Pull remote catalog from gist and reconcile | `--dry-run`, `--profile=<name>` |
|
|
50
43
|
| `login` | — | Authenticate with GitHub via `gh` CLI | — |
|
|
51
|
-
| `status` | — | Show catalog status
|
|
44
|
+
| `status` | — | Show catalog status with package listing | — |
|
|
52
45
|
| `diff` | — | Show diff between local and remote catalog | — |
|
|
53
|
-
| `verify` | — | Verify catalog integrity
|
|
46
|
+
| `verify` | — | Verify catalog integrity | — |
|
|
54
47
|
| `profiles` | — | List all profiles with active indicator | — |
|
|
55
48
|
| `profile` | — | Show or switch active profile | — |
|
|
49
|
+
| `reset` | — | Uninstall all @pi-stef packages and delete config | `--yes` |
|
|
56
50
|
|
|
57
51
|
### Adding Packages
|
|
58
52
|
|
|
59
53
|
```bash
|
|
60
|
-
# Add from a git source (
|
|
61
|
-
/ct add
|
|
62
|
-
|
|
63
|
-
# Add with explicit rating and type
|
|
64
|
-
/ct add my-skill git:github.com/user/repo#packages/my-skill --rating=useful --type=skill
|
|
54
|
+
# Add from a git source (name auto-derived)
|
|
55
|
+
/ct add git:github.com/user/repo#packages/my-skill
|
|
65
56
|
|
|
66
57
|
# Add an npm package
|
|
67
|
-
/ct add
|
|
58
|
+
/ct add npm:lodash
|
|
59
|
+
|
|
60
|
+
# Add all @pi-stef packages at once
|
|
61
|
+
/ct add --scope=@pi-stef
|
|
68
62
|
```
|
|
69
63
|
|
|
70
64
|
### Removing Packages
|
|
71
65
|
|
|
72
66
|
```bash
|
|
73
67
|
/ct remove my-skill
|
|
68
|
+
/ct remove --scope=@pi-stef
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Enabling and Disabling
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
/ct enable my-skill # Enable a disabled package
|
|
75
|
+
/ct disable my-skill # Disable a package (uninstalls it)
|
|
76
|
+
/ct toggle my-skill # Toggle enabled ↔ disabled
|
|
74
77
|
```
|
|
75
78
|
|
|
76
79
|
## `cat.yaml` Format
|
|
@@ -85,18 +88,14 @@ meta:
|
|
|
85
88
|
packages:
|
|
86
89
|
superpowers-adapter:
|
|
87
90
|
source: "git:github.com/sfiorini/pi-stef#packages/superpowers-adapter"
|
|
88
|
-
rating: core
|
|
89
91
|
type: skill
|
|
90
92
|
team:
|
|
91
93
|
source: "git:github.com/sfiorini/pi-stef#packages/team"
|
|
92
|
-
rating: core
|
|
93
94
|
type: skill
|
|
94
95
|
atlassian:
|
|
95
96
|
source: "git:github.com/sfiorini/pi-stef#packages/atlassian"
|
|
96
|
-
rating: useful
|
|
97
97
|
type: skill
|
|
98
98
|
enabled: false
|
|
99
|
-
previousRating: useful
|
|
100
99
|
```
|
|
101
100
|
|
|
102
101
|
### Package Fields
|
|
@@ -104,11 +103,9 @@ packages:
|
|
|
104
103
|
| Field | Required | Description |
|
|
105
104
|
|---|---|---|
|
|
106
105
|
| `source` | ✓ | Package source URL (`npm:…` or `git:…`) |
|
|
107
|
-
| `rating` | ✓ | One of: `core`, `useful`, `debatable`, `disabled` |
|
|
108
106
|
| `type` | — | `skill` or `pi-native` |
|
|
109
107
|
| `profile` | — | Profile name this package belongs to |
|
|
110
108
|
| `enabled` | — | `true` (default) or `false` |
|
|
111
|
-
| `previousRating` | — | Rating before disable; restored by `ct enable` |
|
|
112
109
|
|
|
113
110
|
### Examples
|
|
114
111
|
|
|
@@ -117,7 +114,6 @@ packages:
|
|
|
117
114
|
packages:
|
|
118
115
|
lodash:
|
|
119
116
|
source: "npm:lodash"
|
|
120
|
-
rating: useful
|
|
121
117
|
```
|
|
122
118
|
|
|
123
119
|
**Git source:**
|
|
@@ -125,17 +121,19 @@ packages:
|
|
|
125
121
|
packages:
|
|
126
122
|
my-extension:
|
|
127
123
|
source: "git:github.com/user/repo#packages/my-extension"
|
|
128
|
-
rating: core
|
|
129
124
|
type: pi-native
|
|
130
125
|
```
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
127
|
+
## Setup Detection
|
|
128
|
+
|
|
129
|
+
Packages can include a `.pi-setup.json` file declaring requirements (environment variables, config files, CLI tools). After install or update, the catalog checks these requirements and warns if anything is missing.
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"env": ["API_TOKEN"],
|
|
134
|
+
"files": ["config.json"],
|
|
135
|
+
"cli": ["docker"]
|
|
136
|
+
}
|
|
139
137
|
```
|
|
140
138
|
|
|
141
139
|
## Profiles
|
|
@@ -150,19 +148,16 @@ meta:
|
|
|
150
148
|
packages:
|
|
151
149
|
superpowers-adapter:
|
|
152
150
|
source: "git:github.com/sfiorini/pi-stef#packages/superpowers-adapter"
|
|
153
|
-
rating: core
|
|
154
151
|
|
|
155
152
|
profiles:
|
|
156
153
|
work:
|
|
157
154
|
packages:
|
|
158
155
|
atlassian:
|
|
159
156
|
source: "git:github.com/sfiorini/pi-stef#packages/atlassian"
|
|
160
|
-
rating: core
|
|
161
157
|
personal:
|
|
162
158
|
packages:
|
|
163
159
|
figma:
|
|
164
160
|
source: "git:github.com/sfiorini/pi-stef#packages/figma"
|
|
165
|
-
rating: useful
|
|
166
161
|
```
|
|
167
162
|
|
|
168
163
|
**Profile commands:**
|
package/package.json
CHANGED
package/src/catalog/crud.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { CatalogYaml, CatalogPackage } from "../config/schema.js";
|
|
2
|
-
import
|
|
3
|
-
import { isValidSource, isDisabled, nextRating } from "./ratings.js";
|
|
2
|
+
import { isValidSource } from "./ratings.js";
|
|
4
3
|
|
|
5
4
|
// ---------------------------------------------------------------------------
|
|
6
5
|
// Immutable helpers
|
|
@@ -28,7 +27,6 @@ export function addPackage(
|
|
|
28
27
|
catalog: CatalogYaml,
|
|
29
28
|
name: string,
|
|
30
29
|
source: string,
|
|
31
|
-
rating: RatingValue,
|
|
32
30
|
type?: "skill" | "pi-native",
|
|
33
31
|
): CatalogYaml {
|
|
34
32
|
if (catalog.packages[name]) {
|
|
@@ -41,7 +39,7 @@ export function addPackage(
|
|
|
41
39
|
);
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
const entry: CatalogPackage = { source
|
|
42
|
+
const entry: CatalogPackage = { source };
|
|
45
43
|
if (type !== undefined) {
|
|
46
44
|
entry.type = type;
|
|
47
45
|
}
|
|
@@ -70,8 +68,7 @@ export function removePackage(
|
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
/**
|
|
73
|
-
* Toggle a package's
|
|
74
|
-
* core → useful → debatable → disabled → core
|
|
71
|
+
* Toggle a package's enabled state: enabled ↔ disabled.
|
|
75
72
|
*
|
|
76
73
|
* @throws if the package is not found
|
|
77
74
|
*/
|
|
@@ -87,13 +84,13 @@ export function togglePackage(
|
|
|
87
84
|
const next = cloneCatalog(catalog);
|
|
88
85
|
next.packages[name] = {
|
|
89
86
|
...entry,
|
|
90
|
-
|
|
87
|
+
enabled: entry.enabled === false ? true : false,
|
|
91
88
|
};
|
|
92
89
|
return next;
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
/**
|
|
96
|
-
* Enable a disabled package
|
|
93
|
+
* Enable a disabled package.
|
|
97
94
|
* No-op when the package is already enabled.
|
|
98
95
|
*
|
|
99
96
|
* @throws if the package is not found
|
|
@@ -108,19 +105,17 @@ export function enablePackage(
|
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
// No-op if already enabled
|
|
111
|
-
if (
|
|
108
|
+
if (entry.enabled !== false) {
|
|
112
109
|
return catalog;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
const restored = entry.previousRating ?? "core";
|
|
116
112
|
const next = cloneCatalog(catalog);
|
|
117
|
-
|
|
118
|
-
next.packages[name] = { ...clean, rating: restored };
|
|
113
|
+
next.packages[name] = { ...entry, enabled: true };
|
|
119
114
|
return next;
|
|
120
115
|
}
|
|
121
116
|
|
|
122
117
|
/**
|
|
123
|
-
* Disable a package
|
|
118
|
+
* Disable a package.
|
|
124
119
|
*
|
|
125
120
|
* @throws if the package is not found
|
|
126
121
|
*/
|
|
@@ -134,10 +129,6 @@ export function disablePackage(
|
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
const next = cloneCatalog(catalog);
|
|
137
|
-
next.packages[name] = {
|
|
138
|
-
...entry,
|
|
139
|
-
previousRating: entry.rating,
|
|
140
|
-
rating: "disabled",
|
|
141
|
-
};
|
|
132
|
+
next.packages[name] = { ...entry, enabled: false };
|
|
142
133
|
return next;
|
|
143
134
|
}
|
package/src/catalog/install.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface InstalledPackage {
|
|
|
31
31
|
name: string;
|
|
32
32
|
/** Installed version if discoverable, otherwise undefined. */
|
|
33
33
|
version: string | undefined;
|
|
34
|
+
/** Absolute path to the installed package directory, if discoverable. */
|
|
35
|
+
installDir?: string;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export type InstalledMap = Record<string, InstalledPackage>;
|
|
@@ -135,10 +137,14 @@ function readPackagesFromSettings(
|
|
|
135
137
|
const parsed = parseSource(rawSource);
|
|
136
138
|
|
|
137
139
|
let version: string | undefined;
|
|
140
|
+
let installDir: string | undefined;
|
|
138
141
|
if (parsed.type === "npm") {
|
|
139
142
|
version = readNpmVersion(home, parsed.npmName!);
|
|
143
|
+
installDir = path.join(npmNodeModulesDir(home), parsed.npmName!);
|
|
140
144
|
} else if (parsed.type === "local") {
|
|
141
145
|
version = readLocalVersion(home, rawSource);
|
|
146
|
+
const settingsDir = path.join(home, ".pi", "agent");
|
|
147
|
+
installDir = path.resolve(settingsDir, rawSource);
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
const key = parsed.type === "local" ? rawSource : parsed.name;
|
|
@@ -147,6 +153,7 @@ function readPackagesFromSettings(
|
|
|
147
153
|
source: rawSource,
|
|
148
154
|
name: parsed.name,
|
|
149
155
|
version,
|
|
156
|
+
installDir,
|
|
150
157
|
};
|
|
151
158
|
}
|
|
152
159
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: rating → enabled boolean.
|
|
3
|
+
*
|
|
4
|
+
* Operates on raw YAML output (before Zod validation) so that the `rating`
|
|
5
|
+
* field — which Zod would strip — is still available for conversion.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - rating "disabled" → enabled: false
|
|
9
|
+
* - any other rating → enabled: true (or omit, since Zod defaults to true)
|
|
10
|
+
* - Remove `rating` and `previousRating` fields
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface RawPackage {
|
|
14
|
+
source: string;
|
|
15
|
+
rating?: string;
|
|
16
|
+
previousRating?: string;
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
type?: string;
|
|
19
|
+
profile?: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RawCatalog {
|
|
24
|
+
meta: Record<string, unknown>;
|
|
25
|
+
packages: Record<string, RawPackage>;
|
|
26
|
+
profiles?: Record<string, unknown>;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Migrate a raw YAML catalog object from rating-based to enabled-based.
|
|
32
|
+
*
|
|
33
|
+
* Returns the mutated object (in-place) for chaining. If the catalog has
|
|
34
|
+
* no `packages` key or is not an object, returns it unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export function migrateRatingToEnabledRaw(raw: unknown): unknown {
|
|
37
|
+
if (!raw || typeof raw !== "object") return raw;
|
|
38
|
+
const catalog = raw as RawCatalog;
|
|
39
|
+
if (!catalog.packages || typeof catalog.packages !== "object") return raw;
|
|
40
|
+
|
|
41
|
+
for (const [_name, pkg] of Object.entries(catalog.packages)) {
|
|
42
|
+
if (!pkg || typeof pkg !== "object") continue;
|
|
43
|
+
|
|
44
|
+
if ("rating" in pkg) {
|
|
45
|
+
const rating = pkg.rating;
|
|
46
|
+
if (rating === "disabled") {
|
|
47
|
+
pkg.enabled = false;
|
|
48
|
+
}
|
|
49
|
+
// For non-disabled ratings, don't set enabled — Zod defaults to true
|
|
50
|
+
delete pkg.rating;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if ("previousRating" in pkg) {
|
|
54
|
+
delete pkg.previousRating;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
package/src/catalog/ratings.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/** The ordered rating cycle: core → useful → debatable → disabled → core */
|
|
2
|
-
export const RATING_CYCLE = ["core", "useful", "debatable", "disabled"] as const;
|
|
3
|
-
|
|
4
|
-
export type RatingValue = (typeof RATING_CYCLE)[number];
|
|
5
|
-
|
|
6
|
-
/** Returns the next rating in the cycle. */
|
|
7
|
-
export function nextRating(rating: RatingValue): RatingValue {
|
|
8
|
-
const idx = RATING_CYCLE.indexOf(rating);
|
|
9
|
-
return RATING_CYCLE[(idx + 1) % RATING_CYCLE.length];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Returns true when the rating is "disabled". */
|
|
13
|
-
export function isDisabled(rating: RatingValue): boolean {
|
|
14
|
-
return rating === "disabled";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
1
|
/**
|
|
18
2
|
* Validates a source string.
|
|
19
3
|
* Accepted formats:
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup detection for packages that need additional configuration.
|
|
3
|
+
*
|
|
4
|
+
* Packages can include a `.pi-setup.json` file in their install directory
|
|
5
|
+
* declaring requirements:
|
|
6
|
+
* - `env`: required environment variables
|
|
7
|
+
* - `files`: required config files (relative to config dir ~/.pi/sf/<pkg>/)
|
|
8
|
+
* - `cli`: required CLI tools (checked via `which`)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { parseSource } from "./source.js";
|
|
17
|
+
import { npmNodeModulesDir } from "../config/paths.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Schema
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export const SetupCheckSchema = z.object({
|
|
24
|
+
/** Required environment variables. */
|
|
25
|
+
env: z.array(z.string()).optional(),
|
|
26
|
+
/** Required config files (relative to config dir). */
|
|
27
|
+
files: z.array(z.string()).optional(),
|
|
28
|
+
/** Required CLI tools (checked via `which`). */
|
|
29
|
+
cli: z.array(z.string()).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type SetupCheck = z.infer<typeof SetupCheckSchema>;
|
|
33
|
+
|
|
34
|
+
export interface SetupStatus {
|
|
35
|
+
/** Whether all requirements are met. */
|
|
36
|
+
ok: boolean;
|
|
37
|
+
/** Missing environment variables. */
|
|
38
|
+
missingEnv: string[];
|
|
39
|
+
/** Missing config files. */
|
|
40
|
+
missingFiles: string[];
|
|
41
|
+
/** Missing CLI tools. */
|
|
42
|
+
missingCli: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// checkSetup
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check setup requirements for a package.
|
|
51
|
+
*
|
|
52
|
+
* @param installDir - The package's installed directory (where .pi-setup.json lives)
|
|
53
|
+
* @param configDir - The package's config directory (~/.pi/sf/<pkg>/)
|
|
54
|
+
* @returns Setup status with missing requirements, or undefined if no .pi-setup.json
|
|
55
|
+
*/
|
|
56
|
+
export function checkSetup(
|
|
57
|
+
installDir: string,
|
|
58
|
+
configDir: string,
|
|
59
|
+
): SetupStatus | undefined {
|
|
60
|
+
const setupPath = path.join(installDir, ".pi-setup.json");
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(setupPath)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let raw: unknown;
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(setupPath, "utf-8");
|
|
69
|
+
raw = JSON.parse(content);
|
|
70
|
+
} catch {
|
|
71
|
+
console.warn(`Warning: malformed .pi-setup.json in ${installDir}`);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parsed = SetupCheckSchema.safeParse(raw);
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
console.warn(`Warning: invalid .pi-setup.json in ${installDir}: ${parsed.error.message}`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const check = parsed.data;
|
|
82
|
+
const missingEnv: string[] = [];
|
|
83
|
+
const missingFiles: string[] = [];
|
|
84
|
+
const missingCli: string[] = [];
|
|
85
|
+
|
|
86
|
+
// Check environment variables
|
|
87
|
+
if (check.env) {
|
|
88
|
+
for (const varName of check.env) {
|
|
89
|
+
if (!process.env[varName]) {
|
|
90
|
+
missingEnv.push(varName);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check config files
|
|
96
|
+
if (check.files) {
|
|
97
|
+
for (const filePath of check.files) {
|
|
98
|
+
const fullPath = path.join(configDir, filePath);
|
|
99
|
+
if (!fs.existsSync(fullPath)) {
|
|
100
|
+
missingFiles.push(filePath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check CLI tools
|
|
106
|
+
if (check.cli) {
|
|
107
|
+
for (const tool of check.cli) {
|
|
108
|
+
const result = spawnSync("which", [tool], {
|
|
109
|
+
stdio: "pipe",
|
|
110
|
+
timeout: 5000,
|
|
111
|
+
});
|
|
112
|
+
if (result.status !== 0) {
|
|
113
|
+
missingCli.push(tool);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ok =
|
|
119
|
+
missingEnv.length === 0 &&
|
|
120
|
+
missingFiles.length === 0 &&
|
|
121
|
+
missingCli.length === 0;
|
|
122
|
+
|
|
123
|
+
return { ok, missingEnv, missingFiles, missingCli };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format a setup status as a human-readable message.
|
|
128
|
+
*/
|
|
129
|
+
export function formatSetupStatus(status: SetupStatus): string {
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
if (status.missingEnv.length > 0) {
|
|
132
|
+
parts.push(`Missing env: ${status.missingEnv.join(", ")}`);
|
|
133
|
+
}
|
|
134
|
+
if (status.missingFiles.length > 0) {
|
|
135
|
+
parts.push(`Missing files: ${status.missingFiles.join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
if (status.missingCli.length > 0) {
|
|
138
|
+
parts.push(`Missing CLI: ${status.missingCli.join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
return parts.join("; ");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check setup requirements for a package by source string.
|
|
145
|
+
*
|
|
146
|
+
* Derives install dir from source type and config dir from home.
|
|
147
|
+
* Returns undefined if no .pi-setup.json exists.
|
|
148
|
+
*/
|
|
149
|
+
export function checkSetupForSource(
|
|
150
|
+
source: string,
|
|
151
|
+
home?: string,
|
|
152
|
+
): SetupStatus | undefined {
|
|
153
|
+
const parsed = parseSource(source);
|
|
154
|
+
const resolvedHome = home ?? os.homedir();
|
|
155
|
+
|
|
156
|
+
let installDir: string | undefined;
|
|
157
|
+
if (parsed.type === "npm") {
|
|
158
|
+
installDir = path.join(npmNodeModulesDir(resolvedHome), parsed.npmName!);
|
|
159
|
+
} else if (parsed.type === "local") {
|
|
160
|
+
const settingsDir = path.join(resolvedHome, ".pi", "agent");
|
|
161
|
+
installDir = path.resolve(settingsDir, source);
|
|
162
|
+
} else {
|
|
163
|
+
// git sources — no known install dir
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const configDir = path.join(resolvedHome, ".pi", "sf", parsed.name);
|
|
168
|
+
return checkSetup(installDir, configDir);
|
|
169
|
+
}
|
package/src/commands/add.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `ct add` subcommand implementation.
|
|
3
3
|
*
|
|
4
4
|
* Adds a new package to the catalog. Supports:
|
|
5
|
-
* - Full args: `ct add <
|
|
5
|
+
* - Full args: `ct add <source> [--type <t>]`
|
|
6
6
|
* - Git source without `--type`: prompts for type via `ctx.ui.select()`
|
|
7
7
|
* - After adding, runs `pi install` to install the package
|
|
8
8
|
*
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* and `writeCatalog` / `readCatalog` for persistence.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { RatingValue } from "../catalog/ratings.js";
|
|
14
13
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
15
14
|
import { addPackage } from "../catalog/crud.js";
|
|
16
15
|
import { sourceToKey } from "../catalog/source.js";
|
|
16
|
+
import { checkSetupForSource, formatSetupStatus } from "../catalog/setup.js";
|
|
17
17
|
import { PI_STEF_PACKAGES } from "../catalog/packages.js";
|
|
18
18
|
import { readCatalog, writeCatalog } from "../config/io.js";
|
|
19
19
|
import { piInstall } from "../util/exec.js";
|
|
@@ -36,25 +36,6 @@ export interface AddCtx extends CommandCtx {
|
|
|
36
36
|
// Helpers
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
|
|
39
|
-
const VALID_RATINGS: RatingValue[] = ["core", "useful", "debatable"];
|
|
40
|
-
|
|
41
|
-
function isValidRating(value: string): value is RatingValue {
|
|
42
|
-
return VALID_RATINGS.includes(value as RatingValue);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function resolveRating(flags: Record<string, true | string>): RatingValue {
|
|
46
|
-
const raw =
|
|
47
|
-
"r" in flags
|
|
48
|
-
? flags["r"]
|
|
49
|
-
: "rating" in flags
|
|
50
|
-
? flags["rating"]
|
|
51
|
-
: undefined;
|
|
52
|
-
|
|
53
|
-
if (raw === true || raw === undefined) return "core";
|
|
54
|
-
if (typeof raw === "string" && isValidRating(raw)) return raw;
|
|
55
|
-
return "core";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
39
|
function resolveType(
|
|
59
40
|
flags: Record<string, true | string>,
|
|
60
41
|
): "skill" | "pi-native" | undefined {
|
|
@@ -77,10 +58,10 @@ function resolveType(
|
|
|
77
58
|
/**
|
|
78
59
|
* Execute the `ct add` subcommand.
|
|
79
60
|
*
|
|
80
|
-
* New syntax (preferred): `ct add <source> [--
|
|
61
|
+
* New syntax (preferred): `ct add <source> [--type ...]`
|
|
81
62
|
* — name is auto-derived from source via `sourceToKey()`.
|
|
82
63
|
*
|
|
83
|
-
* Legacy syntax (deprecated): `ct add <name> <source> [--
|
|
64
|
+
* Legacy syntax (deprecated): `ct add <name> <source> [--type ...]`
|
|
84
65
|
* — still accepted but emits a deprecation warning.
|
|
85
66
|
*
|
|
86
67
|
* Reads the catalog, validates inputs, prompts for type if needed,
|
|
@@ -98,7 +79,6 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
98
79
|
}
|
|
99
80
|
|
|
100
81
|
const catalog = readCatalog(ctx.home);
|
|
101
|
-
const rating = resolveRating(flags);
|
|
102
82
|
let added = 0;
|
|
103
83
|
let skipped = 0;
|
|
104
84
|
let currentCatalog = catalog;
|
|
@@ -113,7 +93,7 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
113
93
|
}
|
|
114
94
|
|
|
115
95
|
try {
|
|
116
|
-
currentCatalog = addPackage(currentCatalog, pkg, npmSource
|
|
96
|
+
currentCatalog = addPackage(currentCatalog, pkg, npmSource);
|
|
117
97
|
added++;
|
|
118
98
|
} catch (err: unknown) {
|
|
119
99
|
// Unexpected validation error — warn but continue
|
|
@@ -130,21 +110,37 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
130
110
|
}
|
|
131
111
|
|
|
132
112
|
// Install all added packages
|
|
113
|
+
const setupWarnings: string[] = [];
|
|
133
114
|
if (added > 0) {
|
|
134
115
|
for (const pkg of PI_STEF_PACKAGES) {
|
|
135
116
|
if (currentCatalog.packages[pkg]?.source === `npm:${pkg}`) {
|
|
117
|
+
ctx.ui.setWorkingMessage?.(`Installing ${pkg}...`);
|
|
136
118
|
try {
|
|
137
119
|
await piInstall(`npm:${pkg}`);
|
|
120
|
+
|
|
121
|
+
// Check setup after successful install
|
|
122
|
+
const setup = checkSetupForSource(`npm:${pkg}`, ctx.home);
|
|
123
|
+
if (setup && !setup.ok) {
|
|
124
|
+
setupWarnings.push(`${pkg}: ${formatSetupStatus(setup)}`);
|
|
125
|
+
}
|
|
138
126
|
} catch {
|
|
139
127
|
ctx.ui.notify(`Warning: install of "${pkg}" failed`, "warning");
|
|
140
128
|
}
|
|
141
129
|
}
|
|
142
130
|
}
|
|
131
|
+
ctx.ui.setWorkingMessage?.();
|
|
143
132
|
}
|
|
144
133
|
|
|
145
|
-
|
|
134
|
+
const parts: string[] = [
|
|
146
135
|
`Scope @pi-stef: added ${added}, skipped ${skipped} (already in catalog)`,
|
|
147
|
-
|
|
136
|
+
];
|
|
137
|
+
if (setupWarnings.length > 0) {
|
|
138
|
+
parts.push(`Setup incomplete:\n ${setupWarnings.join("\n ")}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ctx.ui.notify(
|
|
142
|
+
parts.join("\n"),
|
|
143
|
+
setupWarnings.length > 0 ? "warning" : "info",
|
|
148
144
|
);
|
|
149
145
|
return;
|
|
150
146
|
}
|
|
@@ -165,13 +161,12 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
165
161
|
name = sourceToKey(source);
|
|
166
162
|
} else {
|
|
167
163
|
ctx.ui.notify(
|
|
168
|
-
"Usage: ct add <source> [--
|
|
164
|
+
"Usage: ct add <source> [--type <skill|pi-native>]",
|
|
169
165
|
"error",
|
|
170
166
|
);
|
|
171
167
|
return;
|
|
172
168
|
}
|
|
173
169
|
|
|
174
|
-
const rating = resolveRating(flags);
|
|
175
170
|
let type = resolveType(flags);
|
|
176
171
|
|
|
177
172
|
// --- Read catalog ---------------------------------------------------------
|
|
@@ -192,7 +187,7 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
192
187
|
|
|
193
188
|
// --- Add package ----------------------------------------------------------
|
|
194
189
|
try {
|
|
195
|
-
const updated = addPackage(catalog, name, source,
|
|
190
|
+
const updated = addPackage(catalog, name, source, type);
|
|
196
191
|
writeCatalog(updated, ctx.home);
|
|
197
192
|
} catch (err: unknown) {
|
|
198
193
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -203,6 +198,7 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
203
198
|
ctx.ui.notify(`Added "${name}" to catalog`, "info");
|
|
204
199
|
|
|
205
200
|
// --- Run pi install -------------------------------------------------------
|
|
201
|
+
ctx.ui.setWorkingMessage?.(`Installing ${name}...`);
|
|
206
202
|
try {
|
|
207
203
|
await piInstall(source);
|
|
208
204
|
} catch {
|
|
@@ -211,4 +207,14 @@ export async function addCommand(args: CommandArgs, ctx: AddCtx): Promise<void>
|
|
|
211
207
|
"warning",
|
|
212
208
|
);
|
|
213
209
|
}
|
|
210
|
+
ctx.ui.setWorkingMessage?.();
|
|
211
|
+
|
|
212
|
+
// --- Check setup requirements ---------------------------------------------
|
|
213
|
+
const setup = checkSetupForSource(source, ctx.home);
|
|
214
|
+
if (setup && !setup.ok) {
|
|
215
|
+
ctx.ui.notify(
|
|
216
|
+
`Setup incomplete for "${name}": ${formatSetupStatus(setup)}`,
|
|
217
|
+
"warning",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
214
220
|
}
|
|
@@ -30,7 +30,7 @@ export const SUBCOMMAND_DEFS: readonly SubcommandDef[] = [
|
|
|
30
30
|
{ name: "init", description: "Initialize a new catalog" },
|
|
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
|
-
{ name: "toggle", description: "Toggle a package's
|
|
33
|
+
{ name: "toggle", description: "Toggle a package's enabled state" },
|
|
34
34
|
{ name: "update", aliases: ["up"], description: "Update packages to latest versions" },
|
|
35
35
|
{ name: "disable", description: "Disable a package" },
|
|
36
36
|
{ name: "enable", description: "Enable a package" },
|
package/src/commands/init.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import yaml from "js-yaml";
|
|
9
9
|
|
|
10
10
|
import { scanInstalled } from "../catalog/install.js";
|
|
11
|
+
import { migrateRatingToEnabledRaw } from "../catalog/migrate.js";
|
|
11
12
|
import { CatalogYamlSchema } from "../config/schema.js";
|
|
12
13
|
import type { CatalogYaml } from "../config/schema.js";
|
|
13
14
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
@@ -29,7 +30,7 @@ export type InitContext = CommandCtx;
|
|
|
29
30
|
* Initialize a new catalog.
|
|
30
31
|
*
|
|
31
32
|
* - Without flags: scans installed packages and generates a catalog with
|
|
32
|
-
*
|
|
33
|
+
* every discovered package enabled.
|
|
33
34
|
* - With `--from-gist=<id>`: fetches the gist, reads its `cat.yaml` file,
|
|
34
35
|
* validates it, and writes it as the local catalog.
|
|
35
36
|
*/
|
|
@@ -70,7 +71,6 @@ function initFromScan(ctx: InitContext): void {
|
|
|
70
71
|
for (const [name, pkg] of Object.entries(installed)) {
|
|
71
72
|
packages[name] = {
|
|
72
73
|
source: pkg.source,
|
|
73
|
-
rating: "core",
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -115,6 +115,7 @@ async function initFromGist(gistId: string, ctx: InitContext): Promise<void> {
|
|
|
115
115
|
|
|
116
116
|
// Validate and write
|
|
117
117
|
const parsed = yaml.load(gistContent);
|
|
118
|
+
migrateRatingToEnabledRaw(parsed);
|
|
118
119
|
const catalog = CatalogYamlSchema.parse(parsed);
|
|
119
120
|
|
|
120
121
|
writeCatalog(catalog, ctx.home);
|
package/src/commands/remove.ts
CHANGED
|
@@ -97,6 +97,7 @@ export async function removeCommand(
|
|
|
97
97
|
let uninstalled = 0;
|
|
98
98
|
let failed = 0;
|
|
99
99
|
for (const name of piStefNames) {
|
|
100
|
+
ctx.ui.setWorkingMessage?.(`Uninstalling ${name} (${uninstalled + 1}/${piStefNames.length})...`);
|
|
100
101
|
try {
|
|
101
102
|
await piUninstall(sources[name]);
|
|
102
103
|
uninstalled++;
|
|
@@ -105,6 +106,7 @@ export async function removeCommand(
|
|
|
105
106
|
failed++;
|
|
106
107
|
}
|
|
107
108
|
}
|
|
109
|
+
ctx.ui.setWorkingMessage?.();
|
|
108
110
|
|
|
109
111
|
ctx.ui.notify(
|
|
110
112
|
`Scope @pi-stef: removed ${piStefNames.length}, uninstalled ${uninstalled}${failed > 0 ? ` (${failed} uninstall failed)` : ""}`,
|
|
@@ -163,6 +165,7 @@ export async function removeCommand(
|
|
|
163
165
|
ctx.ui.notify(`Removed "${name}" from catalog`, "info");
|
|
164
166
|
|
|
165
167
|
// --- Run pi uninstall -----------------------------------------------------
|
|
168
|
+
ctx.ui.setWorkingMessage?.(`Uninstalling ${name}...`);
|
|
166
169
|
try {
|
|
167
170
|
await piUninstall(source);
|
|
168
171
|
} catch {
|
|
@@ -171,4 +174,5 @@ export async function removeCommand(
|
|
|
171
174
|
"warning",
|
|
172
175
|
);
|
|
173
176
|
}
|
|
177
|
+
ctx.ui.setWorkingMessage?.();
|
|
174
178
|
}
|
package/src/commands/reset.ts
CHANGED
|
@@ -91,6 +91,7 @@ export async function resetCommand(
|
|
|
91
91
|
let failed = 0;
|
|
92
92
|
|
|
93
93
|
for (const name of piStefNames) {
|
|
94
|
+
ctx.ui.setWorkingMessage?.(`Uninstalling ${name} (${uninstalled + 1}/${piStefNames.length})...`);
|
|
94
95
|
try {
|
|
95
96
|
await piUninstall(packages[name].source);
|
|
96
97
|
uninstalled++;
|
|
@@ -99,6 +100,7 @@ export async function resetCommand(
|
|
|
99
100
|
failed++;
|
|
100
101
|
}
|
|
101
102
|
}
|
|
103
|
+
ctx.ui.setWorkingMessage?.();
|
|
102
104
|
|
|
103
105
|
// --- Delete config files --------------------------------------------------
|
|
104
106
|
const dir = catalogDir(ctx.home);
|
package/src/commands/status.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ct status` subcommand implementation.
|
|
3
3
|
*
|
|
4
|
-
* Shows catalog status: profile, package counts
|
|
5
|
-
* installed/missing/orphan counts, gist URL,
|
|
4
|
+
* Shows catalog status: profile, package counts (enabled/disabled),
|
|
5
|
+
* installed/missing/orphan counts, gist URL, last sync time,
|
|
6
|
+
* and individual package listing with setup status.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
9
10
|
import { readCatalog, readLock } from "../config/io.js";
|
|
10
11
|
import { scanInstalled } from "../catalog/install.js";
|
|
11
12
|
import { readCachedGistId } from "../sync/cache.js";
|
|
13
|
+
import { checkSetupForSource } from "../catalog/setup.js";
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// Types
|
|
@@ -17,30 +19,6 @@ import { readCachedGistId } from "../sync/cache.js";
|
|
|
17
19
|
/** Context for `statusCommand`. Uses the base `CommandCtx`. */
|
|
18
20
|
export type StatusCtx = CommandCtx;
|
|
19
21
|
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Helpers
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
interface RatingCounts {
|
|
25
|
-
core: number;
|
|
26
|
-
useful: number;
|
|
27
|
-
debatable: number;
|
|
28
|
-
disabled: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function countByRating(
|
|
32
|
-
packages: Record<string, { rating: string; enabled?: boolean }>,
|
|
33
|
-
): RatingCounts {
|
|
34
|
-
const counts: RatingCounts = { core: 0, useful: 0, debatable: 0, disabled: 0 };
|
|
35
|
-
for (const pkg of Object.values(packages)) {
|
|
36
|
-
const r = pkg.rating as keyof RatingCounts;
|
|
37
|
-
if (r in counts) {
|
|
38
|
-
counts[r]++;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return counts;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
22
|
// ---------------------------------------------------------------------------
|
|
45
23
|
// statusCommand
|
|
46
24
|
// ---------------------------------------------------------------------------
|
|
@@ -49,7 +27,7 @@ function countByRating(
|
|
|
49
27
|
* Execute the `ct status` subcommand.
|
|
50
28
|
*
|
|
51
29
|
* Reads catalog, lock, gist cache, and installed packages to build
|
|
52
|
-
* a comprehensive status summary.
|
|
30
|
+
* a comprehensive status summary with individual package listing.
|
|
53
31
|
*/
|
|
54
32
|
export async function statusCommand(
|
|
55
33
|
args: CommandArgs,
|
|
@@ -67,24 +45,33 @@ export async function statusCommand(
|
|
|
67
45
|
const packages = catalog.packages;
|
|
68
46
|
const totalPackages = Object.keys(packages).length;
|
|
69
47
|
|
|
70
|
-
// ---
|
|
71
|
-
|
|
48
|
+
// --- Enabled / disabled counts ---
|
|
49
|
+
let enabledCount = 0;
|
|
50
|
+
let disabledCount = 0;
|
|
51
|
+
for (const pkg of Object.values(packages)) {
|
|
52
|
+
if (pkg.enabled === false) {
|
|
53
|
+
disabledCount++;
|
|
54
|
+
} else {
|
|
55
|
+
enabledCount++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
72
58
|
|
|
73
59
|
// --- Installed / missing / orphan ---
|
|
60
|
+
// Build a lookup map for installed packages by source
|
|
61
|
+
const installedBySource = new Map<string, { name: string; version?: string }>();
|
|
62
|
+
for (const inst of Object.values(installed)) {
|
|
63
|
+
installedBySource.set(inst.source, { name: inst.name, version: inst.version });
|
|
64
|
+
}
|
|
65
|
+
|
|
74
66
|
const catalogSources = new Set<string>();
|
|
75
67
|
for (const pkg of Object.values(packages)) {
|
|
76
68
|
catalogSources.add(pkg.source);
|
|
77
69
|
}
|
|
78
70
|
|
|
79
|
-
const installedSources = new Set<string>();
|
|
80
|
-
for (const inst of Object.values(installed)) {
|
|
81
|
-
installedSources.add(inst.source);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
71
|
// Count how many catalog packages are actually installed
|
|
85
72
|
let installedCount = 0;
|
|
86
73
|
for (const pkg of Object.values(packages)) {
|
|
87
|
-
if (
|
|
74
|
+
if (installedBySource.has(pkg.source)) {
|
|
88
75
|
installedCount++;
|
|
89
76
|
}
|
|
90
77
|
}
|
|
@@ -116,7 +103,7 @@ export async function statusCommand(
|
|
|
116
103
|
|
|
117
104
|
// Package counts
|
|
118
105
|
lines.push(
|
|
119
|
-
`Packages: ${totalPackages} total (
|
|
106
|
+
`Packages: ${totalPackages} total (${enabledCount} enabled, ${disabledCount} disabled)`,
|
|
120
107
|
);
|
|
121
108
|
|
|
122
109
|
// Installed/missing/orphan
|
|
@@ -138,5 +125,53 @@ export async function statusCommand(
|
|
|
138
125
|
lines.push("Last sync: never synced");
|
|
139
126
|
}
|
|
140
127
|
|
|
128
|
+
// --- Individual package listing ---
|
|
129
|
+
if (totalPackages > 0) {
|
|
130
|
+
lines.push("");
|
|
131
|
+
lines.push("Packages:");
|
|
132
|
+
|
|
133
|
+
for (const [name, pkg] of Object.entries(packages)) {
|
|
134
|
+
const isDisabled = pkg.enabled === false;
|
|
135
|
+
const isInstalled = installedBySource.has(pkg.source);
|
|
136
|
+
const inst = installedBySource.get(pkg.source);
|
|
137
|
+
|
|
138
|
+
// Status indicator
|
|
139
|
+
let status: string;
|
|
140
|
+
if (isDisabled) {
|
|
141
|
+
status = "disabled";
|
|
142
|
+
} else if (isInstalled) {
|
|
143
|
+
status = "installed";
|
|
144
|
+
} else {
|
|
145
|
+
status = "missing";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Version info
|
|
149
|
+
const versionStr = inst?.version ? ` v${inst.version}` : "";
|
|
150
|
+
|
|
151
|
+
// Setup status
|
|
152
|
+
let setupStr = "";
|
|
153
|
+
if (isInstalled && !isDisabled) {
|
|
154
|
+
const setup = checkSetupForSource(pkg.source, ctx.home);
|
|
155
|
+
if (setup && !setup.ok) {
|
|
156
|
+
setupStr = " ⚠ setup incomplete";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lines.push(` ${name} [${status}]${versionStr}${setupStr}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Orphans ---
|
|
165
|
+
if (orphanCount > 0) {
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("Orphans:");
|
|
168
|
+
for (const inst of Object.values(installed)) {
|
|
169
|
+
if (!catalogSources.has(inst.source)) {
|
|
170
|
+
const versionStr = inst.version ? ` v${inst.version}` : "";
|
|
171
|
+
lines.push(` ${inst.name} [orphan]${versionStr}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
141
176
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
142
177
|
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/toggle.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ct toggle`, `ct enable`, and `ct disable` subcommand implementations.
|
|
3
3
|
*
|
|
4
|
-
* - `ct toggle <name>`
|
|
5
|
-
*
|
|
6
|
-
* - `ct
|
|
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
|
-
*
|
|
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}"
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
}
|
package/src/commands/types.ts
CHANGED
|
@@ -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;
|
package/src/commands/update.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { CommandArgs, CommandCtx } from "./types.js";
|
|
14
14
|
import { readCatalog } from "../config/io.js";
|
|
15
15
|
import { piUpdate } from "../util/exec.js";
|
|
16
|
+
import { checkSetupForSource, formatSetupStatus } from "../catalog/setup.js";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// updateCommand
|
|
@@ -48,12 +49,23 @@ export async function updateCommand(
|
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
ctx.ui.setWorkingMessage?.(`Updating ${name}...`);
|
|
51
53
|
try {
|
|
52
54
|
await piUpdate(entry.source);
|
|
53
55
|
ctx.ui.notify(`Updated "${name}"`, "info");
|
|
54
56
|
} catch {
|
|
55
57
|
ctx.ui.notify(`Warning: update of "${name}" failed`, "warning");
|
|
56
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
|
+
}
|
|
57
69
|
return;
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -66,20 +78,36 @@ export async function updateCommand(
|
|
|
66
78
|
|
|
67
79
|
let updated = 0;
|
|
68
80
|
let failed = 0;
|
|
81
|
+
const setupWarnings: string[] = [];
|
|
69
82
|
|
|
70
83
|
for (const pkgName of names) {
|
|
71
84
|
const entry = packages[pkgName];
|
|
85
|
+
ctx.ui.setWorkingMessage?.(`Updating ${pkgName} (${updated + 1}/${names.length})...`);
|
|
72
86
|
try {
|
|
73
87
|
await piUpdate(entry.source);
|
|
74
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
|
+
}
|
|
75
95
|
} catch {
|
|
76
96
|
ctx.ui.notify(`Warning: update of "${pkgName}" failed`, "warning");
|
|
77
97
|
failed++;
|
|
78
98
|
}
|
|
79
99
|
}
|
|
100
|
+
ctx.ui.setWorkingMessage?.();
|
|
80
101
|
|
|
81
|
-
|
|
102
|
+
const parts: string[] = [
|
|
82
103
|
`Updated ${updated}/${names.length} packages${failed > 0 ? ` (${failed} failed)` : ""}`,
|
|
83
|
-
|
|
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",
|
|
84
112
|
);
|
|
85
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
|
|
package/src/config/schema.ts
CHANGED
|
@@ -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
|
@@ -224,13 +224,11 @@ export function registerCatalog(pi: ExtensionAPI): void {
|
|
|
224
224
|
],
|
|
225
225
|
parameters: Type.Object({
|
|
226
226
|
source: Type.String({ description: "Package source (npm:… or git:…)" }),
|
|
227
|
-
rating: Type.Optional(Type.String({ description: "Initial rating (core, useful, debatable)" })),
|
|
228
227
|
scope: Type.Optional(Type.String({ description: "Batch scope: '@pi-stef' to add all @pi-stef packages" })),
|
|
229
228
|
}),
|
|
230
229
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
231
230
|
try {
|
|
232
231
|
const flags: Record<string, true | string> = {};
|
|
233
|
-
if (params.rating) flags.rating = params.rating;
|
|
234
232
|
if (params.scope) flags.scope = params.scope;
|
|
235
233
|
const args: CommandArgs = {
|
|
236
234
|
positional: params.scope ? [] : [params.source],
|
|
@@ -277,10 +275,10 @@ export function registerCatalog(pi: ExtensionAPI): void {
|
|
|
277
275
|
name: "ct_toggle",
|
|
278
276
|
label: "Catalog Toggle",
|
|
279
277
|
description:
|
|
280
|
-
"Toggle a package's
|
|
281
|
-
promptSnippet: "Toggle a package's
|
|
278
|
+
"Toggle a package's enabled state (enabled ↔ disabled).",
|
|
279
|
+
promptSnippet: "Toggle a package's enabled state",
|
|
282
280
|
promptGuidelines: [
|
|
283
|
-
"Use ct_toggle when the user wants to
|
|
281
|
+
"Use ct_toggle when the user wants to enable or disable a package.",
|
|
284
282
|
],
|
|
285
283
|
parameters: Type.Object({
|
|
286
284
|
name: Type.String({ description: "Package name to toggle" }),
|
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);
|