pi-feats 0.1.1
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/LICENSE +21 -0
- package/README.md +508 -0
- package/extensions/README.md +27 -0
- package/extensions/api-server/PLAN.md +70 -0
- package/extensions/api-server/README.md +103 -0
- package/extensions/api-server/application-log-store.ts +21 -0
- package/extensions/api-server/application-runtime.ts +212 -0
- package/extensions/api-server/application-store.ts +30 -0
- package/extensions/api-server/index.ts +52 -0
- package/extensions/api-server/profile-store.ts +367 -0
- package/extensions/api-server/server.ts +863 -0
- package/extensions/cli-resources.ts +564 -0
- package/extensions/guardrails/index.ts +178 -0
- package/extensions/lib/application-handler-templates.ts +63 -0
- package/extensions/lib/profile-env.ts +61 -0
- package/extensions/lib/profile-sandbox.ts +197 -0
- package/extensions/lib/remote-hosts.ts +392 -0
- package/extensions/pi-console-webui/app/[section]/page.tsx +4 -0
- package/extensions/pi-console-webui/app/api/admin/config/[target]/route.ts +5 -0
- package/extensions/pi-console-webui/app/api/admin/services/[service]/restart/route.ts +5 -0
- package/extensions/pi-console-webui/app/api/auth/login/route.ts +9 -0
- package/extensions/pi-console-webui/app/api/auth/logout/route.ts +3 -0
- package/extensions/pi-console-webui/app/api/message/app/[slug]/route.ts +11 -0
- package/extensions/pi-console-webui/app/api/pi/[...path]/route.ts +31 -0
- package/extensions/pi-console-webui/app/applications/[slug]/page.tsx +2 -0
- package/extensions/pi-console-webui/app/globals.css +41 -0
- package/extensions/pi-console-webui/app/icon.svg +1 -0
- package/extensions/pi-console-webui/app/layout.tsx +5 -0
- package/extensions/pi-console-webui/app/login/page.tsx +11 -0
- package/extensions/pi-console-webui/app/page.tsx +2 -0
- package/extensions/pi-console-webui/app/terminal/page.tsx +4 -0
- package/extensions/pi-console-webui/components/admin-config-form.tsx +16 -0
- package/extensions/pi-console-webui/components/application-handler-editor.tsx +39 -0
- package/extensions/pi-console-webui/components/application-logs.tsx +38 -0
- package/extensions/pi-console-webui/components/application-mappings.tsx +28 -0
- package/extensions/pi-console-webui/components/application-sessions.tsx +11 -0
- package/extensions/pi-console-webui/components/application-settings.tsx +60 -0
- package/extensions/pi-console-webui/components/application-workspace.tsx +14 -0
- package/extensions/pi-console-webui/components/applications.tsx +15 -0
- package/extensions/pi-console-webui/components/chat-workspace.tsx +42 -0
- package/extensions/pi-console-webui/components/console-page.tsx +23 -0
- package/extensions/pi-console-webui/components/console-state.tsx +30 -0
- package/extensions/pi-console-webui/components/console.tsx +115 -0
- package/extensions/pi-console-webui/components/guardrails-panel.tsx +78 -0
- package/extensions/pi-console-webui/components/package-resources.tsx +13 -0
- package/extensions/pi-console-webui/components/pulse-resources.tsx +41 -0
- package/extensions/pi-console-webui/components/skill-resources.tsx +35 -0
- package/extensions/pi-console-webui/components/skill-source-document-preview.tsx +7 -0
- package/extensions/pi-console-webui/components/skill-source-import.tsx +7 -0
- package/extensions/pi-console-webui/components/skill-sources.tsx +12 -0
- package/extensions/pi-console-webui/components/terminal-client.tsx +39 -0
- package/extensions/pi-console-webui/components/toast.tsx +18 -0
- package/extensions/pi-console-webui/components/ui/button.tsx +4 -0
- package/extensions/pi-console-webui/components/ui/card.tsx +4 -0
- package/extensions/pi-console-webui/components/ui/input.tsx +4 -0
- package/extensions/pi-console-webui/components/ui/switch.tsx +6 -0
- package/extensions/pi-console-webui/components/ui/tabs.tsx +11 -0
- package/extensions/pi-console-webui/components.json +8 -0
- package/extensions/pi-console-webui/index.ts +33 -0
- package/extensions/pi-console-webui/lib/admin-config.ts +22 -0
- package/extensions/pi-console-webui/lib/auth.ts +21 -0
- package/extensions/pi-console-webui/lib/config.ts +15 -0
- package/extensions/pi-console-webui/lib/pi-api.ts +9 -0
- package/extensions/pi-console-webui/lib/utils.ts +3 -0
- package/extensions/pi-console-webui/next-env.d.ts +6 -0
- package/extensions/pi-console-webui/next.config.js +5 -0
- package/extensions/pi-console-webui/postcss.config.js +1 -0
- package/extensions/pi-console-webui/tailwind.config.ts +2 -0
- package/extensions/pi-console-webui/tsconfig.json +41 -0
- package/extensions/profiles.ts +439 -0
- package/extensions/pulse/index.ts +62 -0
- package/extensions/pulse/store.ts +105 -0
- package/extensions/sequential-workflow.ts +270 -0
- package/extensions/skill-sources/index.ts +4 -0
- package/extensions/skill-sources/store.ts +118 -0
- package/package.json +89 -0
- package/scripts/install-nono.sh +34 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { SessionManager, type ExtensionAPI, type SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Box, render, Text } from "ink";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
type ListKind = "tools" | "skills" | "extensions";
|
|
10
|
+
type Row = [string, string, string];
|
|
11
|
+
|
|
12
|
+
const kindFromArgs = (args: string[]): ListKind | undefined => {
|
|
13
|
+
for (let index = 0; index < args.length - 1; index += 1) {
|
|
14
|
+
const kind = args[index];
|
|
15
|
+
if (args[index + 1] === "list" && (kind === "tools" || kind === "skills" || kind === "extensions")) {
|
|
16
|
+
return kind;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const BUILTIN_TOOLS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"] as const;
|
|
23
|
+
|
|
24
|
+
type Action = "disable" | "enable";
|
|
25
|
+
|
|
26
|
+
interface ParsedAction {
|
|
27
|
+
kind: ListKind;
|
|
28
|
+
action: Action;
|
|
29
|
+
target: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface SessionRename { id: string; name: string; }
|
|
33
|
+
interface PackageAction { action: Action; target: string; }
|
|
34
|
+
|
|
35
|
+
const packageRequestedFromArgs = (args: string[]) => args.some((arg, index) => arg === "packages" && args[index + 1] === "list");
|
|
36
|
+
const packageActionFromArgs = (args: string[]): PackageAction | undefined => {
|
|
37
|
+
for (let index = 0; index < args.length - 2; index += 1) if (args[index] === "packages" && (args[index + 1] === "enable" || args[index + 1] === "disable") && args[index + 2] && !args[index + 2].startsWith("--")) return { action: args[index + 1] as Action, target: args[index + 2] };
|
|
38
|
+
return undefined;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const sessionRenameFromArgs = (args: string[]): SessionRename | undefined => {
|
|
42
|
+
for (let index = 0; index < args.length - 3; index += 1) if (args[index] === "sessions" && args[index + 1] === "rename") {
|
|
43
|
+
const [id, name] = [args[index + 2], args[index + 3]];
|
|
44
|
+
if (!id || !name || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) throw new Error("usage: pi sessions rename <session-id> <name>");
|
|
45
|
+
return { id, name: name.trim() };
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const actionFromArgs = (args: string[]): ParsedAction | undefined => {
|
|
51
|
+
for (let index = 0; index < args.length - 2; index += 1) {
|
|
52
|
+
const kind = args[index];
|
|
53
|
+
const action = args[index + 1];
|
|
54
|
+
const target = args[index + 2];
|
|
55
|
+
if ((kind === "tools" || kind === "skills" || kind === "extensions") &&
|
|
56
|
+
(action === "disable" || action === "enable") &&
|
|
57
|
+
target && !target.startsWith("--")) {
|
|
58
|
+
return { kind, action, target };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const clean = (value: string) => value.replaceAll("\n", " ").replaceAll("\t", " ");
|
|
65
|
+
const clip = (value: string, width: number) => value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}…`;
|
|
66
|
+
|
|
67
|
+
function ResourceTable({ title, headers, rows, highlightStatus = false }: { title: string; headers: Row; rows: Row[]; highlightStatus?: boolean }) {
|
|
68
|
+
const terminalWidth = Math.max(80, process.stdout.columns ?? 80);
|
|
69
|
+
const available = terminalWidth - 10;
|
|
70
|
+
const widths: [number, number, number] = [
|
|
71
|
+
Math.max(12, Math.floor(available * 0.23)),
|
|
72
|
+
Math.max(12, Math.floor(available * 0.18)),
|
|
73
|
+
0,
|
|
74
|
+
];
|
|
75
|
+
widths[2] = available - widths[0] - widths[1];
|
|
76
|
+
const cell = (value: string, width: number) => clip(clean(value), width).padEnd(width);
|
|
77
|
+
const line = `┼${"─".repeat(widths[0] + 2)}┼${"─".repeat(widths[1] + 2)}┼${"─".repeat(widths[2] + 2)}┼`;
|
|
78
|
+
const top = line.replaceAll("┼", "┬").replace(/^┬/, "┌").replace(/┬$/, "┐");
|
|
79
|
+
const bottom = line.replaceAll("┼", "┴").replace(/^┴/, "└").replace(/┴$/, "┘");
|
|
80
|
+
const row = (values: Row) => `│ ${cell(values[0], widths[0])} │ ${cell(values[1], widths[1])} │ ${cell(values[2], widths[2])} │`;
|
|
81
|
+
|
|
82
|
+
const renderedRow = (values: Row, index: number) => {
|
|
83
|
+
const statusColor = values[1] === "enabled" ? "green" : "#f5c2d7";
|
|
84
|
+
return React.createElement(
|
|
85
|
+
Box,
|
|
86
|
+
{ flexDirection: "row", key: `${values.join("\0")}-${index}` },
|
|
87
|
+
React.createElement(Text, { color: "gray" }, "│ "),
|
|
88
|
+
React.createElement(Text, { color: "white" }, cell(values[0], widths[0])),
|
|
89
|
+
React.createElement(Text, { color: "gray" }, " │ "),
|
|
90
|
+
React.createElement(Text, { color: highlightStatus ? statusColor : "white" }, cell(values[1], widths[1])),
|
|
91
|
+
React.createElement(Text, { color: "gray" }, " │ "),
|
|
92
|
+
React.createElement(Text, { color: "white" }, cell(values[2], widths[2])),
|
|
93
|
+
React.createElement(Text, { color: "gray" }, " │"),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return React.createElement(
|
|
98
|
+
Box,
|
|
99
|
+
{ flexDirection: "column" },
|
|
100
|
+
React.createElement(Text, { color: "cyan", bold: true }, title),
|
|
101
|
+
React.createElement(Text, { color: "gray" }, top),
|
|
102
|
+
React.createElement(Text, { color: "cyan", bold: true }, row(headers)),
|
|
103
|
+
React.createElement(Text, { color: "gray" }, line),
|
|
104
|
+
...rows.map(renderedRow),
|
|
105
|
+
React.createElement(Text, { color: "gray" }, bottom),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type SourceRow = [string, string, string, string];
|
|
110
|
+
function SourceTable({ title, detailHeader, rows }: { title: string; detailHeader: string; rows: SourceRow[] }) {
|
|
111
|
+
const available = Math.max(80, process.stdout.columns ?? 80) - 13, widths: [number, number, number, number] = [Math.max(14, Math.floor(available * .20)), 10, Math.max(18, Math.floor(available * .27)), 0];
|
|
112
|
+
widths[3] = available - widths[0] - widths[1] - widths[2];
|
|
113
|
+
const cell = (value: string, width: number) => clip(clean(value), width).padEnd(width);
|
|
114
|
+
const line = `┼${"─".repeat(widths[0] + 2)}┼${"─".repeat(widths[1] + 2)}┼${"─".repeat(widths[2] + 2)}┼${"─".repeat(widths[3] + 2)}┼`;
|
|
115
|
+
const top = line.replaceAll("┼", "┬").replace(/^┬/, "┌").replace(/┬$/, "┐"), bottom = line.replaceAll("┼", "┴").replace(/^┴/, "└").replace(/┴$/, "┘");
|
|
116
|
+
const row = (values: SourceRow) => `│ ${cell(values[0], widths[0])} │ ${cell(values[1], widths[1])} │ ${cell(values[2], widths[2])} │ ${cell(values[3], widths[3])} │`;
|
|
117
|
+
const renderedRow = (values: SourceRow, index: number) => React.createElement(Box, { flexDirection: "row", key: `${values.join("\0")}-${index}` }, React.createElement(Text, { color: "gray" }, "│ "), React.createElement(Text, { color: "white" }, cell(values[0], widths[0])), React.createElement(Text, { color: "gray" }, " │ "), React.createElement(Text, { color: values[1] === "enabled" ? "green" : "#f5c2d7" }, cell(values[1], widths[1])), React.createElement(Text, { color: "gray" }, " │ "), React.createElement(Text, { color: "white" }, cell(values[2], widths[2])), React.createElement(Text, { color: "gray" }, " │ "), React.createElement(Text, { color: "white" }, cell(values[3], widths[3])), React.createElement(Text, { color: "gray" }, " │"));
|
|
118
|
+
return React.createElement(Box, { flexDirection: "column" }, React.createElement(Text, { color: "cyan", bold: true }, title), React.createElement(Text, { color: "gray" }, top), React.createElement(Text, { color: "cyan", bold: true }, row(["NAME", "STATUS", "SOURCE", detailHeader])), React.createElement(Text, { color: "gray" }, line), ...rows.map(renderedRow), React.createElement(Text, { color: "gray" }, bottom));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function SessionTable({ sessions }: { sessions: SessionInfo[] }) {
|
|
122
|
+
const terminalWidth = Math.max(80, process.stdout.columns ?? 80);
|
|
123
|
+
const available = terminalWidth - 13;
|
|
124
|
+
const widths = {
|
|
125
|
+
id: Math.max(16, Math.floor(available * 0.27)),
|
|
126
|
+
session: Math.max(16, Math.floor(available * 0.22)),
|
|
127
|
+
activity: 20,
|
|
128
|
+
path: 0,
|
|
129
|
+
};
|
|
130
|
+
widths.path = available - widths.id - widths.session - widths.activity;
|
|
131
|
+
const cell = (value: string, width: number) => clip(clean(value), width).padEnd(width);
|
|
132
|
+
const line = `┼${"─".repeat(widths.id + 2)}┼${"─".repeat(widths.session + 2)}┼${"─".repeat(widths.activity + 2)}┼${"─".repeat(widths.path + 2)}┼`;
|
|
133
|
+
const top = line.replaceAll("┼", "┬").replace(/^┬/, "┌").replace(/┬$/, "┐");
|
|
134
|
+
const bottom = line.replaceAll("┼", "┴").replace(/^┴/, "└").replace(/┴$/, "┘");
|
|
135
|
+
const row = (values: [string, string, string, string]) => `│ ${cell(values[0], widths.id)} │ ${cell(values[1], widths.session)} │ ${cell(values[2], widths.activity)} │ ${cell(values[3], widths.path)} │`;
|
|
136
|
+
const formatActivity = (date: Date) => new Intl.DateTimeFormat("en-GB", {
|
|
137
|
+
dateStyle: "medium",
|
|
138
|
+
timeStyle: "medium",
|
|
139
|
+
hour12: false,
|
|
140
|
+
}).format(date);
|
|
141
|
+
|
|
142
|
+
return React.createElement(
|
|
143
|
+
Box,
|
|
144
|
+
{ flexDirection: "column" },
|
|
145
|
+
React.createElement(Text, { color: "cyan", bold: true }, "SESSIONS"),
|
|
146
|
+
React.createElement(Text, { color: "gray" }, top),
|
|
147
|
+
React.createElement(Text, { color: "cyan", bold: true }, row(["ID", "SESSION", "LAST ACTIVITY", "PATH"])),
|
|
148
|
+
React.createElement(Text, { color: "gray" }, line),
|
|
149
|
+
...sessions.map((session) => React.createElement(
|
|
150
|
+
Text,
|
|
151
|
+
{ color: "white", key: session.path },
|
|
152
|
+
row([session.id, session.name ?? session.firstMessage, formatActivity(session.modified), session.path]),
|
|
153
|
+
)),
|
|
154
|
+
React.createElement(Text, { color: "gray" }, bottom),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function requestedProfile(): string | undefined {
|
|
159
|
+
const args = process.argv.slice(2);
|
|
160
|
+
const index = args.indexOf("--profile");
|
|
161
|
+
if (index >= 0) return args[index + 1];
|
|
162
|
+
return args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function readSettings() {
|
|
166
|
+
const configuredAgentDir = process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent");
|
|
167
|
+
const rootAgentDir = process.env.PI_PROFILE_ROOT ?? configuredAgentDir;
|
|
168
|
+
const profile = process.env.PI_ACTIVE_PROFILE ?? requestedProfile();
|
|
169
|
+
// Sandboxed profiles execute directly in their own persistent directory.
|
|
170
|
+
const agentDir = configuredAgentDir;
|
|
171
|
+
const path = join(agentDir, "settings.json");
|
|
172
|
+
const runtimePath = join(rootAgentDir, "settings.json");
|
|
173
|
+
const settings = existsSync(path) ? JSON.parse(await readFile(path, "utf8")) as Record<string, unknown> : {} as Record<string, unknown>;
|
|
174
|
+
// Extensions and packages are controlled by the default runtime, even when
|
|
175
|
+
// a command is operating on a named profile workspace.
|
|
176
|
+
const runtimeSettings = runtimePath === path ? settings : existsSync(runtimePath) ? JSON.parse(await readFile(runtimePath, "utf8")) as Record<string, unknown> : {} as Record<string, unknown>;
|
|
177
|
+
return { agentDir, resourceRoot: rootAgentDir, settings, runtimeSettings };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const isExcluded = (resourcePath: string, exclusions: string[]): boolean =>
|
|
181
|
+
exclusions.includes(`!${resourcePath}`);
|
|
182
|
+
|
|
183
|
+
async function findSkillFiles(path: string): Promise<string[]> {
|
|
184
|
+
if (!existsSync(path)) return [];
|
|
185
|
+
if (basename(path) === "SKILL.md") return [path];
|
|
186
|
+
|
|
187
|
+
const files: string[] = [];
|
|
188
|
+
const skillFile = join(path, "SKILL.md");
|
|
189
|
+
if (existsSync(skillFile)) files.push(skillFile);
|
|
190
|
+
try {
|
|
191
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
192
|
+
if (entry.isDirectory()) files.push(...await findSkillFiles(join(path, entry.name)));
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
return files;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function skillEnabled(resourcePath: string, skillRoot: string, settings: Record<string, unknown>, exclusions: string[]): boolean {
|
|
199
|
+
const policy = (settings.profile as { enabledSkills?: unknown } | undefined)?.enabledSkills;
|
|
200
|
+
const name = relative(skillRoot, resourcePath).replaceAll("\\", "/");
|
|
201
|
+
if (Array.isArray(policy)) return policy.includes("*") || policy.includes(name);
|
|
202
|
+
return !isExcluded(resourcePath, exclusions);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function skillRows(paths: string[], skillRoot: string, settings: Record<string, unknown>, exclusions: string[]): Promise<Row[]> {
|
|
206
|
+
const rows: Row[] = [];
|
|
207
|
+
const seen = new Set<string>();
|
|
208
|
+
for (const path of paths) {
|
|
209
|
+
for (const skillFile of await findSkillFiles(path)) {
|
|
210
|
+
if (seen.has(skillFile)) continue;
|
|
211
|
+
seen.add(skillFile);
|
|
212
|
+
const resourcePath = dirname(skillFile);
|
|
213
|
+
const name = relative(skillRoot, resourcePath).replaceAll("\\", "/");
|
|
214
|
+
rows.push([name && !name.startsWith("..") ? name : basename(resourcePath), skillEnabled(resourcePath, skillRoot, settings, exclusions) ? "enabled" : "disabled", skillFile]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return rows.sort((a, b) => a[0].localeCompare(b[0]) || a[2].localeCompare(b[2]));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function extensionRows(paths: string[], exclusions: string[]): Promise<Row[]> {
|
|
221
|
+
const rows: Row[] = [];
|
|
222
|
+
for (const path of paths) {
|
|
223
|
+
if (!existsSync(path)) continue;
|
|
224
|
+
if ([".ts", ".js"].includes(extname(path))) {
|
|
225
|
+
rows.push([basename(path, extname(path)), isExcluded(path, exclusions) ? "disabled" : "enabled", path]);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// A configured directory with index.ts/index.js is one extension. Its
|
|
229
|
+
// sibling source/config files are implementation details, not extensions.
|
|
230
|
+
if (existsSync(join(path, "index.ts")) || existsSync(join(path, "index.js"))) {
|
|
231
|
+
rows.push([basename(path), isExcluded(path, exclusions) ? "disabled" : "enabled", path]);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
235
|
+
if (entry.isFile() && [".ts", ".js"].includes(extname(entry.name))) {
|
|
236
|
+
const resourcePath = join(path, entry.name);
|
|
237
|
+
rows.push([basename(entry.name, extname(entry.name)), isExcluded(resourcePath, exclusions) ? "disabled" : "enabled", resourcePath]);
|
|
238
|
+
} else if (entry.isDirectory() && (existsSync(join(path, entry.name, "index.ts")) || existsSync(join(path, entry.name, "index.js")))) {
|
|
239
|
+
const resourcePath = join(path, entry.name);
|
|
240
|
+
rows.push([entry.name, isExcluded(resourcePath, exclusions) ? "disabled" : "enabled", resourcePath]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return rows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type ConfiguredPackage = { source: string; base: string; name: string; manifest?: { version?: string; description?: string; pi?: { extensions?: unknown } } };
|
|
248
|
+
|
|
249
|
+
function packageBase(source: string, resourceRoot: string): string | undefined {
|
|
250
|
+
if (source.startsWith("npm:")) return join(resourceRoot, "npm", "node_modules", source.slice(4));
|
|
251
|
+
if (source.startsWith(".") || source.startsWith("/")) return resolve(resourceRoot, source);
|
|
252
|
+
if (!source.startsWith("git:") && !/^(?:https?|ssh):\/\//.test(source)) return undefined;
|
|
253
|
+
let remote = source.replace(/^git:/, "");
|
|
254
|
+
const ref = remote.lastIndexOf("@");
|
|
255
|
+
if (ref > remote.lastIndexOf("/")) remote = remote.slice(0, ref);
|
|
256
|
+
remote = remote.replace(/^https?:\/\//, "").replace(/^ssh:\/\/git@/, "").replace(/^git@/, "").replace(/^([^/:]+):/, "$1/").replace(/\.git$/, "");
|
|
257
|
+
return join(resourceRoot, "git", remote);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function configuredPackages(resourceRoot: string, settings: Record<string, unknown>): Promise<ConfiguredPackage[]> {
|
|
261
|
+
const sources = Array.isArray(settings.packages) ? settings.packages.filter((value): value is string => typeof value === "string") : [];
|
|
262
|
+
const packages: ConfiguredPackage[] = [];
|
|
263
|
+
for (const source of sources) {
|
|
264
|
+
const base = packageBase(source, resourceRoot);
|
|
265
|
+
if (!base) continue;
|
|
266
|
+
try {
|
|
267
|
+
const manifest = JSON.parse(await readFile(join(base, "package.json"), "utf8")) as ConfiguredPackage["manifest"] & { name?: string };
|
|
268
|
+
packages.push({ source, base, name: manifest?.name ?? source, manifest });
|
|
269
|
+
} catch {
|
|
270
|
+
packages.push({ source, base, name: source.replace(/^npm:/, "") });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return packages;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function configuredPackageExtensions(resourceRoot: string, settings: Record<string, unknown>): Promise<Array<{ path: string; packageName: string }>> {
|
|
277
|
+
const paths: Array<{ path: string; packageName: string }> = [];
|
|
278
|
+
for (const pkg of await configuredPackages(resourceRoot, settings)) {
|
|
279
|
+
for (const extension of Array.isArray(pkg.manifest?.pi?.extensions) ? pkg.manifest.pi.extensions : []) {
|
|
280
|
+
if (typeof extension === "string") paths.push({ path: join(pkg.base, extension), packageName: pkg.name });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return paths;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function packageToolSources(resourceRoot: string, settings: Record<string, unknown>, names: string[]): Promise<Map<string, string>> {
|
|
287
|
+
const sources = new Map<string, string>();
|
|
288
|
+
for (const { path, packageName } of await configuredPackageExtensions(resourceRoot, settings)) try { const source = await readFile(path, "utf8"); for (const name of names) if (source.includes(`"${name}"`) || source.includes(`'${name}'`) || source.includes(`\`${name}\``)) sources.set(name, packageName); } catch {}
|
|
289
|
+
return sources;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function packageRows(resourceRoot: string, settings: Record<string, unknown>): Promise<Row[]> {
|
|
293
|
+
return (await configuredPackages(resourceRoot, settings))
|
|
294
|
+
.map((pkg) => [pkg.name, existsSync(join(pkg.base, "package.json")) ? "enabled" : "disabled", pkg.manifest ? `${pkg.manifest.version ?? "unknown version"}${pkg.manifest.description ? ` · ${pkg.manifest.description}` : ""}` : "package metadata unavailable"] as Row)
|
|
295
|
+
.sort((left, right) => left[0].localeCompare(right[0]));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function applyPackageAction(action: PackageAction) {
|
|
299
|
+
const { resourceRoot, runtimeSettings } = await readSettings();
|
|
300
|
+
const agentDir = resourceRoot, settings = runtimeSettings;
|
|
301
|
+
const name = action.target.replace(/^npm:/, "");
|
|
302
|
+
if (!existsSync(join(resourceRoot, "npm", "node_modules", name, "package.json"))) throw new Error(`Package "${name}" is not installed in ${join(resourceRoot, "npm")}.`);
|
|
303
|
+
const entry = `npm:${name}`, configured = Array.isArray(settings.packages) ? settings.packages.filter((value): value is string => typeof value === "string") : [];
|
|
304
|
+
const packages = action.action === "enable" ? [...new Set([...configured.filter((value) => value !== name), entry])] : configured.filter((value) => value !== entry && value !== name);
|
|
305
|
+
const updated = { ...settings, packages } as Record<string, unknown>; if (packages.length === 0) delete updated.packages;
|
|
306
|
+
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify(updated, null, 2)}\n`, "utf8");
|
|
307
|
+
console.log(`Package "${name}" ${action.action === "enable" ? "enabled" : "disabled"} in ${join(agentDir, "settings.json")}. Use /reload to apply.`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function renderAndClose(element: React.ReactElement) {
|
|
311
|
+
const app = render(element, { stdout: process.stdout, stdin: process.stdin, exitOnCtrlC: false, patchConsole: false });
|
|
312
|
+
await new Promise((resolveRender) => setTimeout(resolveRender, 25));
|
|
313
|
+
app.unmount();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function setExclusion(entries: string[], resourcePath: string, disable: boolean) {
|
|
317
|
+
const exclusion = `!${resourcePath}`;
|
|
318
|
+
if (disable) return entries.includes(exclusion) ? entries : [...entries, exclusion];
|
|
319
|
+
return entries.filter((entry) => entry !== exclusion);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function findResource(kind: "extensions" | "skills", target: string, settings: Record<string, unknown>, agentDir: string, resourceRoot: string) {
|
|
323
|
+
if (existsSync(target)) {
|
|
324
|
+
if (kind === "skills" && (basename(target) === "SKILL.md" || existsSync(join(target, "SKILL.md")))) {
|
|
325
|
+
return basename(target) === "SKILL.md" ? dirname(target) : target;
|
|
326
|
+
}
|
|
327
|
+
if (kind === "extensions" && ([".ts", ".js"].includes(extname(target)) || existsSync(join(target, "index.ts")) || existsSync(join(target, "index.js")))) {
|
|
328
|
+
return target;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const rawEntries = Array.isArray(settings[kind]) ? settings[kind].filter((entry): entry is string => typeof entry === "string") : [];
|
|
333
|
+
const roots = [join(agentDir, kind), ...(kind === "skills" ? [join(resourceRoot, "skills")] : []), ...rawEntries.filter((entry) => !/^[!+\-]/.test(entry)), join(process.cwd(), ".pi", kind)];
|
|
334
|
+
const name = target.replace(/^[!+\-]+/, "");
|
|
335
|
+
|
|
336
|
+
for (const root of [...new Set(roots)]) {
|
|
337
|
+
if (!existsSync(root)) continue;
|
|
338
|
+
if (kind === "skills") {
|
|
339
|
+
for (const skillFile of await findSkillFiles(root)) {
|
|
340
|
+
const resourcePath = dirname(skillFile);
|
|
341
|
+
if (basename(resourcePath) === name) return resourcePath;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if ([".ts", ".js"].includes(extname(root)) && basename(root, extname(root)) === name) return root;
|
|
347
|
+
try {
|
|
348
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
349
|
+
if (entry.isFile() && [".ts", ".js"].includes(extname(entry.name)) && basename(entry.name, extname(entry.name)) === name) {
|
|
350
|
+
return join(root, entry.name);
|
|
351
|
+
}
|
|
352
|
+
if (entry.isDirectory() && entry.name === name && (existsSync(join(root, entry.name, "index.ts")) || existsSync(join(root, entry.name, "index.js")))) {
|
|
353
|
+
return join(root, entry.name);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch {}
|
|
357
|
+
}
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isWithin(path: string, root: string) {
|
|
362
|
+
const value = relative(root, path);
|
|
363
|
+
return value !== "" && !value.startsWith("../") && value !== ".." && !value.startsWith("..\\");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function applyProfileSkillAction(action: ParsedAction, resourcePath: string, settings: Record<string, unknown>, agentDir: string, resourceRoot: string, settingsPath: string): Promise<boolean> {
|
|
367
|
+
const profile = settings.profile;
|
|
368
|
+
if (!profile || typeof profile !== "object" || agentDir === resourceRoot) return false;
|
|
369
|
+
const sharedRoot = join(resourceRoot, "skills");
|
|
370
|
+
const profileRoot = join(agentDir, "skills");
|
|
371
|
+
const source = isWithin(resourcePath, sharedRoot) ? { root: sharedRoot, key: "enabledSkills" } : isWithin(resourcePath, profileRoot) ? { root: profileRoot, key: "enabledProfileSkills" } : undefined;
|
|
372
|
+
if (!source) return false;
|
|
373
|
+
|
|
374
|
+
const name = relative(source.root, resourcePath).replaceAll("\\", "/");
|
|
375
|
+
const profileSettings = profile as Record<string, unknown>;
|
|
376
|
+
const configuredValue = profileSettings[source.key];
|
|
377
|
+
const configured = Array.isArray(configuredValue)
|
|
378
|
+
? configuredValue.filter((value): value is string => typeof value === "string")
|
|
379
|
+
: undefined;
|
|
380
|
+
let updated: string[];
|
|
381
|
+
if (action.action === "enable") {
|
|
382
|
+
updated = !configured || configured.includes("*") ? (configured ?? ["*"]) : [...new Set([...configured, name])];
|
|
383
|
+
} else if (!configured || configured.includes("*")) {
|
|
384
|
+
const names = [...new Set((await findSkillFiles(source.root)).map((file) => relative(source.root, dirname(file)).replaceAll("\\", "/")))];
|
|
385
|
+
updated = names.filter((entry) => entry !== name);
|
|
386
|
+
} else {
|
|
387
|
+
updated = configured.filter((entry) => entry !== name);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (configured && configured.length === updated.length && configured.every((entry, index) => entry === updated[index])) {
|
|
391
|
+
console.log(`Skill "${name}" is already ${action.action}d for this profile.`);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
const updatedProfile = { ...(profile as Record<string, unknown>), [source.key]: updated };
|
|
395
|
+
await writeFile(settingsPath, `${JSON.stringify({ ...settings, profile: updatedProfile }, null, 2)}\n`, "utf8");
|
|
396
|
+
console.log(`Skill "${name}" ${action.action}d for this profile. Use /reload to apply.`);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function applyAction(action: ParsedAction) {
|
|
401
|
+
const loaded = await readSettings();
|
|
402
|
+
const resourceRoot = loaded.resourceRoot;
|
|
403
|
+
// An extension is runtime-wide. Tools and Skills remain profile-scoped.
|
|
404
|
+
const agentDir = action.kind === "extensions" ? resourceRoot : loaded.agentDir;
|
|
405
|
+
const settings = action.kind === "extensions" ? loaded.runtimeSettings : loaded.settings;
|
|
406
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
407
|
+
const enable = action.action === "enable";
|
|
408
|
+
|
|
409
|
+
if (action.kind === "tools") {
|
|
410
|
+
if (!(BUILTIN_TOOLS as readonly string[]).includes(action.target)) {
|
|
411
|
+
throw new Error(`\"${action.target}\" não é uma tool nativa. Desabilite a extension que fornece essa tool.`);
|
|
412
|
+
}
|
|
413
|
+
const configured = Array.isArray(settings.defaultTools)
|
|
414
|
+
? settings.defaultTools.filter((entry): entry is string => typeof entry === "string")
|
|
415
|
+
: [...BUILTIN_TOOLS];
|
|
416
|
+
const updatedTools = enable
|
|
417
|
+
? [...new Set([...configured, action.target])]
|
|
418
|
+
: configured.filter((tool) => tool !== action.target);
|
|
419
|
+
if (configured.length === updatedTools.length && configured.every((tool, index) => tool === updatedTools[index])) {
|
|
420
|
+
console.log(`Tool \"${action.target}\" já está ${enable ? "habilitada" : "desabilitada"}.`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const updatedSettings: Record<string, unknown> = { ...settings, defaultTools: updatedTools };
|
|
424
|
+
if (updatedTools.length === BUILTIN_TOOLS.length && BUILTIN_TOOLS.every((tool) => updatedTools.includes(tool))) {
|
|
425
|
+
delete updatedSettings.defaultTools;
|
|
426
|
+
}
|
|
427
|
+
await writeFile(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
|
|
428
|
+
console.log(`Tool \"${action.target}\" ${enable ? "habilitada" : "desabilitada"} em ${settingsPath}. Use /reload para aplicar.`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const resourcePath = await findResource(action.kind, action.target, settings, agentDir, resourceRoot);
|
|
433
|
+
if (!resourcePath) throw new Error(`${action.kind.slice(0, -1)} \"${action.target}\" não encontrada.`);
|
|
434
|
+
if (action.kind === "skills" && await applyProfileSkillAction(action, resourcePath, settings, agentDir, resourceRoot, settingsPath)) return;
|
|
435
|
+
const entries = Array.isArray(settings[action.kind])
|
|
436
|
+
? settings[action.kind].filter((entry): entry is string => typeof entry === "string")
|
|
437
|
+
: [];
|
|
438
|
+
const updatedEntries = setExclusion(entries, resourcePath, !enable);
|
|
439
|
+
if (entries.length === updatedEntries.length && entries.every((entry, index) => entry === updatedEntries[index])) {
|
|
440
|
+
console.log(`${action.kind.slice(0, -1)} \"${action.target}\" já está ${enable ? "habilitada" : "desabilitada"}.`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const updatedSettings: Record<string, unknown> = { ...settings, [action.kind]: updatedEntries };
|
|
444
|
+
if (updatedEntries.length === 0) delete updatedSettings[action.kind];
|
|
445
|
+
await writeFile(settingsPath, `${JSON.stringify(updatedSettings, null, 2)}\n`, "utf8");
|
|
446
|
+
console.log(`${action.kind.slice(0, -1)} \"${action.target}\" ${enable ? "habilitada" : "desabilitada"} em ${settingsPath}. Use /reload para aplicar.`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export default async function (pi: ExtensionAPI) {
|
|
450
|
+
pi.registerFlag("list-sessions", {
|
|
451
|
+
description: "List saved Pi sessions",
|
|
452
|
+
type: "boolean",
|
|
453
|
+
default: false,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const args = process.argv.slice(2);
|
|
457
|
+
// Remote commands must reach the profiles extension before this extension
|
|
458
|
+
// creates its local --no-session helper process. The remote Pi will process
|
|
459
|
+
// resource commands on its own host/runtime.
|
|
460
|
+
if (args[0] === "profile" || args.some((arg) => /^remote:[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(arg))) return;
|
|
461
|
+
const kind = kindFromArgs(args);
|
|
462
|
+
const action = actionFromArgs(args);
|
|
463
|
+
const packageRequested = packageRequestedFromArgs(args);
|
|
464
|
+
const packageAction = packageActionFromArgs(args);
|
|
465
|
+
const sessionRename = sessionRenameFromArgs(args);
|
|
466
|
+
const sessionRequested = args.some((arg) => arg === "--list-sessions" || arg === "--list-sessions=true")
|
|
467
|
+
|| args.some((arg, index) => arg === "sessions" && args[index + 1] === "list");
|
|
468
|
+
if (!kind && !action && !packageRequested && !packageAction && !sessionRequested && !sessionRename) return;
|
|
469
|
+
|
|
470
|
+
// Run CLI operations in an ephemeral child so they never create an empty session.
|
|
471
|
+
if (process.env.PI_CLI_RESOURCES !== "1" && !sessionRename) {
|
|
472
|
+
const profile = requestedProfile();
|
|
473
|
+
const root = process.env.PI_PROFILE_ROOT ?? process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent");
|
|
474
|
+
const target = profile ? join(root, "profiles", profile) : process.env.PI_CODING_AGENT_DIR;
|
|
475
|
+
const child = spawn(process.execPath, [process.argv[1], "--no-session", ...args], {
|
|
476
|
+
stdio: "inherit",
|
|
477
|
+
env: { ...process.env, PI_CLI_RESOURCES: "1", PI_CODING_AGENT_DIR: target, PI_PROFILE_ROOT: root, ...(profile ? { PI_ACTIVE_PROFILE: profile } : {}) },
|
|
478
|
+
});
|
|
479
|
+
const code = await new Promise<number>((resolveExit, reject) => {
|
|
480
|
+
child.once("error", reject);
|
|
481
|
+
child.once("exit", (exitCode) => resolveExit(exitCode ?? 1));
|
|
482
|
+
});
|
|
483
|
+
process.exit(code);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (sessionRename) {
|
|
487
|
+
try {
|
|
488
|
+
if (!sessionRename.name) throw new Error("Session name cannot be empty.");
|
|
489
|
+
const profile = process.env.PI_ACTIVE_PROFILE ?? requestedProfile();
|
|
490
|
+
const root = process.env.PI_PROFILE_ROOT ?? process.env.PI_CODING_AGENT_DIR ?? join(process.env.HOME ?? "", ".pi", "agent");
|
|
491
|
+
const sessionDir = profile && profile !== "default" ? join(root, "profiles", profile, "sessions") : process.env.PI_CODING_AGENT_SESSION_DIR;
|
|
492
|
+
const sessions = sessionDir ? await SessionManager.listAll(sessionDir) : await SessionManager.listAll();
|
|
493
|
+
const existing = sessions.find((session) => session.id === sessionRename.id);
|
|
494
|
+
if (!existing) throw new Error(`Session '${sessionRename.id}' not found.`);
|
|
495
|
+
SessionManager.open(existing.path, sessionDir, process.cwd()).appendSessionInfo(sessionRename.name);
|
|
496
|
+
console.log(`Session '${sessionRename.id}' renamed to '${sessionRename.name}'.`);
|
|
497
|
+
process.exit(0);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (packageAction) {
|
|
505
|
+
try {
|
|
506
|
+
await applyPackageAction(packageAction);
|
|
507
|
+
process.exit(0);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (action) {
|
|
515
|
+
try {
|
|
516
|
+
await applyAction(action);
|
|
517
|
+
process.exit(0);
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
pi.on("session_start", async () => {
|
|
525
|
+
if (sessionRequested) {
|
|
526
|
+
const sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR;
|
|
527
|
+
const sessions = sessionDir ? await SessionManager.listAll(sessionDir) : await SessionManager.listAll();
|
|
528
|
+
await renderAndClose(React.createElement(SessionTable, { sessions }));
|
|
529
|
+
} else if (packageRequested) {
|
|
530
|
+
const { resourceRoot, runtimeSettings } = await readSettings();
|
|
531
|
+
await renderAndClose(React.createElement(ResourceTable, { title: "PACKAGES", headers: ["NAME", "STATUS", "VERSION / DESCRIPTION"], rows: await packageRows(resourceRoot, runtimeSettings), highlightStatus: true }));
|
|
532
|
+
} else if (kind === "tools") {
|
|
533
|
+
const active = new Set(pi.getActiveTools());
|
|
534
|
+
const { resourceRoot, runtimeSettings } = await readSettings();
|
|
535
|
+
const sources = await packageToolSources(resourceRoot, runtimeSettings, pi.getAllTools().map((tool) => tool.name));
|
|
536
|
+
const rows: SourceRow[] = pi.getAllTools()
|
|
537
|
+
.map((tool) => [tool.name, active.has(tool.name) ? "enabled" : "disabled", sources.has(tool.name) ? `npm: ${sources.get(tool.name)}` : (BUILTIN_TOOLS as readonly string[]).includes(tool.name) ? "Built-in" : "Extension", tool.description] as SourceRow)
|
|
538
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
539
|
+
await renderAndClose(React.createElement(SourceTable, { title: "TOOLS", detailHeader: "DESCRIPTION", rows }));
|
|
540
|
+
} else {
|
|
541
|
+
const loaded = await readSettings();
|
|
542
|
+
const resourceRoot = loaded.resourceRoot;
|
|
543
|
+
const agentDir = kind === "extensions" ? resourceRoot : loaded.agentDir;
|
|
544
|
+
const settings = kind === "extensions" ? loaded.runtimeSettings : loaded.settings;
|
|
545
|
+
const rawEntries = Array.isArray(settings[kind]) ? settings[kind].filter((value): value is string => typeof value === "string") : [];
|
|
546
|
+
const configuredPaths = rawEntries.filter((value) => !/^[!+\-]/.test(value));
|
|
547
|
+
const exclusions = rawEntries.filter((value) => value.startsWith("!"));
|
|
548
|
+
const catalogRoot = join(resourceRoot, kind);
|
|
549
|
+
const packageExtensions = await configuredPackageExtensions(resourceRoot, kind === "extensions" ? loaded.runtimeSettings : settings);
|
|
550
|
+
const packagePaths = new Map(packageExtensions.map(({ path, packageName }) => [path, packageName]));
|
|
551
|
+
const paths = kind === "skills"
|
|
552
|
+
? [...new Set([catalogRoot, ...configuredPaths])]
|
|
553
|
+
: [...new Set([...(configuredPaths.length > 0 ? configuredPaths : [catalogRoot]), ...packageExtensions.map(({ path }) => path)])];
|
|
554
|
+
const rows: SourceRow[] = (kind === "skills" ? await skillRows(paths, catalogRoot, settings, exclusions) : await extensionRows(paths, exclusions)).map((row) => {
|
|
555
|
+
const packageName = packagePaths.get(row[2]);
|
|
556
|
+
const source = packageName ? `Package: ${packageName}` : (row[2].startsWith(join(resourceRoot, "skills")) ? "Shared" : row[2].includes("/profiles/") ? "Profile" : "Local");
|
|
557
|
+
return [row[0], row[1], source, row[2]];
|
|
558
|
+
});
|
|
559
|
+
await renderAndClose(React.createElement(SourceTable, { title: kind.toUpperCase(), detailHeader: "PATH", rows }));
|
|
560
|
+
}
|
|
561
|
+
// This command always runs in an ephemeral child process.
|
|
562
|
+
process.exit(0);
|
|
563
|
+
});
|
|
564
|
+
}
|