@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.
- 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/packages.ts +56 -0
- package/src/catalog/ratings.ts +0 -16
- package/src/catalog/setup.ts +169 -0
- package/src/commands/add.ts +113 -28
- package/src/commands/definitions.ts +3 -1
- package/src/commands/init.ts +3 -2
- package/src/commands/remove.ts +76 -0
- package/src/commands/reset.ts +136 -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 +113 -0
- package/src/config/io.ts +3 -0
- package/src/config/schema.ts +0 -8
- package/src/register.ts +80 -12
- package/src/sync/pull.ts +2 -0
- package/src/util/exec.ts +12 -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
|
+
}
|
|
@@ -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
|
+
}
|
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
|
+
}
|