pi-profile-switch 0.1.0 → 0.3.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.
@@ -24,20 +24,17 @@ export async function listProfiles(input: {
24
24
  realAgentDir: string;
25
25
  cwd: string;
26
26
  projectTrusted: boolean;
27
- }): Promise<{ entries: ProfileListEntry[]; warnings: string[] }> {
27
+ }): Promise<ProfileListEntry[]> {
28
28
  const catalog = await ProfileCatalog.load(input.realAgentDir, {
29
29
  projectDir: input.projectTrusted ? input.cwd : undefined,
30
30
  });
31
- return {
32
- entries: catalog.list().map((profile) => ({
33
- name: profile.name,
34
- source: profile.source,
35
- ...(typeof profile.definition.label === "string" ? { label: profile.definition.label } : {}),
36
- ...(typeof profile.definition.description === "string" ? { description: profile.definition.description } : {}),
37
- shadowsGlobal: profile.source === "project" && catalog.shadowsGlobal(profile.name),
38
- })),
39
- warnings: [...catalog.warnings],
40
- };
31
+ return catalog.list().map((profile) => ({
32
+ name: profile.name,
33
+ source: profile.source,
34
+ ...(typeof profile.definition.label === "string" ? { label: profile.definition.label } : {}),
35
+ ...(typeof profile.definition.description === "string" ? { description: profile.definition.description } : {}),
36
+ shadowsGlobal: profile.source === "project" && catalog.shadowsGlobal(profile.name),
37
+ }));
41
38
  }
42
39
 
43
40
  export function formatProfileList(entries: ProfileListEntry[], activeProfile?: string): string {
@@ -8,9 +8,14 @@
8
8
  * keeps the current value (prefill via placeholder); there is no
9
9
  * field-clearing gesture (delete + create instead). Any cancelled step
10
10
  * aborts the wizard — nothing is written.
11
+ *
12
+ * Create can start from a shipped preset (src/profile-presets.ts). A preset is
13
+ * prefilled form state only: the wizard copies its complete definition into the
14
+ * chosen catalog, and the new profile belongs to the user from then on.
11
15
  */
12
16
 
13
17
  import type { ProfileDefinition } from "../profile-catalog.ts";
18
+ import { PROFILE_PRESETS, type ProfilePreset } from "../profile-presets.ts";
14
19
  import type { CatalogScope } from "./profile-crud.ts";
15
20
 
16
21
  export interface ProfileWizardUi {
@@ -22,6 +27,8 @@ export interface ProfileWizardResult {
22
27
  scope: CatalogScope;
23
28
  name: string;
24
29
  definition: ProfileDefinition;
30
+ /** Name of the preset this definition was copied from, when any. */
31
+ preset?: string;
25
32
  }
26
33
 
27
34
  interface ExistingProfile {
@@ -108,23 +115,49 @@ async function captureDefinition(
108
115
  return definition;
109
116
  }
110
117
 
111
- /** Create: scope first (project only when trusted), then name, then fields. */
118
+ const BLANK_OPTION = "blank — start from an empty definition";
119
+
120
+ /** One preset row. The caller identifies the preset by the option's INDEX, so
121
+ * a preset named like another row's text cannot be misread. */
122
+ function presetOption(preset: ProfilePreset): string {
123
+ const hint = preset.definition.description ?? preset.definition.label;
124
+ return hint !== undefined ? `${preset.name} — ${hint}` : preset.name;
125
+ }
126
+
127
+ /** Create: scope, optional preset, name, then fields. */
112
128
  export async function runProfileCreateWizard(
113
129
  ui: ProfileWizardUi,
114
- input: { projectTrusted: boolean },
130
+ input: { projectTrusted: boolean; presets?: readonly ProfilePreset[] },
115
131
  ): Promise<ProfileWizardResult | undefined> {
116
132
  const scopeOptions = input.projectTrusted ? ["global", "project"] : ["global"];
117
133
  // A single available scope needs no dialog.
118
134
  const scope = scopeOptions.length === 1 ? scopeOptions[0] : await ui.select("write to which catalog?", scopeOptions);
119
135
  if (scope === undefined) return undefined;
120
136
 
121
- const name = await ui.input("profile name");
122
- if (name === undefined || name.trim().length === 0) return undefined;
137
+ const presets = input.presets ?? PROFILE_PRESETS;
138
+ let preset: ProfilePreset | undefined;
139
+ if (presets.length > 0) {
140
+ const options = [BLANK_OPTION, ...presets.map(presetOption)];
141
+ const chosen = await ui.select("start from which preset?", options);
142
+ if (chosen === undefined) return undefined;
143
+ preset = presets[options.indexOf(chosen) - 1];
144
+ }
145
+
146
+ // An empty answer takes the preset's name, so Enter accepts the preset.
147
+ const name = await ui.input("profile name", preset?.name);
148
+ if (name === undefined) return undefined;
149
+ const resolvedName = name.trim().length > 0 ? name.trim() : preset?.name;
150
+ if (resolvedName === undefined) return undefined;
123
151
 
124
- const definition = await captureDefinition(ui);
152
+ const definition = await captureDefinition(ui, preset?.definition);
125
153
  if (definition === undefined) return undefined;
126
154
 
127
- return { scope: scope as CatalogScope, name: name.trim(), definition };
155
+ return {
156
+ scope: scope as CatalogScope,
157
+ name: resolvedName,
158
+ definition,
159
+ ...(preset !== undefined ? { preset: preset.name } : {}),
160
+ };
128
161
  }
129
162
 
130
163
  /** Edit: fields prefilled from the existing complete definition. */