@aipper/aiws 0.0.48 → 0.0.50
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/dist/cli.js +30 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/change-finish.js +7 -0
- package/dist/commands/change-finish.js.map +1 -1
- package/dist/commands/change.js +24 -0
- package/dist/commands/change.js.map +1 -1
- package/dist/commands/doctor.d.ts +29 -0
- package/dist/commands/doctor.js +57 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +1 -3
- package/dist/commands/init.js +7 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.js +11 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/ws-deliver.js +2 -3
- package/dist/commands/ws-deliver.js.map +1 -1
- package/dist/commands/ws-finish.d.ts +7 -0
- package/dist/commands/ws-finish.js +19 -128
- package/dist/commands/ws-finish.js.map +1 -1
- package/dist/commands/ws-finish.test.d.ts +1 -0
- package/dist/commands/ws-finish.test.js +167 -0
- package/dist/commands/ws-finish.test.js.map +1 -0
- package/dist/lib/pi-force-policy.d.ts +66 -0
- package/dist/lib/pi-force-policy.js +187 -0
- package/dist/lib/pi-force-policy.js.map +1 -0
- package/dist/lib/pi-force-policy.test.d.ts +1 -0
- package/dist/lib/pi-force-policy.test.js +99 -0
- package/dist/lib/pi-force-policy.test.js.map +1 -0
- package/dist/lib/pi-plugins-doctor.d.ts +130 -0
- package/dist/lib/pi-plugins-doctor.js +537 -0
- package/dist/lib/pi-plugins-doctor.js.map +1 -0
- package/dist/lib/pi-plugins-doctor.test.d.ts +1 -0
- package/dist/lib/pi-plugins-doctor.test.js +137 -0
- package/dist/lib/pi-plugins-doctor.test.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi user-level plugins doctor (TOOLING-002L / PB-010 / pi-plugin-doctor).
|
|
3
|
+
* Pure classify + format; I/O helpers injectable for tests.
|
|
4
|
+
*/
|
|
5
|
+
export type PluginTier = "required" | "recommended_a" | "recommended_b" | "forbidden";
|
|
6
|
+
export interface ManifestPlugin {
|
|
7
|
+
id: string;
|
|
8
|
+
package: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
tier?: PluginTier | string;
|
|
11
|
+
severity?: "conflict" | "forbidden" | string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PiPluginsManifest {
|
|
15
|
+
version: number;
|
|
16
|
+
scope: string;
|
|
17
|
+
source_protocol: string;
|
|
18
|
+
install_template: string;
|
|
19
|
+
required: ManifestPlugin[];
|
|
20
|
+
recommended_a: ManifestPlugin[];
|
|
21
|
+
recommended_b: ManifestPlugin[];
|
|
22
|
+
forbidden: ManifestPlugin[];
|
|
23
|
+
}
|
|
24
|
+
export interface InstalledPackage {
|
|
25
|
+
/** bare or scoped name without version */
|
|
26
|
+
name: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
raw?: string;
|
|
29
|
+
}
|
|
30
|
+
export type DetectSource = "pi_list" | "settings" | "none" | "injected";
|
|
31
|
+
export interface DoctorClassifyInput {
|
|
32
|
+
manifest: PiPluginsManifest;
|
|
33
|
+
installed: InstalledPackage[];
|
|
34
|
+
piCliAvailable: boolean;
|
|
35
|
+
detectSource: DetectSource;
|
|
36
|
+
}
|
|
37
|
+
export interface DoctorFinding {
|
|
38
|
+
package: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
tier: string;
|
|
41
|
+
kind: "missing_required" | "missing_recommended" | "conflict" | "forbidden_present" | "ok_required";
|
|
42
|
+
installCmd?: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface DoctorReport {
|
|
46
|
+
piCliAvailable: boolean;
|
|
47
|
+
detectSource: DetectSource;
|
|
48
|
+
installedNames: string[];
|
|
49
|
+
missingRequired: DoctorFinding[];
|
|
50
|
+
missingRecommendedA: DoctorFinding[];
|
|
51
|
+
missingRecommendedB: DoctorFinding[];
|
|
52
|
+
conflicts: DoctorFinding[];
|
|
53
|
+
forbiddenPresent: DoctorFinding[];
|
|
54
|
+
okRequired: DoctorFinding[];
|
|
55
|
+
/** true when required all present and no conflicts */
|
|
56
|
+
ok: boolean;
|
|
57
|
+
/** CLI doctor exit should be non-zero when !ok or pi missing for strict mode */
|
|
58
|
+
exitCode: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const DEFAULT_MANIFEST: PiPluginsManifest;
|
|
61
|
+
/** Normalize package name for set membership (lowercase, strip version suffix). */
|
|
62
|
+
export declare function normalizePackageName(raw: string): string;
|
|
63
|
+
export declare function installCommandFor(plugin: ManifestPlugin, template?: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Loose parse of `pi list` / package listing text.
|
|
66
|
+
* Accepts lines like:
|
|
67
|
+
* - @tintinweb/pi-subagents@0.14.2
|
|
68
|
+
* - npm:@tintinweb/pi-subagents@0.14.2
|
|
69
|
+
* - pi-powerbar 0.1.0
|
|
70
|
+
* - "name": "@tintinweb/pi-subagents"
|
|
71
|
+
* Skips URLs, filesystem paths, and section headers ("User packages:").
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseInstalledFromText(text: string): InstalledPackage[];
|
|
74
|
+
export declare function parseInstalledFromSettings(settingsJson: unknown): InstalledPackage[];
|
|
75
|
+
/**
|
|
76
|
+
* Classify installed packages against manifest.
|
|
77
|
+
* Special case: forbidden id `pi-subagents` means unscoped only — do not match `@tintinweb/pi-subagents`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function classifyPiPlugins(input: DoctorClassifyInput): DoctorReport;
|
|
80
|
+
/**
|
|
81
|
+
* Unscoped `pi-subagents` must not match `@tintinweb/pi-subagents`.
|
|
82
|
+
* Presence of unscoped name alone is enough for conflict (even if tintinweb also installed).
|
|
83
|
+
*/
|
|
84
|
+
export declare function isForbiddenPresent(set: Set<string>, forbiddenPackage: string): boolean;
|
|
85
|
+
export declare function formatDoctorReport(report: DoctorReport, opts?: {
|
|
86
|
+
header?: string;
|
|
87
|
+
}): string;
|
|
88
|
+
export declare function loadManifestFromSpec(): Promise<PiPluginsManifest>;
|
|
89
|
+
export declare function normalizeManifest(data: Partial<PiPluginsManifest> | null | undefined): PiPluginsManifest;
|
|
90
|
+
export declare function detectInstalledPackages(opts?: {
|
|
91
|
+
homeDir?: string;
|
|
92
|
+
runPiList?: () => Promise<{
|
|
93
|
+
ok: boolean;
|
|
94
|
+
text: string;
|
|
95
|
+
}>;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
installed: InstalledPackage[];
|
|
98
|
+
piCliAvailable: boolean;
|
|
99
|
+
detectSource: DetectSource;
|
|
100
|
+
}>;
|
|
101
|
+
export declare function runPiPluginsDoctor(opts?: {
|
|
102
|
+
homeDir?: string;
|
|
103
|
+
manifest?: PiPluginsManifest;
|
|
104
|
+
installed?: InstalledPackage[];
|
|
105
|
+
runPiList?: () => Promise<{
|
|
106
|
+
ok: boolean;
|
|
107
|
+
text: string;
|
|
108
|
+
}>;
|
|
109
|
+
}): Promise<{
|
|
110
|
+
report: DoctorReport;
|
|
111
|
+
text: string;
|
|
112
|
+
}>;
|
|
113
|
+
/**
|
|
114
|
+
* Install required packages only (user scope). Never silent from callers without explicit flag.
|
|
115
|
+
*/
|
|
116
|
+
export declare function installRequiredPiPlugins(opts?: {
|
|
117
|
+
manifest?: PiPluginsManifest;
|
|
118
|
+
runInstall?: (args: string[]) => Promise<{
|
|
119
|
+
code: number;
|
|
120
|
+
stdout: string;
|
|
121
|
+
stderr: string;
|
|
122
|
+
}>;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
attempted: string[];
|
|
125
|
+
results: Array<{
|
|
126
|
+
cmd: string;
|
|
127
|
+
code: number;
|
|
128
|
+
stderr: string;
|
|
129
|
+
}>;
|
|
130
|
+
}>;
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi user-level plugins doctor (TOOLING-002L / PB-010 / pi-plugin-doctor).
|
|
3
|
+
* Pure classify + format; I/O helpers injectable for tests.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { runCommand } from "../exec.js";
|
|
11
|
+
import { pathExists, readText } from "../fs.js";
|
|
12
|
+
export const DEFAULT_MANIFEST = {
|
|
13
|
+
version: 1,
|
|
14
|
+
scope: "user",
|
|
15
|
+
source_protocol: "npm:",
|
|
16
|
+
install_template: "pi install npm:{package}{versionSuffix}",
|
|
17
|
+
required: [
|
|
18
|
+
{
|
|
19
|
+
id: "@tintinweb/pi-subagents",
|
|
20
|
+
package: "@tintinweb/pi-subagents",
|
|
21
|
+
version: "0.14.2",
|
|
22
|
+
tier: "required",
|
|
23
|
+
reason: "Single approved Pi subagent runtime for AIWS force policy",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
recommended_a: [
|
|
27
|
+
{ id: "pi-extension-settings", package: "pi-extension-settings", tier: "recommended_a" },
|
|
28
|
+
{ id: "pi-powerbar", package: "pi-powerbar", tier: "recommended_a" },
|
|
29
|
+
{ id: "pi-lsp", package: "pi-lsp", tier: "recommended_a" },
|
|
30
|
+
{ id: "pi-hashline-edit-pro", package: "pi-hashline-edit-pro", tier: "recommended_a" },
|
|
31
|
+
{ id: "rpiv-todo", package: "rpiv-todo", tier: "recommended_a" },
|
|
32
|
+
{ id: "rpiv-ask-user-question", package: "rpiv-ask-user-question", tier: "recommended_a" },
|
|
33
|
+
{ id: "pi-btw", package: "pi-btw", tier: "recommended_a" },
|
|
34
|
+
{ id: "pi-caffeinate", package: "pi-caffeinate", tier: "recommended_a" },
|
|
35
|
+
{ id: "pi-workspace-history", package: "pi-workspace-history", tier: "recommended_a" },
|
|
36
|
+
],
|
|
37
|
+
recommended_b: [
|
|
38
|
+
{ id: "pi-mcp-adapter", package: "pi-mcp-adapter", tier: "recommended_b" },
|
|
39
|
+
{ id: "pi-fff", package: "pi-fff", tier: "recommended_b" },
|
|
40
|
+
{ id: "pi-slopchop", package: "pi-slopchop", tier: "recommended_b" },
|
|
41
|
+
{ id: "plan-mode", package: "plan-mode", tier: "recommended_b" },
|
|
42
|
+
{ id: "pi-cache-optimizer", package: "pi-cache-optimizer", tier: "recommended_b" },
|
|
43
|
+
{ id: "pi-rtk-optimizer", package: "pi-rtk-optimizer", tier: "recommended_b" },
|
|
44
|
+
{ id: "pi-agent-browser-native", package: "pi-agent-browser-native", tier: "recommended_b" },
|
|
45
|
+
{ id: "pi-add-dir", package: "pi-add-dir", tier: "recommended_b" },
|
|
46
|
+
],
|
|
47
|
+
forbidden: [
|
|
48
|
+
{
|
|
49
|
+
id: "@narumitw/pi-subagents",
|
|
50
|
+
package: "@narumitw/pi-subagents",
|
|
51
|
+
severity: "conflict",
|
|
52
|
+
reason: "Second subagent runtime; exclusive with @tintinweb/pi-subagents",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "pi-subagents",
|
|
56
|
+
package: "pi-subagents",
|
|
57
|
+
severity: "conflict",
|
|
58
|
+
reason: "Unscoped/third-party subagents package; exclusive with @tintinweb/pi-subagents",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "@narumitw/pi-goal",
|
|
62
|
+
package: "@narumitw/pi-goal",
|
|
63
|
+
severity: "forbidden",
|
|
64
|
+
reason: "Dual goal orchestrator vs aiws goal",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "pi-autoresearch",
|
|
68
|
+
package: "pi-autoresearch",
|
|
69
|
+
severity: "forbidden",
|
|
70
|
+
reason: "Long main-write research loops under AIWS governance",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
/** Normalize package name for set membership (lowercase, strip version suffix). */
|
|
75
|
+
export function normalizePackageName(raw) {
|
|
76
|
+
let s = raw.trim().replace(/^npm:/i, "");
|
|
77
|
+
// strip trailing @version for unscoped; keep scope for @scope/name@version
|
|
78
|
+
if (s.startsWith("@")) {
|
|
79
|
+
const at = s.lastIndexOf("@");
|
|
80
|
+
if (at > 0)
|
|
81
|
+
s = s.slice(0, at);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const at = s.indexOf("@");
|
|
85
|
+
if (at > 0)
|
|
86
|
+
s = s.slice(0, at);
|
|
87
|
+
}
|
|
88
|
+
return s.toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
export function installCommandFor(plugin, template) {
|
|
91
|
+
const t = template || DEFAULT_MANIFEST.install_template;
|
|
92
|
+
const versionSuffix = plugin.version ? `@${plugin.version}` : "";
|
|
93
|
+
return t.replace("{package}", plugin.package).replace("{versionSuffix}", versionSuffix);
|
|
94
|
+
}
|
|
95
|
+
const PARSE_SKIP_TOKENS = new Set([
|
|
96
|
+
"installed",
|
|
97
|
+
"packages",
|
|
98
|
+
"package",
|
|
99
|
+
"name",
|
|
100
|
+
"version",
|
|
101
|
+
"user",
|
|
102
|
+
"project",
|
|
103
|
+
"global",
|
|
104
|
+
"local",
|
|
105
|
+
"https",
|
|
106
|
+
"http",
|
|
107
|
+
"git",
|
|
108
|
+
"github",
|
|
109
|
+
"com",
|
|
110
|
+
"npm",
|
|
111
|
+
]);
|
|
112
|
+
/**
|
|
113
|
+
* Loose parse of `pi list` / package listing text.
|
|
114
|
+
* Accepts lines like:
|
|
115
|
+
* - @tintinweb/pi-subagents@0.14.2
|
|
116
|
+
* - npm:@tintinweb/pi-subagents@0.14.2
|
|
117
|
+
* - pi-powerbar 0.1.0
|
|
118
|
+
* - "name": "@tintinweb/pi-subagents"
|
|
119
|
+
* Skips URLs, filesystem paths, and section headers ("User packages:").
|
|
120
|
+
*/
|
|
121
|
+
export function parseInstalledFromText(text) {
|
|
122
|
+
const out = new Map();
|
|
123
|
+
const lines = text.split(/\r?\n/);
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
127
|
+
continue;
|
|
128
|
+
// git/http install lines: last path segment may be a package id
|
|
129
|
+
const gitUrl = trimmed.match(/^https?:\/\/[^\s]+\/([^/\s]+?)(?:\.git)?\/?\s*$/i);
|
|
130
|
+
if (gitUrl?.[1]) {
|
|
131
|
+
const name = normalizePackageName(gitUrl[1]);
|
|
132
|
+
if (name && !PARSE_SKIP_TOKENS.has(name) && (name.includes("-") || name.includes("_"))) {
|
|
133
|
+
out.set(name, { name, raw: trimmed });
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (/^https?:\/\//i.test(trimmed))
|
|
138
|
+
continue;
|
|
139
|
+
if (/^\/|^[A-Za-z]:[\\/]/.test(trimmed))
|
|
140
|
+
continue;
|
|
141
|
+
if (/packages?\s*:/i.test(trimmed) && !trimmed.includes("@") && !/^npm:/i.test(trimmed))
|
|
142
|
+
continue;
|
|
143
|
+
// JSON-ish "name": "pkg"
|
|
144
|
+
const jsonName = trimmed.match(/"name"\s*:\s*"(@?[^"]+)"/i);
|
|
145
|
+
if (jsonName?.[1]) {
|
|
146
|
+
const name = normalizePackageName(jsonName[1]);
|
|
147
|
+
if (name && !PARSE_SKIP_TOKENS.has(name))
|
|
148
|
+
out.set(name, { name, raw: trimmed });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// npm:pkg@version or pkg@version
|
|
152
|
+
const npmPref = trimmed.replace(/^npm:/i, "").trim();
|
|
153
|
+
const scoped = npmPref.match(/^(@[^/\s]+\/[^@\s]+)(?:@([^\s]+))?/);
|
|
154
|
+
if (scoped?.[1]) {
|
|
155
|
+
const name = normalizePackageName(scoped[1]);
|
|
156
|
+
out.set(name, { name, version: scoped[2], raw: trimmed });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const unscoped = npmPref.match(/^([a-z0-9][a-z0-9._-]{1,})(?:@([^\s]+))?$/i);
|
|
160
|
+
if (unscoped?.[1]) {
|
|
161
|
+
const name = normalizePackageName(unscoped[1]);
|
|
162
|
+
if (PARSE_SKIP_TOKENS.has(name))
|
|
163
|
+
continue;
|
|
164
|
+
if (!name.includes("-") && !name.includes("_") && name.length < 4)
|
|
165
|
+
continue;
|
|
166
|
+
out.set(name, { name, version: unscoped[2], raw: trimmed });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Array.from(out.values());
|
|
170
|
+
}
|
|
171
|
+
export function parseInstalledFromSettings(settingsJson) {
|
|
172
|
+
const out = new Map();
|
|
173
|
+
if (!settingsJson || typeof settingsJson !== "object")
|
|
174
|
+
return [];
|
|
175
|
+
const obj = settingsJson;
|
|
176
|
+
const candidates = [];
|
|
177
|
+
for (const key of ["packages", "plugins", "extensions", "installed"]) {
|
|
178
|
+
if (key in obj)
|
|
179
|
+
candidates.push(obj[key]);
|
|
180
|
+
}
|
|
181
|
+
const addName = (raw, version) => {
|
|
182
|
+
const name = normalizePackageName(raw);
|
|
183
|
+
if (!name)
|
|
184
|
+
return;
|
|
185
|
+
out.set(name, { name, version, raw });
|
|
186
|
+
};
|
|
187
|
+
for (const c of candidates) {
|
|
188
|
+
if (Array.isArray(c)) {
|
|
189
|
+
for (const item of c) {
|
|
190
|
+
if (typeof item === "string")
|
|
191
|
+
addName(item);
|
|
192
|
+
else if (item && typeof item === "object") {
|
|
193
|
+
const rec = item;
|
|
194
|
+
const n = String(rec.name || rec.package || rec.id || "").trim();
|
|
195
|
+
if (n)
|
|
196
|
+
addName(n, rec.version != null ? String(rec.version) : undefined);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (c && typeof c === "object") {
|
|
201
|
+
for (const [k, v] of Object.entries(c)) {
|
|
202
|
+
if (typeof v === "string")
|
|
203
|
+
addName(k.includes("/") || k.startsWith("@") ? k : k, v);
|
|
204
|
+
else if (v && typeof v === "object") {
|
|
205
|
+
const rec = v;
|
|
206
|
+
const n = String(rec.name || rec.package || k).trim();
|
|
207
|
+
addName(n, rec.version != null ? String(rec.version) : undefined);
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
addName(k);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return Array.from(out.values());
|
|
215
|
+
}
|
|
216
|
+
function installedSet(installed) {
|
|
217
|
+
return new Set(installed.map((p) => normalizePackageName(p.name)));
|
|
218
|
+
}
|
|
219
|
+
function isPresent(set, pkg) {
|
|
220
|
+
return set.has(normalizePackageName(pkg));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Classify installed packages against manifest.
|
|
224
|
+
* Special case: forbidden id `pi-subagents` means unscoped only — do not match `@tintinweb/pi-subagents`.
|
|
225
|
+
*/
|
|
226
|
+
export function classifyPiPlugins(input) {
|
|
227
|
+
const { manifest, installed, piCliAvailable, detectSource } = input;
|
|
228
|
+
const set = installedSet(installed);
|
|
229
|
+
const names = Array.from(set).sort();
|
|
230
|
+
const missingRequired = [];
|
|
231
|
+
const okRequired = [];
|
|
232
|
+
for (const p of manifest.required || []) {
|
|
233
|
+
if (isPresent(set, p.package)) {
|
|
234
|
+
okRequired.push({
|
|
235
|
+
package: p.package,
|
|
236
|
+
version: p.version,
|
|
237
|
+
tier: "required",
|
|
238
|
+
kind: "ok_required",
|
|
239
|
+
reason: p.reason,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
missingRequired.push({
|
|
244
|
+
package: p.package,
|
|
245
|
+
version: p.version,
|
|
246
|
+
tier: "required",
|
|
247
|
+
kind: "missing_required",
|
|
248
|
+
installCmd: installCommandFor(p, manifest.install_template),
|
|
249
|
+
reason: p.reason,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const missingRecommendedA = [];
|
|
254
|
+
for (const p of manifest.recommended_a || []) {
|
|
255
|
+
if (!isPresent(set, p.package)) {
|
|
256
|
+
missingRecommendedA.push({
|
|
257
|
+
package: p.package,
|
|
258
|
+
version: p.version,
|
|
259
|
+
tier: "recommended_a",
|
|
260
|
+
kind: "missing_recommended",
|
|
261
|
+
installCmd: installCommandFor(p, manifest.install_template),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const missingRecommendedB = [];
|
|
266
|
+
for (const p of manifest.recommended_b || []) {
|
|
267
|
+
if (!isPresent(set, p.package)) {
|
|
268
|
+
missingRecommendedB.push({
|
|
269
|
+
package: p.package,
|
|
270
|
+
version: p.version,
|
|
271
|
+
tier: "recommended_b",
|
|
272
|
+
kind: "missing_recommended",
|
|
273
|
+
installCmd: installCommandFor(p, manifest.install_template),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const conflicts = [];
|
|
278
|
+
const forbiddenPresent = [];
|
|
279
|
+
for (const p of manifest.forbidden || []) {
|
|
280
|
+
const present = isForbiddenPresent(set, p.package);
|
|
281
|
+
if (!present)
|
|
282
|
+
continue;
|
|
283
|
+
const severity = p.severity === "forbidden" ? "forbidden" : "conflict";
|
|
284
|
+
const finding = {
|
|
285
|
+
package: p.package,
|
|
286
|
+
tier: "forbidden",
|
|
287
|
+
kind: severity === "forbidden" ? "forbidden_present" : "conflict",
|
|
288
|
+
reason: p.reason,
|
|
289
|
+
};
|
|
290
|
+
if (severity === "forbidden")
|
|
291
|
+
forbiddenPresent.push(finding);
|
|
292
|
+
else
|
|
293
|
+
conflicts.push(finding);
|
|
294
|
+
}
|
|
295
|
+
const hasBlockers = missingRequired.length > 0 || conflicts.length > 0 || forbiddenPresent.length > 0;
|
|
296
|
+
const ok = missingRequired.length === 0 && conflicts.length === 0 && forbiddenPresent.length === 0;
|
|
297
|
+
// doctor CLI: non-zero if missing required or conflicts/forbidden; pi missing alone is warn (exit 0) unless required also missing
|
|
298
|
+
let exitCode = 0;
|
|
299
|
+
if (hasBlockers)
|
|
300
|
+
exitCode = 1;
|
|
301
|
+
else if (!piCliAvailable && detectSource === "none")
|
|
302
|
+
exitCode = 0; // graceful
|
|
303
|
+
return {
|
|
304
|
+
piCliAvailable,
|
|
305
|
+
detectSource,
|
|
306
|
+
installedNames: names,
|
|
307
|
+
missingRequired,
|
|
308
|
+
missingRecommendedA,
|
|
309
|
+
missingRecommendedB,
|
|
310
|
+
conflicts,
|
|
311
|
+
forbiddenPresent,
|
|
312
|
+
okRequired,
|
|
313
|
+
ok,
|
|
314
|
+
exitCode,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Unscoped `pi-subagents` must not match `@tintinweb/pi-subagents`.
|
|
319
|
+
* Presence of unscoped name alone is enough for conflict (even if tintinweb also installed).
|
|
320
|
+
*/
|
|
321
|
+
export function isForbiddenPresent(set, forbiddenPackage) {
|
|
322
|
+
const n = normalizePackageName(forbiddenPackage);
|
|
323
|
+
if (n === "pi-subagents") {
|
|
324
|
+
return set.has("pi-subagents");
|
|
325
|
+
}
|
|
326
|
+
return set.has(n);
|
|
327
|
+
}
|
|
328
|
+
export function formatDoctorReport(report, opts) {
|
|
329
|
+
const lines = [];
|
|
330
|
+
lines.push(opts?.header ?? "aiws doctor: Pi user-level plugins");
|
|
331
|
+
lines.push(`detect: ${report.detectSource}${report.piCliAvailable ? " (pi cli ok)" : " (pi cli missing or unused)"}`);
|
|
332
|
+
lines.push(`installed: ${report.installedNames.length ? report.installedNames.join(", ") : "(none detected)"}`);
|
|
333
|
+
lines.push("");
|
|
334
|
+
if (!report.piCliAvailable && report.detectSource === "none") {
|
|
335
|
+
lines.push("warn: `pi` CLI not found and no ~/.pi/agent settings packages detected.");
|
|
336
|
+
lines.push(" Install Pi, then: pi install npm:@tintinweb/pi-subagents@0.14.2");
|
|
337
|
+
lines.push("");
|
|
338
|
+
}
|
|
339
|
+
if (report.missingRequired.length) {
|
|
340
|
+
lines.push("## missing required");
|
|
341
|
+
for (const f of report.missingRequired) {
|
|
342
|
+
lines.push(`- ${f.package}${f.version ? `@${f.version}` : ""}${f.reason ? ` — ${f.reason}` : ""}`);
|
|
343
|
+
if (f.installCmd)
|
|
344
|
+
lines.push(` ${f.installCmd}`);
|
|
345
|
+
}
|
|
346
|
+
lines.push("");
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
lines.push("## required: ok");
|
|
350
|
+
for (const f of report.okRequired) {
|
|
351
|
+
lines.push(`- ${f.package}${f.version ? ` (pin ${f.version})` : ""}`);
|
|
352
|
+
}
|
|
353
|
+
lines.push("");
|
|
354
|
+
}
|
|
355
|
+
if (report.conflicts.length) {
|
|
356
|
+
lines.push("## conflicts (remove or disable; keep only @tintinweb/pi-subagents)");
|
|
357
|
+
for (const f of report.conflicts) {
|
|
358
|
+
lines.push(`- ${f.package}${f.reason ? ` — ${f.reason}` : ""}`);
|
|
359
|
+
}
|
|
360
|
+
lines.push("");
|
|
361
|
+
}
|
|
362
|
+
if (report.forbiddenPresent.length) {
|
|
363
|
+
lines.push("## forbidden present (AIWS governance recommends removing)");
|
|
364
|
+
for (const f of report.forbiddenPresent) {
|
|
365
|
+
lines.push(`- ${f.package}${f.reason ? ` — ${f.reason}` : ""}`);
|
|
366
|
+
}
|
|
367
|
+
lines.push("");
|
|
368
|
+
}
|
|
369
|
+
if (report.missingRecommendedA.length) {
|
|
370
|
+
lines.push("## missing recommended (tier A) — optional");
|
|
371
|
+
for (const f of report.missingRecommendedA.slice(0, 20)) {
|
|
372
|
+
lines.push(`- ${f.installCmd || f.package}`);
|
|
373
|
+
}
|
|
374
|
+
lines.push("");
|
|
375
|
+
}
|
|
376
|
+
if (report.missingRecommendedB.length) {
|
|
377
|
+
lines.push("## missing recommended (tier B) — on demand");
|
|
378
|
+
for (const f of report.missingRecommendedB.slice(0, 20)) {
|
|
379
|
+
lines.push(`- ${f.installCmd || f.package}`);
|
|
380
|
+
}
|
|
381
|
+
lines.push("");
|
|
382
|
+
}
|
|
383
|
+
lines.push(report.ok
|
|
384
|
+
? "status: ok (required present, no conflicts)"
|
|
385
|
+
: "status: action needed (see missing required / conflicts)");
|
|
386
|
+
lines.push("note: default is detect+prompt only; use `aiws doctor --install-pi-plugins` to install required (user scope).");
|
|
387
|
+
return lines.join("\n");
|
|
388
|
+
}
|
|
389
|
+
export async function loadManifestFromSpec() {
|
|
390
|
+
try {
|
|
391
|
+
const require = createRequire(import.meta.url);
|
|
392
|
+
let rootDir = null;
|
|
393
|
+
try {
|
|
394
|
+
const pkgJsonPath = require.resolve("@aipper/aiws-spec/package.json");
|
|
395
|
+
rootDir = path.dirname(pkgJsonPath);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
const here = fileURLToPath(import.meta.url);
|
|
399
|
+
rootDir = path.resolve(path.dirname(here), "../../../spec");
|
|
400
|
+
}
|
|
401
|
+
const jsonPath = path.join(rootDir, "docs", "pi-plugins-manifest.json");
|
|
402
|
+
if (await pathExists(jsonPath)) {
|
|
403
|
+
const data = JSON.parse(await readText(jsonPath));
|
|
404
|
+
return normalizeManifest(data);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// fall through
|
|
409
|
+
}
|
|
410
|
+
return DEFAULT_MANIFEST;
|
|
411
|
+
}
|
|
412
|
+
export function normalizeManifest(data) {
|
|
413
|
+
const base = DEFAULT_MANIFEST;
|
|
414
|
+
if (!data || typeof data !== "object")
|
|
415
|
+
return base;
|
|
416
|
+
return {
|
|
417
|
+
version: typeof data.version === "number" ? data.version : base.version,
|
|
418
|
+
scope: String(data.scope || base.scope),
|
|
419
|
+
source_protocol: String(data.source_protocol || base.source_protocol),
|
|
420
|
+
install_template: String(data.install_template || base.install_template),
|
|
421
|
+
required: Array.isArray(data.required) ? data.required : base.required,
|
|
422
|
+
recommended_a: Array.isArray(data.recommended_a) ? data.recommended_a : base.recommended_a,
|
|
423
|
+
recommended_b: Array.isArray(data.recommended_b) ? data.recommended_b : base.recommended_b,
|
|
424
|
+
forbidden: Array.isArray(data.forbidden) ? data.forbidden : base.forbidden,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export async function detectInstalledPackages(opts) {
|
|
428
|
+
const home = opts?.homeDir ?? os.homedir();
|
|
429
|
+
const runPiList = opts?.runPiList ??
|
|
430
|
+
(async () => {
|
|
431
|
+
try {
|
|
432
|
+
const r = await runCommand("pi", ["list"]);
|
|
433
|
+
if (r.code === 0 || r.stdout.trim()) {
|
|
434
|
+
return { ok: true, text: `${r.stdout}\n${r.stderr}` };
|
|
435
|
+
}
|
|
436
|
+
return { ok: false, text: `${r.stdout}\n${r.stderr}` };
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return { ok: false, text: "" };
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
const pi = await runPiList();
|
|
443
|
+
if (pi.ok && pi.text.trim()) {
|
|
444
|
+
const installed = parseInstalledFromText(pi.text);
|
|
445
|
+
if (installed.length > 0) {
|
|
446
|
+
return { installed, piCliAvailable: true, detectSource: "pi_list" };
|
|
447
|
+
}
|
|
448
|
+
// pi exists but empty list
|
|
449
|
+
return { installed: [], piCliAvailable: true, detectSource: "pi_list" };
|
|
450
|
+
}
|
|
451
|
+
// settings fallback
|
|
452
|
+
const settingsPath = path.join(home, ".pi", "agent", "settings.json");
|
|
453
|
+
try {
|
|
454
|
+
if (await pathExists(settingsPath)) {
|
|
455
|
+
const json = JSON.parse(await readText(settingsPath));
|
|
456
|
+
const installed = parseInstalledFromSettings(json);
|
|
457
|
+
// also scan packages dir names
|
|
458
|
+
const pkgDir = path.join(home, ".pi", "agent", "packages");
|
|
459
|
+
if (await pathExists(pkgDir)) {
|
|
460
|
+
const ents = await fs.readdir(pkgDir).catch(() => []);
|
|
461
|
+
for (const e of ents) {
|
|
462
|
+
const name = normalizePackageName(e.replace(/@/g, "/").replace(/^npm_/, ""));
|
|
463
|
+
// directory names are messy; also try reading package.json name
|
|
464
|
+
const pj = path.join(pkgDir, e, "package.json");
|
|
465
|
+
if (await pathExists(pj)) {
|
|
466
|
+
try {
|
|
467
|
+
const meta = JSON.parse(await readText(pj));
|
|
468
|
+
if (meta?.name) {
|
|
469
|
+
installed.push({ name: normalizePackageName(String(meta.name)), version: meta.version ? String(meta.version) : undefined });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
if (name)
|
|
474
|
+
installed.push({ name });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const dedup = new Map(installed.map((p) => [normalizePackageName(p.name), p]));
|
|
480
|
+
return {
|
|
481
|
+
installed: Array.from(dedup.values()),
|
|
482
|
+
piCliAvailable: false,
|
|
483
|
+
detectSource: "settings",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// ignore
|
|
489
|
+
}
|
|
490
|
+
return { installed: [], piCliAvailable: false, detectSource: "none" };
|
|
491
|
+
}
|
|
492
|
+
export async function runPiPluginsDoctor(opts) {
|
|
493
|
+
const manifest = opts?.manifest ?? (await loadManifestFromSpec());
|
|
494
|
+
let installed = opts?.installed;
|
|
495
|
+
let piCliAvailable = true;
|
|
496
|
+
let detectSource = "injected";
|
|
497
|
+
if (!installed) {
|
|
498
|
+
const det = await detectInstalledPackages({ homeDir: opts?.homeDir, runPiList: opts?.runPiList });
|
|
499
|
+
installed = det.installed;
|
|
500
|
+
piCliAvailable = det.piCliAvailable;
|
|
501
|
+
detectSource = det.detectSource;
|
|
502
|
+
}
|
|
503
|
+
const report = classifyPiPlugins({
|
|
504
|
+
manifest,
|
|
505
|
+
installed,
|
|
506
|
+
piCliAvailable,
|
|
507
|
+
detectSource,
|
|
508
|
+
});
|
|
509
|
+
return { report, text: formatDoctorReport(report) };
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Install required packages only (user scope). Never silent from callers without explicit flag.
|
|
513
|
+
*/
|
|
514
|
+
export async function installRequiredPiPlugins(opts) {
|
|
515
|
+
const manifest = opts?.manifest ?? (await loadManifestFromSpec());
|
|
516
|
+
const runInstall = opts?.runInstall ??
|
|
517
|
+
(async (args) => {
|
|
518
|
+
try {
|
|
519
|
+
return await runCommand("pi", args);
|
|
520
|
+
}
|
|
521
|
+
catch (e) {
|
|
522
|
+
return { code: 1, stdout: "", stderr: e instanceof Error ? e.message : String(e) };
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
const attempted = [];
|
|
526
|
+
const results = [];
|
|
527
|
+
for (const p of manifest.required || []) {
|
|
528
|
+
const versionSuffix = p.version ? `@${p.version}` : "";
|
|
529
|
+
const npmSpec = `npm:${p.package}${versionSuffix}`;
|
|
530
|
+
const cmd = `pi install ${npmSpec}`;
|
|
531
|
+
attempted.push(cmd);
|
|
532
|
+
const r = await runInstall(["install", npmSpec]);
|
|
533
|
+
results.push({ cmd, code: r.code, stderr: r.stderr });
|
|
534
|
+
}
|
|
535
|
+
return { attempted, results };
|
|
536
|
+
}
|
|
537
|
+
//# sourceMappingURL=pi-plugins-doctor.js.map
|