pi-profiles-manager 1.1.1 → 1.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-profiles-manager",
3
- "version": "1.1.1",
3
+ "version": "1.1.5",
4
4
  "description": "Interactive SDD model profile management built natively for the Pi Coding Agent.",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -70,16 +70,27 @@ describe("registerCommands", () => {
70
70
  );
71
71
  });
72
72
 
73
- it("registers /profiles:sync alias command", () => {
73
+ it("registers slash-command aliases for every profile action", () => {
74
74
  const pi = mockPi();
75
75
  const mgr = new ProfileManager(pi, mockCtx());
76
76
  mgr.setConfig(configWithProfiles([]));
77
77
 
78
78
  registerCommands(pi, mgr, vi.fn() as any, vi.fn() as any);
79
- expect(pi.registerCommand).toHaveBeenCalledWith(
80
- "profiles:sync",
81
- expect.any(Object),
82
- );
79
+
80
+ for (const action of [
81
+ "list",
82
+ "status",
83
+ "sync",
84
+ "use",
85
+ "save",
86
+ "next",
87
+ "off",
88
+ ]) {
89
+ expect(pi.registerCommand).toHaveBeenCalledWith(
90
+ `profiles:${action}`,
91
+ expect.any(Object),
92
+ );
93
+ }
83
94
  });
84
95
 
85
96
  it("completes actions first and profile names after use or save", () => {
@@ -267,23 +278,28 @@ describe("registerCommands", () => {
267
278
  expect(openTui).not.toHaveBeenCalled();
268
279
  });
269
280
 
270
- it("profiles:sync alias command delegates directly to sync without opening TUI", async () => {
281
+ it("profiles action aliases delegate through the existing action handling", async () => {
271
282
  const pi = mockPi();
272
283
  const ctx = mockCtx();
273
284
  const mgr = new ProfileManager(pi, ctx);
285
+ const save = vi.fn(async () => {});
274
286
  const sync = vi.fn(async () => {});
275
287
  const openTui = vi.fn(async () => {});
276
- mgr.setConfig(configWithProfiles([]));
288
+ mgr.setConfig(configWithProfiles(["alpha"]));
277
289
 
278
- registerCommands(pi, mgr, vi.fn() as any, vi.fn() as any, openTui, sync);
290
+ registerCommands(pi, mgr, save as any, vi.fn() as any, openTui, sync);
279
291
 
280
- const callArgs = (pi.registerCommand as any).mock.calls.find(
281
- (c: any[]) => c[0] === "profiles:sync",
282
- );
283
- const handler = callArgs[1].handler;
284
- await handler("", ctx);
292
+ const handlerFor = (command: string) =>
293
+ (pi.registerCommand as any).mock.calls.find(
294
+ (c: any[]) => c[0] === command,
295
+ )[1].handler;
296
+ await handlerFor("profiles:save")("snapshot", ctx);
297
+ await handlerFor("profiles:sync")("", ctx);
298
+ await handlerFor("profiles:list")("", ctx);
285
299
 
300
+ expect(save).toHaveBeenCalledWith(ctx, "snapshot");
286
301
  expect(sync).toHaveBeenCalledWith(ctx);
302
+ expect(ctx.ui.notify).toHaveBeenCalledWith("alpha");
287
303
  expect(openTui).not.toHaveBeenCalled();
288
304
  });
289
305
  });
package/src/commands.ts CHANGED
@@ -30,19 +30,52 @@ export function registerCommands(
30
30
  openTui: OpenTuiFn = async () => {},
31
31
  sync: SyncFn = async () => {},
32
32
  ) {
33
- pi.registerCommand("profiles:sync", {
34
- description: "Sync PiProfiles with managed agents folder",
35
- async handler(_args: string, ctx: ExtensionCommandContext) {
36
- try {
33
+ const handleAction = async (args: string, ctx: ExtensionCommandContext) => {
34
+ manager.setContext(ctx);
35
+ const { verb, name } = parseCommand(args);
36
+
37
+ try {
38
+ if (verb === "list") {
39
+ return ctx.ui.notify(manager.names().join("\n") || "No profiles");
40
+ }
41
+ if (verb === "status") {
42
+ const active = manager.state.get(ctx.sessionManager.getSessionId());
43
+ return ctx.ui.notify(active?.profile ?? "none");
44
+ }
45
+ if (verb === "sync") {
37
46
  await sync(ctx);
38
- } catch (error: unknown) {
39
- ctx.ui.notify(
40
- error instanceof Error ? error.message : String(error),
41
- "error",
42
- );
47
+ return;
43
48
  }
44
- },
45
- });
49
+ if (verb === "use") {
50
+ if (!name) {
51
+ return ctx.ui.notify("Usage: /profiles use <name>", "error");
52
+ }
53
+ await manager.use(name);
54
+ return;
55
+ }
56
+ if (verb === "save") {
57
+ if (!name) {
58
+ return ctx.ui.notify("Usage: /profiles save <name>", "error");
59
+ }
60
+ await _save(ctx, name);
61
+ return;
62
+ }
63
+ if (verb === "next") {
64
+ await manager.next();
65
+ return;
66
+ }
67
+ if (verb === "off") {
68
+ await manager.off();
69
+ return;
70
+ }
71
+ return openTui(ctx as ContextLike);
72
+ } catch (error: unknown) {
73
+ ctx.ui.notify(
74
+ error instanceof Error ? error.message : String(error),
75
+ "error",
76
+ );
77
+ }
78
+ };
46
79
 
47
80
  pi.registerCommand("profiles", {
48
81
  description: "Manage SDD model profiles",
@@ -65,51 +98,14 @@ export function registerCommands(
65
98
  .filter((name) => name.startsWith(namePrefix))
66
99
  .map((value) => ({ value, label: value }));
67
100
  },
68
- async handler(args: string, ctx: ExtensionCommandContext) {
69
- manager.setContext(ctx);
70
- const { verb, name } = parseCommand(args);
71
-
72
- try {
73
- if (verb === "list") {
74
- return ctx.ui.notify(manager.names().join("\n") || "No profiles");
75
- }
76
- if (verb === "status") {
77
- const active = manager.state.get(ctx.sessionManager.getSessionId());
78
- return ctx.ui.notify(active?.profile ?? "none");
79
- }
80
- if (verb === "sync") {
81
- await sync(ctx);
82
- return;
83
- }
84
- if (verb === "use") {
85
- if (!name) {
86
- return ctx.ui.notify("Usage: /profiles use <name>", "error");
87
- }
88
- await manager.use(name);
89
- return;
90
- }
91
- if (verb === "save") {
92
- if (!name) {
93
- return ctx.ui.notify("Usage: /profiles save <name>", "error");
94
- }
95
- await _save(ctx, name);
96
- return;
97
- }
98
- if (verb === "next") {
99
- await manager.next();
100
- return;
101
- }
102
- if (verb === "off") {
103
- await manager.off();
104
- return;
105
- }
106
- return openTui(ctx as ContextLike);
107
- } catch (error: unknown) {
108
- ctx.ui.notify(
109
- error instanceof Error ? error.message : String(error),
110
- "error",
111
- );
112
- }
113
- },
101
+ handler: handleAction,
114
102
  });
103
+
104
+ for (const action of COMMAND_ACTIONS) {
105
+ pi.registerCommand(`profiles:${action}`, {
106
+ description: `PiProfiles: ${action}`,
107
+ handler: (args: string, ctx: ExtensionCommandContext) =>
108
+ handleAction(`${action} ${args}`.trim(), ctx),
109
+ });
110
+ }
115
111
  }
@@ -34,6 +34,7 @@ vi.mock("@earendil-works/pi-tui", () => ({
34
34
  },
35
35
  Text: class {
36
36
  constructor(...args: unknown[]) { tuiState.texts.push(args); }
37
+ setText(text: string) { tuiState.texts.push([text, 1, 0]); }
37
38
  },
38
39
  matchesKey: (data: string, key: string) => data === key,
39
40
  }));
@@ -266,7 +267,7 @@ describe("package extension", () => {
266
267
  });
267
268
 
268
269
  it.each(["return", "escape"])(
269
- "keeps export confirmation open until %s dismisses it",
270
+ "keeps export string visible until %s dismisses it",
270
271
  async (dismissKey) => {
271
272
  const syncFs = await import("node:fs");
272
273
  vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
@@ -290,7 +291,7 @@ describe("package extension", () => {
290
291
  .fn()
291
292
  .mockResolvedValueOnce("alpha")
292
293
  .mockResolvedValueOnce("export")
293
- .mockImplementationOnce(async (factory: any, options: unknown) => {
294
+ .mockImplementationOnce(async (factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, kb: unknown, done: (value: unknown) => void) => { handleInput(data: string): void }, options: unknown) => {
294
295
  confirmationOptions = options;
295
296
  return await new Promise((resolve) => {
296
297
  finishConfirmation = resolve;
@@ -320,7 +321,12 @@ describe("package extension", () => {
320
321
  "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
321
322
  );
322
323
  expect(tuiState.texts).toContainEqual([
323
- "Copied profile 'alpha' to clipboard.",
324
+ "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
325
+ 1,
326
+ 0,
327
+ ]);
328
+ expect(tuiState.texts).toContainEqual([
329
+ expect.stringContaining("Copied profile 'alpha' to clipboard"),
324
330
  1,
325
331
  0,
326
332
  ]);
@@ -331,7 +337,7 @@ describe("package extension", () => {
331
337
  expect(confirmationView).toBeDefined();
332
338
  expect(handlerSettled).toBe(false);
333
339
 
334
- confirmationView!.handleInput(dismissKey);
340
+ confirmationView?.handleInput(dismissKey);
335
341
  await handling;
336
342
 
337
343
  expect(finishConfirmation).toBeDefined();
@@ -343,7 +349,7 @@ describe("package extension", () => {
343
349
  },
344
350
  );
345
351
 
346
- it("reports clipboard failures without showing export confirmation", async () => {
352
+ it("keeps export string visible and shows error if clipboard fails", async () => {
347
353
  const syncFs = await import("node:fs");
348
354
  vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
349
355
  version: 1,
@@ -358,10 +364,27 @@ describe("package extension", () => {
358
364
  const handler = (vi.mocked(pi.registerCommand).mock.calls as any[]).find(
359
365
  ([name]: [string]) => name === "profiles",
360
366
  )[1].handler as (args: string, ctx: any) => Promise<void>;
367
+
368
+ let confirmationView: { handleInput(data: string): void } | undefined;
369
+ let finishConfirmation: ((value: unknown) => void) | undefined;
361
370
  const custom = vi
362
371
  .fn()
363
372
  .mockResolvedValueOnce("alpha")
364
373
  .mockResolvedValueOnce("export")
374
+ .mockImplementationOnce(async (factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, kb: unknown, done: (value: unknown) => void) => { handleInput(data: string): void }, _options: unknown) => {
375
+ return await new Promise((resolve) => {
376
+ finishConfirmation = resolve;
377
+ confirmationView = factory(
378
+ { requestRender: vi.fn() },
379
+ {
380
+ fg: (_color: string, text: string) => text,
381
+ bold: (text: string) => text,
382
+ },
383
+ {},
384
+ resolve,
385
+ );
386
+ });
387
+ })
365
388
  .mockResolvedValueOnce(null);
366
389
  const ctx = {
367
390
  sessionManager: { getSessionId: () => "session-1" },
@@ -369,22 +392,28 @@ describe("package extension", () => {
369
392
  ui: { custom, notify: vi.fn(), setStatus: vi.fn() },
370
393
  };
371
394
 
372
- await handler("", ctx);
395
+ let handlerSettled = false;
396
+ const handling = handler("", ctx).then(() => { handlerSettled = true; });
397
+ await vi.waitFor(() => expect(custom).toHaveBeenCalledTimes(3));
373
398
 
374
- expect(ctx.ui.notify).toHaveBeenCalledWith(
375
- "Failed to copy profile 'alpha' to clipboard: clipboard unavailable",
376
- "error",
377
- );
378
- expect(custom).toHaveBeenCalledTimes(3);
379
- expect(tuiState.texts).not.toContainEqual([
380
- "Copied profile 'alpha' to clipboard.",
399
+ expect(tuiState.texts).toContainEqual([
400
+ "piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
381
401
  1,
382
402
  0,
383
403
  ]);
384
- expect(ctx.ui.notify).not.toHaveBeenCalledWith(
385
- "Copied profile 'alpha' to clipboard.",
386
- "info",
387
- );
404
+ expect(tuiState.texts).toContainEqual([
405
+ expect.stringContaining("Failed to copy profile 'alpha' to clipboard"),
406
+ 1,
407
+ 0,
408
+ ]);
409
+ expect(confirmationView).toBeDefined();
410
+ expect(handlerSettled).toBe(false);
411
+
412
+ confirmationView?.handleInput("return");
413
+ await handling;
414
+
415
+ expect(finishConfirmation).toBeDefined();
416
+ expect(handlerSettled).toBe(true);
388
417
  });
389
418
 
390
419
  it("imports typed profile strings into the authoritative config without legacy storage", async () => {
package/src/extension.ts CHANGED
@@ -174,12 +174,26 @@ function prompt(ctx: ContextLike, title: string, initial = ""): Promise<string |
174
174
  }, { overlay: true });
175
175
  }
176
176
 
177
- function showCopyConfirmation(ctx: ContextLike, message: string): Promise<void> {
178
- return (ctx.ui.custom as any)((tui: any, theme: any, _kb: any, done: any) => {
179
- const container = new Container() as any;
177
+ function showExportDialog(ctx: ContextLike, profileName: string, exportString: string, copyPromise: Promise<void>): Promise<void> {
178
+ return (ctx.ui.custom as (factory: unknown, options?: unknown) => Promise<void>)((tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, _kb: unknown, done: (value: void) => void) => {
179
+ const container = new Container() as { addChild: (child: unknown) => void; render: (width: number) => unknown; invalidate: () => void };
180
+ const statusText = new Text(theme.fg("accent", theme.bold(`Copying profile '${profileName}' to clipboard...`)), 1, 0);
181
+
180
182
  container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
181
- container.addChild(new Text(theme.fg("accent", theme.bold(message)), 1, 0));
183
+ container.addChild(statusText);
184
+ container.addChild(new Text(theme.fg("accent", exportString), 1, 0));
182
185
  container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
186
+
187
+ copyPromise.then(() => {
188
+ statusText.setText(theme.fg("accent", theme.bold(`Copied profile '${profileName}' to clipboard.`)));
189
+ container.invalidate();
190
+ tui.requestRender();
191
+ }).catch((error) => {
192
+ statusText.setText(theme.fg("error", theme.bold(`Failed to copy profile '${profileName}' to clipboard: ${error instanceof Error ? error.message : String(error)}`)));
193
+ container.invalidate();
194
+ tui.requestRender();
195
+ });
196
+
183
197
  return {
184
198
  render: (width: number) => container.render(width),
185
199
  invalidate: () => container.invalidate(),
@@ -509,12 +523,9 @@ export default function extension(pi: PiLike) {
509
523
  favorite: selected === manager.config.defaultProfile,
510
524
  },
511
525
  })).toString("base64");
512
- try {
513
- await copyToClipboard(`piprofile:1:${encoded}`);
514
- await showCopyConfirmation(ctx, `Copied profile '${selected}' to clipboard.`);
515
- } catch (error) {
516
- ctx.ui.notify(`Failed to copy profile '${selected}' to clipboard: ${error instanceof Error ? error.message : String(error)}`, "error");
517
- }
526
+ const exportString = `piprofile:1:${encoded}`;
527
+ const copyPromise = copyToClipboard(exportString);
528
+ await showExportDialog(ctx, selected, exportString, copyPromise);
518
529
  continue;
519
530
  }
520
531
  if (action === "delete") {