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
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ProfileWizard: the interactive create/edit/duplicate dialog for
|
|
3
|
-
* `/profile create|edit|delete|duplicate` (ticket 09), UI-injected for
|
|
4
|
-
* testability.
|
|
5
|
-
*
|
|
6
|
-
* Definitions are complete and self-contained — the wizard edits whole
|
|
7
|
-
* fields, never inheritance or merge syntax. On edit, an empty answer
|
|
8
|
-
* keeps the current value (prefill via placeholder); there is no
|
|
9
|
-
* field-clearing gesture (delete + create instead). Any cancelled step
|
|
10
|
-
* aborts the wizard — nothing is written.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ProfileDefinition } from "../profile-catalog.ts";
|
|
14
|
-
import type { CatalogScope } from "./profile-crud.ts";
|
|
15
|
-
|
|
16
|
-
export interface ProfileWizardUi {
|
|
17
|
-
select(title: string, options: string[]): Promise<string | undefined>;
|
|
18
|
-
input(title: string, placeholder?: string): Promise<string | undefined>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ProfileWizardResult {
|
|
22
|
-
scope: CatalogScope;
|
|
23
|
-
name: string;
|
|
24
|
-
definition: ProfileDefinition;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface ExistingProfile {
|
|
28
|
-
name: string;
|
|
29
|
-
source: CatalogScope;
|
|
30
|
-
definition: ProfileDefinition;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseList(raw: string): string[] {
|
|
34
|
-
return raw
|
|
35
|
-
.split(",")
|
|
36
|
-
.map((entry) => entry.trim())
|
|
37
|
-
.filter((entry) => entry.length > 0);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** "provider/id[/thinkingLevel]" → ProfileModel; empty/undefined → none. */
|
|
41
|
-
function parseModel(raw: string): { defaultProvider: string; defaultModel: string; defaultThinkingLevel?: string } | undefined {
|
|
42
|
-
const trimmed = raw.trim();
|
|
43
|
-
if (trimmed.length === 0) return undefined;
|
|
44
|
-
const [provider, id, thinkingLevel] = trimmed.split("/").map((part) => part.trim());
|
|
45
|
-
if (provider === undefined || provider.length === 0 || id === undefined || id.length === 0) {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
return thinkingLevel !== undefined && thinkingLevel.length > 0
|
|
49
|
-
? { defaultProvider: provider, defaultModel: id, defaultThinkingLevel: thinkingLevel }
|
|
50
|
-
: { defaultProvider: provider, defaultModel: id };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function captureDefinition(
|
|
54
|
-
ui: ProfileWizardUi,
|
|
55
|
-
existing?: ProfileDefinition,
|
|
56
|
-
): Promise<ProfileDefinition | undefined> {
|
|
57
|
-
const definition: ProfileDefinition = {};
|
|
58
|
-
|
|
59
|
-
const label = await ui.input("label (optional; empty keeps current)", existing?.label);
|
|
60
|
-
if (label === undefined) return undefined;
|
|
61
|
-
if (label.trim().length > 0) definition.label = label.trim();
|
|
62
|
-
else if (existing?.label !== undefined) definition.label = existing.label;
|
|
63
|
-
|
|
64
|
-
const description = await ui.input("description (optional; empty keeps current)", existing?.description);
|
|
65
|
-
if (description === undefined) return undefined;
|
|
66
|
-
if (description.trim().length > 0) definition.description = description.trim();
|
|
67
|
-
else if (existing?.description !== undefined) definition.description = existing.description;
|
|
68
|
-
|
|
69
|
-
const listFields = [
|
|
70
|
-
["skills", "skills (comma-separated names or globs, empty = none)"],
|
|
71
|
-
["extensions", "extensions (names or globs, empty = none)"],
|
|
72
|
-
["mcps", "mcp servers (names or globs, empty = none)"],
|
|
73
|
-
["tools", "tools (names or globs, empty = pi default set)"],
|
|
74
|
-
] as const;
|
|
75
|
-
for (const [field, prompt] of listFields) {
|
|
76
|
-
const current = existing?.[field]?.join(", ");
|
|
77
|
-
const raw = await ui.input(prompt, current);
|
|
78
|
-
if (raw === undefined) return undefined;
|
|
79
|
-
const parsed = parseList(raw);
|
|
80
|
-
// Empty keeps the existing value on edit; on create it omits the field.
|
|
81
|
-
if (parsed.length > 0) {
|
|
82
|
-
definition[field] = parsed;
|
|
83
|
-
} else if (current !== undefined) {
|
|
84
|
-
definition[field] = existing?.[field];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const instructions = await ui.input(
|
|
89
|
-
"instructions (appended to the system prompt; empty keeps current)",
|
|
90
|
-
existing?.instructions,
|
|
91
|
-
);
|
|
92
|
-
if (instructions === undefined) return undefined;
|
|
93
|
-
if (instructions.trim().length > 0) definition.instructions = instructions;
|
|
94
|
-
else if (existing?.instructions !== undefined) definition.instructions = existing.instructions;
|
|
95
|
-
|
|
96
|
-
const currentModel =
|
|
97
|
-
existing?.defaultProvider !== undefined && existing?.defaultModel !== undefined
|
|
98
|
-
? `${existing.defaultProvider}/${existing.defaultModel}${existing.defaultThinkingLevel !== undefined ? `/${existing.defaultThinkingLevel}` : ""}`
|
|
99
|
-
: undefined;
|
|
100
|
-
const modelRaw = await ui.input("model provider/id[/thinking] (empty = none)", currentModel);
|
|
101
|
-
if (modelRaw === undefined) return undefined;
|
|
102
|
-
const model = parseModel(modelRaw);
|
|
103
|
-
if (model !== undefined) {
|
|
104
|
-
definition.defaultProvider = model.defaultProvider;
|
|
105
|
-
definition.defaultModel = model.defaultModel;
|
|
106
|
-
if (model.defaultThinkingLevel !== undefined) definition.defaultThinkingLevel = model.defaultThinkingLevel;
|
|
107
|
-
} else if (existing?.defaultProvider !== undefined && modelRaw.trim().length === 0) {
|
|
108
|
-
definition.defaultProvider = existing.defaultProvider;
|
|
109
|
-
definition.defaultModel = existing.defaultModel;
|
|
110
|
-
if (existing.defaultThinkingLevel !== undefined) definition.defaultThinkingLevel = existing.defaultThinkingLevel;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return definition;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Create: scope first (project only when trusted), then name, then fields. */
|
|
117
|
-
export async function runProfileCreateWizard(
|
|
118
|
-
ui: ProfileWizardUi,
|
|
119
|
-
input: { projectTrusted: boolean },
|
|
120
|
-
): Promise<ProfileWizardResult | undefined> {
|
|
121
|
-
const scopeOptions = input.projectTrusted ? ["global", "project"] : ["global"];
|
|
122
|
-
// A single available scope needs no dialog.
|
|
123
|
-
const scope = scopeOptions.length === 1 ? scopeOptions[0] : await ui.select("write to which catalog?", scopeOptions);
|
|
124
|
-
if (scope === undefined) return undefined;
|
|
125
|
-
|
|
126
|
-
const name = await ui.input("profile name");
|
|
127
|
-
if (name === undefined || name.trim().length === 0) return undefined;
|
|
128
|
-
|
|
129
|
-
const definition = await captureDefinition(ui);
|
|
130
|
-
if (definition === undefined) return undefined;
|
|
131
|
-
|
|
132
|
-
return { scope: scope as CatalogScope, name: name.trim(), definition };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Edit: fields prefilled from the existing complete definition. */
|
|
136
|
-
export async function runProfileEditWizard(
|
|
137
|
-
ui: ProfileWizardUi,
|
|
138
|
-
input: { existing: ExistingProfile },
|
|
139
|
-
): Promise<ProfileWizardResult | undefined> {
|
|
140
|
-
const definition = await captureDefinition(ui, input.existing.definition);
|
|
141
|
-
if (definition === undefined) return undefined;
|
|
142
|
-
return { scope: input.existing.source, name: input.existing.name, definition };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Duplicate: pick a source, name the copy — the only variant mechanism. */
|
|
146
|
-
export async function runProfileDuplicateWizard(
|
|
147
|
-
ui: ProfileWizardUi,
|
|
148
|
-
input: { candidates: ExistingProfile[] },
|
|
149
|
-
): Promise<{ scope: CatalogScope; sourceName: string; newName: string } | undefined> {
|
|
150
|
-
if (input.candidates.length === 0) return undefined;
|
|
151
|
-
const options = input.candidates.map(
|
|
152
|
-
(candidate) => `${candidate.name} [${candidate.source}]${candidate.definition.label !== undefined ? ` — ${candidate.definition.label}` : ""}`,
|
|
153
|
-
);
|
|
154
|
-
const chosen = await ui.select("duplicate which profile?", options);
|
|
155
|
-
if (chosen === undefined) return undefined;
|
|
156
|
-
const source = input.candidates.find((candidate) => chosen.startsWith(`${candidate.name} [`));
|
|
157
|
-
if (source === undefined) return undefined;
|
|
158
|
-
|
|
159
|
-
const newName = await ui.input("new profile name");
|
|
160
|
-
if (newName === undefined || newName.trim().length === 0) return undefined;
|
|
161
|
-
|
|
162
|
-
return { scope: source.source, sourceName: source.name, newName: newName.trim() };
|
|
163
|
-
}
|