pi-profile-switch 0.9.1 → 0.10.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 +14 -7
- package/README.zh-CN.md +13 -6
- package/bin/pi-profile.ts +9 -1
- package/bin/postinstall.js +2 -2
- package/extensions/pi-profile/index.ts +31 -188
- package/package.json +1 -1
- package/skills/profile-config/SKILL.md +104 -104
- package/src/launcher/initial-profile.ts +12 -6
- package/src/launcher/untrusted-project-diagnostic.ts +84 -0
- package/src/profile-catalog.ts +2 -2
- package/src/profile-resolver.ts +114 -27
- package/src/project-trust.ts +10 -7
- package/src/runtime-state-store.ts +3 -4
- package/src/settings-generator.ts +1 -0
- package/src/starter-assets.ts +6 -6
- package/src/switching/apply-plan.ts +36 -8
- package/src/switching/list-profiles.ts +2 -2
- package/src/switching/{customize.ts → overlay.ts} +67 -35
- package/src/switching/status.ts +1 -1
- package/src/switching/switch-profile.ts +7 -3
- package/src/profile-catalog-store.ts +0 -86
- package/src/switching/profile-crud.ts +0 -136
- package/src/switching/profile-wizard.ts +0 -163
package/README.md
CHANGED
|
@@ -107,21 +107,28 @@ If you used an earlier version that stored all profiles in a single `profiles.js
|
|
|
107
107
|
|
|
108
108
|
## Commands
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
The `/profile` command family manages everything in-session:
|
|
111
111
|
|
|
112
112
|
| Command | What it does |
|
|
113
113
|
| --- | --- |
|
|
114
|
-
| `/profile` | Interactive profile picker |
|
|
115
|
-
| `/profile list` / `/profile status` | Show profiles / active profile details |
|
|
114
|
+
| `/profile` | Interactive profile picker; without interactive UI it prints the profile list instead |
|
|
116
115
|
| `/profile use <name>` / `/profile reload` | Switch / reload without restarting (rollback on failure) |
|
|
117
|
-
| `/profile
|
|
118
|
-
| `/profile
|
|
116
|
+
| `/profile status` | Active profile details: resolved resources and paths, stored overlay, MCP server tri-state |
|
|
117
|
+
| `/profile overlay disable\|enable skill\|extension\|mcp\|tool <name-or-glob>` | Narrow / un-narrow the active profile for this session only; `disable` entries accept names or globs |
|
|
118
|
+
| `/profile overlay clear` | Discard the overlay and reactivate the profile exactly as declared |
|
|
119
119
|
|
|
120
|
-
All
|
|
120
|
+
All forms work in every mode, including non-interactive ones (`--mode rpc|print|json`); the bare selector degrades to the profile list where no interactive UI exists. The overlay is a runtime-only narrowing: it is never written to a catalog file and never survives a restart. Tools follow the same disable/enable model as the other resource kinds: a tool `disable` entry narrows the profile's resolved tool references — or the runtime's full available tool set when the profile declares no `tools`.
|
|
121
|
+
|
|
122
|
+
Removed subcommands and their replacements:
|
|
123
|
+
|
|
124
|
+
| Removed | Replacement |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `/profile create` / `/profile edit` / `/profile delete` / `/profile duplicate` | edit the catalog JSON files directly, or configure profiles conversationally with the [`profile-config`](skills/profile-config/SKILL.md) skill |
|
|
127
|
+
| the `tools` replace-form of `/profile overlay` | `/profile overlay disable tool <name-or-glob>` (disable the complement, with globs) or declare the fixed set in the profile's `tools` field |
|
|
121
128
|
|
|
122
129
|
## Docs
|
|
123
130
|
|
|
124
|
-
- [Architecture](docs/architecture/overview.md) · [ADRs](docs/adr/) · [Glossary](CONTEXT.md)
|
|
131
|
+
- [Architecture](docs/architecture/overview.md) · [ADRs](docs/adr/) · [Glossary](CONTEXT.md)
|
|
125
132
|
- JSON Schemas: [`schemas/`](schemas/)
|
|
126
133
|
|
|
127
134
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -107,17 +107,24 @@ pi-profile-switch 会向全局 `profiles/` 目录播种一个初始 **`ask`** pr
|
|
|
107
107
|
|
|
108
108
|
## 命令
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
`/profile` 命令族完成所有会话内操作:
|
|
111
111
|
|
|
112
112
|
| 命令 | 作用 |
|
|
113
113
|
| --- | --- |
|
|
114
|
-
| `/profile` | 交互式选择 profile |
|
|
115
|
-
| `/profile list` / `/profile status` | 列出 profile / 查看活动 profile 详情 |
|
|
114
|
+
| `/profile` | 交互式选择 profile;无交互界面时打印 profile 列表 |
|
|
116
115
|
| `/profile use <name>` / `/profile reload` | 会话内切换 / 重载(失败自动回滚) |
|
|
117
|
-
| `/profile
|
|
118
|
-
| `/profile
|
|
116
|
+
| `/profile status` | 查看活动 profile 详情:解析后的资源与路径、已存 overlay、MCP server 三态 |
|
|
117
|
+
| `/profile overlay disable\|enable skill\|extension\|mcp\|tool <name-or-glob>` | 仅本次会话收窄 / 恢复活动 profile;`disable` 接受名称或 glob |
|
|
118
|
+
| `/profile overlay clear` | 丢弃 overlay,完全按定义重新激活 profile |
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
所有命令在非交互模式(`--mode rpc|print|json`)下同样生效;裸 `/profile` 在无交互界面时降级为 profile 列表。overlay 是会话级收窄:绝不写入 catalog 文件,也不会跨重启保留。tool 与其他资源类别使用相同的 disable/enable 模型:tool `disable` 条目收窄 profile 解析出的工具引用;profile 未声明 `tools` 时,则收窄运行时的全部可用工具集。
|
|
121
|
+
|
|
122
|
+
已移除子命令及其替代:
|
|
123
|
+
|
|
124
|
+
| 移除 | 替代 |
|
|
125
|
+
| --- | --- |
|
|
126
|
+
| `/profile create` / `/profile edit` / `/profile delete` / `/profile duplicate` | 直接编辑 catalog JSON 文件,或通过 [`profile-config`](skills/profile-config/SKILL.md) skill 对话式配置 |
|
|
127
|
+
| `/profile overlay` 的 `tools` 替换式 | `/profile overlay disable tool <name-or-glob>`(用 glob 禁用其余工具),或在 profile 的 `tools` 字段中声明固定集合 |
|
|
121
128
|
|
|
122
129
|
## 文档
|
|
123
130
|
|
package/bin/pi-profile.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
16
16
|
import { ExtensionError } from "../src/extension-discovery.ts";
|
|
17
17
|
import { parseLauncherArgs } from "../src/launcher/args.ts";
|
|
18
18
|
import { UnknownProfileError, resolveInitialProfile } from "../src/launcher/initial-profile.ts";
|
|
19
|
+
import { untrustedProjectDiagnostic } from "../src/launcher/untrusted-project-diagnostic.ts";
|
|
19
20
|
import { sweepStaleInstances } from "../src/launcher/runtime-cleanup.ts";
|
|
20
21
|
import { spawnPi } from "../src/launcher/spawn.ts";
|
|
21
22
|
import { McpConfigError, MissingMcpAdapterError } from "../src/mcp-config.ts";
|
|
@@ -37,7 +38,7 @@ try {
|
|
|
37
38
|
}
|
|
38
39
|
// Fails before spawning when the profile is unknown or cannot activate.
|
|
39
40
|
// --approve/--no-approve are consumed here as a one-run trust input.
|
|
40
|
-
const { plan, discovery, projectDir, warnings } = await resolveInitialProfile(args.profile, {
|
|
41
|
+
const { plan, discovery, projectDir, projectTrusted, warnings } = await resolveInitialProfile(args.profile, {
|
|
41
42
|
agentDir,
|
|
42
43
|
cwd: process.cwd(),
|
|
43
44
|
trustOverride: args.trustOverride,
|
|
@@ -45,6 +46,13 @@ try {
|
|
|
45
46
|
for (const warning of warnings) {
|
|
46
47
|
console.error(`pi-profile: warning: ${warning}`);
|
|
47
48
|
}
|
|
49
|
+
// Untrusted-project notice: fires identically for every profile (default
|
|
50
|
+
// included) because it is driven by the trust determination, not by which
|
|
51
|
+
// profile branch resolution took. Startup continues either way.
|
|
52
|
+
const untrustedNotice = untrustedProjectDiagnostic(process.cwd(), projectTrusted);
|
|
53
|
+
if (untrustedNotice) {
|
|
54
|
+
console.error(`pi-profile: warning: ${untrustedNotice}`);
|
|
55
|
+
}
|
|
48
56
|
// Stale per-launch instance dirs (dead pid, or no pid past the grace
|
|
49
57
|
// window) are swept before this launch materializes its own. Unrecognized
|
|
50
58
|
// entries are dispositioned per content scan: kept + reported when they
|
package/bin/postinstall.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* launcher instead: `bin/pi-profile.ts` calls the runtime ensure
|
|
13
13
|
* (src/starter-assets.ts) on every launch, before resolving the initial
|
|
14
14
|
* profile. The authoritative behavior contract for both assets lives in
|
|
15
|
-
* openspec/specs/profile-catalog/spec.md ("
|
|
16
|
-
* "
|
|
15
|
+
* openspec/specs/profile-catalog/spec.md ("Seeding the starter profile" and
|
|
16
|
+
* "Distributing the profile-config skill"); keep this script's rules in sync with
|
|
17
17
|
* src/starter-assets.ts, which is the single TS implementation.
|
|
18
18
|
*
|
|
19
19
|
* Idempotent and conservative:
|
|
@@ -6,21 +6,8 @@ import { readTrustInputs } from "../../src/launcher/initial-profile.ts";
|
|
|
6
6
|
import { discoverAdapterServerNames } from "../../src/mcp-config.ts";
|
|
7
7
|
import { RuntimeStateStore } from "../../src/runtime-state-store.ts";
|
|
8
8
|
import { applyLaunchPlan, readLaunchPlanFile } from "../../src/switching/apply-plan.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { OVERLAY_USAGE, applyOverlayMutation, clearOverlay, parseOverlayArgs } from "../../src/switching/overlay.ts";
|
|
10
10
|
import { formatProfileList, listProfiles } from "../../src/switching/list-profiles.ts";
|
|
11
|
-
import {
|
|
12
|
-
createProfile,
|
|
13
|
-
deleteProfile,
|
|
14
|
-
duplicateProfile,
|
|
15
|
-
editProfile,
|
|
16
|
-
readCatalogScope,
|
|
17
|
-
} from "../../src/switching/profile-crud.ts";
|
|
18
|
-
import type { ProfileDefinition } from "../../src/profile-catalog.ts";
|
|
19
|
-
import {
|
|
20
|
-
runProfileCreateWizard,
|
|
21
|
-
runProfileDuplicateWizard,
|
|
22
|
-
runProfileEditWizard,
|
|
23
|
-
} from "../../src/switching/profile-wizard.ts";
|
|
24
11
|
import { buildStatusReport, formatStatusMarkdown } from "../../src/switching/status.ts";
|
|
25
12
|
import { switchProfile, type SwitchDeps } from "../../src/switching/switch-profile.ts";
|
|
26
13
|
import { getGlobalStateDir } from "../../src/workspace.ts";
|
|
@@ -37,18 +24,15 @@ import { getGlobalStateDir } from "../../src/workspace.ts";
|
|
|
37
24
|
* via `before_agent_start`.
|
|
38
25
|
* - `/profile use <name>` / `/profile reload`: in-session switching without
|
|
39
26
|
* restarting the Pi process (src/switching/switch-profile.ts).
|
|
40
|
-
* - `/profile
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* is stale afterwards.
|
|
50
|
-
* - list/status ship structured `details` payloads (`{kind, profiles}` /
|
|
51
|
-
* `{kind, report}`) for RPC consumers (ticket 11).
|
|
27
|
+
* - `/profile overlay ...`: runtime overlay (ticket 06; renamed with the
|
|
28
|
+
* command word — disable/enable/tools/clear).
|
|
29
|
+
* - `/profile` (selector; degrades to the list without dialog-capable UI)
|
|
30
|
+
* and `/profile status`: observability surface (ticket 07). Status
|
|
31
|
+
* combines the active launch plan, the stored overlay, fresh MCP
|
|
32
|
+
* discovery, and Pi's actual command registrations (the winner evidence
|
|
33
|
+
* for same-name conflicts).
|
|
34
|
+
* - The degraded list and status ship structured `details` payloads
|
|
35
|
+
* (`{kind, profiles}` / `{kind, report}`) for RPC consumers (ticket 11).
|
|
52
36
|
*
|
|
53
37
|
* Pi re-executes this module on reload, so post-reload state is established
|
|
54
38
|
* exclusively through `session_start` — nothing stale survives.
|
|
@@ -92,7 +76,7 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
92
76
|
});
|
|
93
77
|
|
|
94
78
|
pi.registerCommand("profile", {
|
|
95
|
-
description: "pi-profile: /profile [use|reload|
|
|
79
|
+
description: "pi-profile: /profile [use|reload|status|overlay]",
|
|
96
80
|
handler: async (args, ctx) => {
|
|
97
81
|
const [subcommandRaw, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
98
82
|
const subcommand = subcommandRaw ?? ""; // bare /profile → selector
|
|
@@ -107,35 +91,15 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
107
91
|
// stale context after reload — see above
|
|
108
92
|
}
|
|
109
93
|
};
|
|
110
|
-
const usage = `usage: /profile [use <name> | reload |
|
|
94
|
+
const usage = `usage: /profile [use <name> | reload | status | ${OVERLAY_USAGE}]`;
|
|
111
95
|
if (subcommand === "use" && rest.length === 0) {
|
|
112
96
|
notify("usage: /profile use <name>", "error");
|
|
113
97
|
return;
|
|
114
98
|
}
|
|
115
|
-
if (
|
|
116
|
-
subcommand !== "" &&
|
|
117
|
-
![
|
|
118
|
-
"use",
|
|
119
|
-
"reload",
|
|
120
|
-
"customize",
|
|
121
|
-
"reset",
|
|
122
|
-
"list",
|
|
123
|
-
"status",
|
|
124
|
-
"create",
|
|
125
|
-
"edit",
|
|
126
|
-
"delete",
|
|
127
|
-
"duplicate",
|
|
128
|
-
].includes(
|
|
129
|
-
subcommand,
|
|
130
|
-
)
|
|
131
|
-
) {
|
|
99
|
+
if (subcommand !== "" && !["use", "reload", "status", "overlay"].includes(subcommand)) {
|
|
132
100
|
notify(usage, "error");
|
|
133
101
|
return;
|
|
134
102
|
}
|
|
135
|
-
if (["edit", "delete"].includes(subcommand) && rest[0] === undefined) {
|
|
136
|
-
notify(`usage: /profile ${subcommand} <name>`, "error");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
103
|
try {
|
|
140
104
|
const plan = await readLaunchPlanFile(runtimeDir);
|
|
141
105
|
if (plan?.agentDir === undefined) {
|
|
@@ -148,6 +112,7 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
148
112
|
runtimeDir,
|
|
149
113
|
realAgentDir: plan.agentDir,
|
|
150
114
|
cwd: ctx.cwd,
|
|
115
|
+
getAllTools: () => pi.getAllTools(),
|
|
151
116
|
waitForIdle: () => ctx.waitForIdle(),
|
|
152
117
|
reload: () => ctx.reload(),
|
|
153
118
|
// A real reload invalidates this context (Pi re-executes
|
|
@@ -172,148 +137,22 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
172
137
|
notify(`profile reloaded: ${result.profile}`, "info");
|
|
173
138
|
return;
|
|
174
139
|
}
|
|
175
|
-
if (subcommand === "
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (subcommand === "reset") {
|
|
182
|
-
const result = await resetOverlay(deps);
|
|
183
|
-
for (const warning of result.warnings) notify(warning, "warning");
|
|
184
|
-
notify(`overlay cleared: ${result.profile}`, "info");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
// Profile catalog CRUD (ticket 09): TUI-only wizards; mutations
|
|
188
|
-
// land in the chosen scope file. Editing the active profile
|
|
189
|
-
// reloads immediately; deleting the active profile requires a
|
|
190
|
-
// replacement chosen up front, then switches to it.
|
|
191
|
-
if (["create", "edit", "delete", "duplicate"].includes(subcommand)) {
|
|
192
|
-
if (ctx.mode !== "tui") {
|
|
193
|
-
notify(`/profile ${subcommand} requires TUI mode (current mode: ${ctx.mode})`, "error");
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const scopeInput = { realAgentDir: plan.agentDir, cwd: ctx.cwd };
|
|
197
|
-
if (subcommand === "create") {
|
|
198
|
-
const { projectTrusted: canWriteProject } = await readTrustInputs({
|
|
199
|
-
agentDir: plan.agentDir,
|
|
200
|
-
cwd: ctx.cwd,
|
|
201
|
-
});
|
|
202
|
-
const wizard = await runProfileCreateWizard(ctx.ui, { projectTrusted: canWriteProject });
|
|
203
|
-
if (wizard === undefined) return;
|
|
204
|
-
await createProfile(scopeInput, wizard.scope, wizard.name, wizard.definition);
|
|
205
|
-
notify(`created profile "${wizard.name}" (${wizard.scope}) — activate with /profile use ${wizard.name}`, "info");
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (subcommand === "duplicate") {
|
|
209
|
-
const entries = await listProfiles(scopeInput);
|
|
210
|
-
// Read each scope once (trust-gated — never reads an
|
|
211
|
-
// untrusted project's catalog).
|
|
212
|
-
const byScope = {
|
|
213
|
-
global: await readCatalogScope(scopeInput, "global"),
|
|
214
|
-
project: await readCatalogScope(scopeInput, "project"),
|
|
215
|
-
};
|
|
216
|
-
const candidates: Array<{ name: string; source: "global" | "project"; definition: ProfileDefinition }> = [];
|
|
217
|
-
for (const entry of entries) {
|
|
218
|
-
if (entry.source === "builtin") continue;
|
|
219
|
-
const definition = byScope[entry.source].get(entry.name);
|
|
220
|
-
if (definition !== undefined) {
|
|
221
|
-
candidates.push({ name: entry.name, source: entry.source, definition });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
const wizard = await runProfileDuplicateWizard(ctx.ui, { candidates });
|
|
225
|
-
if (wizard === undefined) return;
|
|
226
|
-
await duplicateProfile(scopeInput, wizard.scope, wizard.sourceName, wizard.newName);
|
|
227
|
-
notify(
|
|
228
|
-
`duplicated "${wizard.sourceName}" → "${wizard.newName}" (${wizard.scope}) — the full definition was copied`,
|
|
229
|
-
"info",
|
|
230
|
-
);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const name = rest[0] as string;
|
|
234
|
-
if (subcommand === "edit") {
|
|
235
|
-
// Edit the WINNING definition in its source scope.
|
|
236
|
-
const entries = await listProfiles(scopeInput);
|
|
237
|
-
const existing = entries.find((entry) => entry.name === name);
|
|
238
|
-
if (existing === undefined || existing.source === "builtin") {
|
|
239
|
-
notify(`profile "${name}" not found in a writable catalog`, "error");
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const definition = (await readCatalogScope(scopeInput, existing.source)).get(name);
|
|
243
|
-
if (definition === undefined) {
|
|
244
|
-
notify(`profile "${name}" not found in the ${existing.source} catalog`, "error");
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
const wizard = await runProfileEditWizard(ctx.ui, {
|
|
248
|
-
existing: { name, source: existing.source, definition },
|
|
249
|
-
});
|
|
250
|
-
if (wizard === undefined) return;
|
|
251
|
-
await editProfile(scopeInput, wizard.scope, wizard.name, wizard.definition);
|
|
252
|
-
if (name === plan.profile) {
|
|
253
|
-
notify(`saved profile "${name}"; reloading`, "info");
|
|
254
|
-
await switchProfile(plan.profile, deps, { reloadCurrent: true });
|
|
255
|
-
} else {
|
|
256
|
-
notify(`saved profile "${name}" (inactive — runtime untouched)`, "info");
|
|
257
|
-
}
|
|
140
|
+
if (subcommand === "overlay") {
|
|
141
|
+
const command = parseOverlayArgs(rest.join(" "));
|
|
142
|
+
if (command.kind === "clear") {
|
|
143
|
+
const result = await clearOverlay(deps);
|
|
144
|
+
for (const warning of result.warnings) notify(warning, "warning");
|
|
145
|
+
notify(`overlay cleared: ${result.profile}`, "info");
|
|
258
146
|
return;
|
|
259
147
|
}
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
if (existing === undefined || existing.source === "builtin") {
|
|
264
|
-
notify(`profile "${name}" not found in a writable catalog`, "error");
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
// Both scopes hold the name: choose which record to delete.
|
|
268
|
-
let scope = existing.source as "global" | "project";
|
|
269
|
-
const projectCatalog = await readCatalogScope(scopeInput, "project");
|
|
270
|
-
const globalCatalog = await readCatalogScope(scopeInput, "global");
|
|
271
|
-
if (projectCatalog.has(name) && globalCatalog.has(name)) {
|
|
272
|
-
const chosen = await ctx.ui.select(`delete "${name}" from which catalog?`, ["global", "project"]);
|
|
273
|
-
if (chosen === undefined) return;
|
|
274
|
-
scope = chosen as "global" | "project";
|
|
275
|
-
}
|
|
276
|
-
// If the OTHER scope keeps the name alive, deletion reveals
|
|
277
|
-
// it and the session can stay on the revealed same-name
|
|
278
|
-
// definition (symmetric: project→global and global→project).
|
|
279
|
-
const survivesElsewhere =
|
|
280
|
-
(scope === "project" && globalCatalog.has(name)) || (scope === "global" && projectCatalog.has(name));
|
|
281
|
-
let replacement: string | undefined;
|
|
282
|
-
if (name === plan.profile && !survivesElsewhere) {
|
|
283
|
-
const survivors = entries.filter((entry) => entry.name !== name);
|
|
284
|
-
const chosen = await ctx.ui.select(
|
|
285
|
-
`"${name}" is active — switch to which profile?`,
|
|
286
|
-
survivors.map((entry) => `${entry.name} [${entry.source}]`),
|
|
287
|
-
);
|
|
288
|
-
if (chosen === undefined) return;
|
|
289
|
-
replacement = chosen.split(" [")[0];
|
|
290
|
-
}
|
|
291
|
-
await deleteProfile(scopeInput, scope, name, { activeProfile: plan.profile, replacement });
|
|
292
|
-
if (name === plan.profile) {
|
|
293
|
-
if (replacement !== undefined) {
|
|
294
|
-
notify(`deleted "${name}" (${scope}); switching to ${replacement}`, "info");
|
|
295
|
-
await switchProfile(replacement, deps, { clearOverlay: true });
|
|
296
|
-
} else {
|
|
297
|
-
notify(`deleted the ${scope} record of "${name}"; reloading the revealed definition`, "info");
|
|
298
|
-
await switchProfile(plan.profile, deps, { reloadCurrent: true });
|
|
299
|
-
}
|
|
300
|
-
} else {
|
|
301
|
-
notify(`deleted profile "${name}" (${scope})`, "info");
|
|
302
|
-
}
|
|
148
|
+
const result = await applyOverlayMutation(deps, command.mutate);
|
|
149
|
+
for (const warning of result.warnings) notify(warning, "warning");
|
|
150
|
+
notify(`overlay updated: ${result.profile}`, "info");
|
|
303
151
|
return;
|
|
304
152
|
}
|
|
305
153
|
// Observability surface (ticket 07): bare /profile opens the
|
|
306
|
-
// selector;
|
|
154
|
+
// selector; status renders via a displayed custom message.
|
|
307
155
|
const entries = await listProfiles({ realAgentDir: plan.agentDir, cwd: ctx.cwd });
|
|
308
|
-
if (subcommand === "list") {
|
|
309
|
-
pi.sendMessage({
|
|
310
|
-
customType: "pi-profile",
|
|
311
|
-
content: formatProfileList(entries, plan.profile),
|
|
312
|
-
display: true,
|
|
313
|
-
details: { kind: "list", profiles: entries },
|
|
314
|
-
});
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
156
|
if (subcommand === "status") {
|
|
318
157
|
const { projectTrusted } = await readTrustInputs({ agentDir: plan.agentDir, cwd: ctx.cwd });
|
|
319
158
|
const stateDir = plan.source === "project" ? path.join(ctx.cwd, ".pi") : getGlobalStateDir(plan.agentDir);
|
|
@@ -338,13 +177,17 @@ export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
|
338
177
|
});
|
|
339
178
|
return;
|
|
340
179
|
}
|
|
341
|
-
// Bare /profile: the interactive selector.
|
|
342
|
-
//
|
|
343
|
-
|
|
180
|
+
// Bare /profile: the interactive selector. Degradation boundary
|
|
181
|
+
// (delta "Observability surface"): outside TUI mode the selector
|
|
182
|
+
// does not run — the bare invocation degrades to the trust-gated
|
|
183
|
+
// list, carrying the structured payload the removed `list`
|
|
184
|
+
// subcommand emitted.
|
|
185
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
344
186
|
pi.sendMessage({
|
|
345
187
|
customType: "pi-profile",
|
|
346
188
|
content: formatProfileList(entries, plan.profile),
|
|
347
189
|
display: true,
|
|
190
|
+
details: { kind: "list", profiles: entries },
|
|
348
191
|
});
|
|
349
192
|
return;
|
|
350
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-profile-switch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Named profiles for Pi: reference skills, extensions, MCP servers, and tools per workflow, switched without restarting. Install: npm install -g pi-profile-switch (not pi install).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|