azp-cli 0.0.3 → 0.0.4

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/src/index.ts CHANGED
@@ -1,10 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from "commander";
4
+ import inquirer from "inquirer";
4
5
  import { version } from "../package.json";
5
6
  import { authenticate } from "./auth";
6
7
  import { activateOnce, deactivateOnce, showMainMenu } from "./cli";
7
- import { configureUi, logBlank, logDim, logError, showHeader } from "./ui";
8
+ import { runPresetAddWizard, runPresetEditWizard } from "./presets-cli";
9
+ import {
10
+ expandTemplate,
11
+ getDefaultPresetsFilePath,
12
+ getPreset,
13
+ listPresetNames,
14
+ loadPresets,
15
+ removePreset,
16
+ savePresets,
17
+ resolveActivatePresetOptions,
18
+ resolveDeactivatePresetOptions,
19
+ setDefaultPresetName,
20
+ upsertPreset,
21
+ } from "./presets";
22
+ import { configureUi, logBlank, logDim, logError, logInfo, logSuccess, logWarning, showHeader } from "./ui";
8
23
 
9
24
  type OutputFormat = "text" | "json";
10
25
 
@@ -13,6 +28,7 @@ type ActivateCommandOptions = {
13
28
  roleName?: string[];
14
29
  durationHours?: number;
15
30
  justification?: string;
31
+ preset?: string;
16
32
  noInteractive?: boolean;
17
33
  yes?: boolean;
18
34
  allowMultiple?: boolean;
@@ -25,6 +41,7 @@ type DeactivateCommandOptions = {
25
41
  subscriptionId?: string;
26
42
  roleName?: string[];
27
43
  justification?: string;
44
+ preset?: string;
28
45
  noInteractive?: boolean;
29
46
  yes?: boolean;
30
47
  allowMultiple?: boolean;
@@ -33,6 +50,26 @@ type DeactivateCommandOptions = {
33
50
  quiet?: boolean;
34
51
  };
35
52
 
53
+ type PresetCommandOptions = {
54
+ output?: OutputFormat;
55
+ quiet?: boolean;
56
+ fromAzure?: boolean;
57
+ yes?: boolean;
58
+ };
59
+
60
+ const getOptionValueSource = (command: Command, optionName: string): string | undefined => {
61
+ const fn = (command as any).getOptionValueSource;
62
+ if (typeof fn === "function") return fn.call(command, optionName);
63
+ return undefined;
64
+ };
65
+
66
+ const resolveValue = <T>(command: Command, optionName: string, cliValue: T, presetValue: T | undefined): T => {
67
+ const source = getOptionValueSource(command, optionName);
68
+ if (source === "cli") return cliValue;
69
+ if (presetValue !== undefined) return presetValue;
70
+ return cliValue;
71
+ };
72
+
36
73
  const program = new Command();
37
74
 
38
75
  program.name("azp-cli").description("Azure PIM CLI - A CLI tool for Azure Privilege Identity Management (PIM)").version(version);
@@ -54,13 +91,14 @@ program
54
91
  )
55
92
  .option("--duration-hours <n>", "Duration hours (1-8)", (value: string) => Number.parseInt(value, 10))
56
93
  .option("--justification <text>", "Justification for activation")
94
+ .option("--preset <name>", "Use a saved preset (fills defaults; flags still override)")
57
95
  .option("--no-interactive", "Do not prompt; require flags to be unambiguous")
58
96
  .option("-y, --yes", "Skip confirmation prompt")
59
97
  .option("--allow-multiple", "Allow activating multiple eligible matches for a role name")
60
98
  .option("--dry-run", "Resolve targets and print summary without submitting activation requests")
61
99
  .option("--output <text|json>", "Output format", "text")
62
100
  .option("--quiet", "Suppress non-essential output (recommended with --output json)")
63
- .action(async (cmd: ActivateCommandOptions) => {
101
+ .action(async (cmd: ActivateCommandOptions, command: Command) => {
64
102
  try {
65
103
  const output = (cmd.output ?? "text") as OutputFormat;
66
104
  const quiet = Boolean(cmd.quiet || output === "json");
@@ -69,26 +107,65 @@ program
69
107
  // Show header (text mode only)
70
108
  showHeader();
71
109
 
72
- // Authenticate
73
- const authContext = await authenticate();
110
+ const explicitPresetName = getOptionValueSource(command, "preset") === "cli" ? cmd.preset : undefined;
74
111
 
75
112
  const requestedRoleNames = cmd.roleName ?? [];
76
- const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun);
113
+ const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun || explicitPresetName);
114
+
115
+ // Authenticate (required for both interactive and one-shot flows)
116
+ const authContext = await authenticate();
77
117
 
78
118
  if (!wantsOneShot) {
79
119
  await showMainMenu(authContext);
80
120
  return;
81
121
  }
82
122
 
123
+ const presets = await loadPresets();
124
+ const hasRoleNamesFromCli = getOptionValueSource(command, "roleName") === "cli";
125
+ const hasSubscriptionFromCli = getOptionValueSource(command, "subscriptionId") === "cli";
126
+
127
+ const defaultPresetName = presets.data.defaults?.activatePreset;
128
+ const shouldUseDefaultPreset =
129
+ !explicitPresetName && Boolean(defaultPresetName) && (!hasSubscriptionFromCli || !hasRoleNamesFromCli);
130
+
131
+ const presetNameToUse = explicitPresetName ?? (shouldUseDefaultPreset ? defaultPresetName : undefined);
132
+
133
+ if (explicitPresetName) {
134
+ const entry = getPreset(presets.data, explicitPresetName);
135
+ if (!entry) {
136
+ const names = listPresetNames(presets.data);
137
+ throw new Error(
138
+ `Preset not found: "${explicitPresetName}". Presets file: ${presets.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`
139
+ );
140
+ }
141
+ if (!entry.activate) {
142
+ throw new Error(`Preset "${explicitPresetName}" does not define an activate block.`);
143
+ }
144
+ }
145
+
146
+ const presetOptions = resolveActivatePresetOptions(presets.data, presetNameToUse);
147
+
148
+ const effectiveSubscriptionId = resolveValue(command, "subscriptionId", cmd.subscriptionId ?? "", presetOptions.subscriptionId);
149
+ const effectiveRoleNames = resolveValue(command, "roleName", requestedRoleNames, presetOptions.roleNames ?? undefined);
150
+ const effectiveDurationHours = resolveValue(command, "durationHours", cmd.durationHours, presetOptions.durationHours);
151
+ const effectiveAllowMultiple = resolveValue(command, "allowMultiple", cmd.allowMultiple, presetOptions.allowMultiple);
152
+ const effectiveJustificationRaw = resolveValue(command, "justification", cmd.justification, presetOptions.justification);
153
+ const effectiveJustification = effectiveJustificationRaw
154
+ ? expandTemplate(effectiveJustificationRaw, {
155
+ userId: authContext.userId,
156
+ userPrincipalName: authContext.userPrincipalName,
157
+ })
158
+ : undefined;
159
+
83
160
  const result = await activateOnce(authContext, {
84
- subscriptionId: cmd.subscriptionId ?? "",
85
- roleNames: requestedRoleNames,
86
- durationHours: cmd.durationHours,
87
- justification: cmd.justification,
161
+ subscriptionId: effectiveSubscriptionId,
162
+ roleNames: effectiveRoleNames,
163
+ durationHours: effectiveDurationHours,
164
+ justification: effectiveJustification,
88
165
  dryRun: cmd.dryRun,
89
166
  noInteractive: cmd.noInteractive,
90
167
  yes: cmd.yes,
91
- allowMultiple: cmd.allowMultiple,
168
+ allowMultiple: effectiveAllowMultiple,
92
169
  });
93
170
 
94
171
  if (output === "json") {
@@ -138,13 +215,14 @@ program
138
215
  []
139
216
  )
140
217
  .option("--justification <text>", "Justification for deactivation")
218
+ .option("--preset <name>", "Use a saved preset (fills defaults; flags still override)")
141
219
  .option("--no-interactive", "Do not prompt; require flags to be unambiguous")
142
220
  .option("-y, --yes", "Skip confirmation prompt")
143
221
  .option("--allow-multiple", "Allow deactivating multiple active matches for a role name")
144
222
  .option("--dry-run", "Resolve targets and print summary without submitting deactivation requests")
145
223
  .option("--output <text|json>", "Output format", "text")
146
224
  .option("--quiet", "Suppress non-essential output (recommended with --output json)")
147
- .action(async (cmd: DeactivateCommandOptions) => {
225
+ .action(async (cmd: DeactivateCommandOptions, command: Command) => {
148
226
  try {
149
227
  const output = (cmd.output ?? "text") as OutputFormat;
150
228
  const quiet = Boolean(cmd.quiet || output === "json");
@@ -153,25 +231,60 @@ program
153
231
  // Show header (text mode only)
154
232
  showHeader();
155
233
 
156
- // Authenticate
157
- const authContext = await authenticate();
234
+ const explicitPresetName = getOptionValueSource(command, "preset") === "cli" ? cmd.preset : undefined;
158
235
 
159
236
  const requestedRoleNames = cmd.roleName ?? [];
160
- const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun);
237
+ const wantsOneShot = Boolean(cmd.noInteractive || cmd.subscriptionId || requestedRoleNames.length > 0 || cmd.dryRun || explicitPresetName);
238
+
239
+ // Authenticate (required for both interactive and one-shot flows)
240
+ const authContext = await authenticate();
161
241
 
162
242
  if (!wantsOneShot) {
163
243
  await showMainMenu(authContext);
164
244
  return;
165
245
  }
166
246
 
247
+ const presets = await loadPresets();
248
+ const hasRoleNamesFromCli = getOptionValueSource(command, "roleName") === "cli";
249
+
250
+ const defaultPresetName = presets.data.defaults?.deactivatePreset;
251
+ const shouldUseDefaultPreset = !explicitPresetName && Boolean(defaultPresetName) && !hasRoleNamesFromCli;
252
+ const presetNameToUse = explicitPresetName ?? (shouldUseDefaultPreset ? defaultPresetName : undefined);
253
+
254
+ if (explicitPresetName) {
255
+ const entry = getPreset(presets.data, explicitPresetName);
256
+ if (!entry) {
257
+ const names = listPresetNames(presets.data);
258
+ throw new Error(
259
+ `Preset not found: "${explicitPresetName}". Presets file: ${presets.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`
260
+ );
261
+ }
262
+ if (!entry.deactivate) {
263
+ throw new Error(`Preset "${explicitPresetName}" does not define a deactivate block.`);
264
+ }
265
+ }
266
+
267
+ const presetOptions = resolveDeactivatePresetOptions(presets.data, presetNameToUse);
268
+
269
+ const effectiveSubscriptionId = resolveValue(command, "subscriptionId", cmd.subscriptionId, presetOptions.subscriptionId);
270
+ const effectiveRoleNames = resolveValue(command, "roleName", requestedRoleNames, presetOptions.roleNames ?? undefined);
271
+ const effectiveAllowMultiple = resolveValue(command, "allowMultiple", cmd.allowMultiple, presetOptions.allowMultiple);
272
+ const effectiveJustificationRaw = resolveValue(command, "justification", cmd.justification, presetOptions.justification);
273
+ const effectiveJustification = effectiveJustificationRaw
274
+ ? expandTemplate(effectiveJustificationRaw, {
275
+ userId: authContext.userId,
276
+ userPrincipalName: authContext.userPrincipalName,
277
+ })
278
+ : undefined;
279
+
167
280
  const result = await deactivateOnce(authContext, {
168
- subscriptionId: cmd.subscriptionId,
169
- roleNames: requestedRoleNames,
170
- justification: cmd.justification,
281
+ subscriptionId: effectiveSubscriptionId,
282
+ roleNames: effectiveRoleNames,
283
+ justification: effectiveJustification,
171
284
  dryRun: cmd.dryRun,
172
285
  noInteractive: cmd.noInteractive,
173
286
  yes: cmd.yes,
174
- allowMultiple: cmd.allowMultiple,
287
+ allowMultiple: effectiveAllowMultiple,
175
288
  });
176
289
 
177
290
  if (output === "json") {
@@ -213,4 +326,372 @@ program
213
326
  program.outputHelp();
214
327
  });
215
328
 
329
+ const presetCommand = program
330
+ .command("preset")
331
+ .description("Manage azp-cli presets")
332
+ .addHelpText(
333
+ "after",
334
+ `\nPresets file location:\n ${getDefaultPresetsFilePath()}\n\nYou can override via environment variable AZP_PRESETS_PATH.\n`
335
+ );
336
+
337
+ presetCommand
338
+ .command("list")
339
+ .description("List available presets")
340
+ .option("--output <text|json>", "Output format", "text")
341
+ .option("--quiet", "Suppress non-essential output (recommended with --output json)")
342
+ .action(async (cmd: PresetCommandOptions) => {
343
+ try {
344
+ const output = (cmd.output ?? "text") as OutputFormat;
345
+ const quiet = Boolean(cmd.quiet || output === "json");
346
+ configureUi({ quiet });
347
+
348
+ showHeader();
349
+
350
+ const loaded = await loadPresets();
351
+ const names = listPresetNames(loaded.data);
352
+
353
+ if (output === "json") {
354
+ process.stdout.write(
355
+ `${JSON.stringify(
356
+ {
357
+ ok: true,
358
+ filePath: loaded.filePath,
359
+ defaults: loaded.data.defaults ?? {},
360
+ presets: names,
361
+ },
362
+ null,
363
+ 2
364
+ )}\n`
365
+ );
366
+ return;
367
+ }
368
+
369
+ logInfo(`Presets file: ${loaded.filePath}`);
370
+ if (!loaded.exists) {
371
+ logDim("(File does not exist yet; use 'azp preset add' to create one.)");
372
+ }
373
+ logBlank();
374
+
375
+ if (names.length === 0) {
376
+ logWarning("No presets found.");
377
+ return;
378
+ }
379
+
380
+ const defaultActivate = loaded.data.defaults?.activatePreset;
381
+ const defaultDeactivate = loaded.data.defaults?.deactivatePreset;
382
+ for (const name of names) {
383
+ const tags: string[] = [];
384
+ if (defaultActivate === name) tags.push("default:activate");
385
+ if (defaultDeactivate === name) tags.push("default:deactivate");
386
+ const suffix = tags.length ? ` (${tags.join(", ")})` : "";
387
+ logInfo(`${name}${suffix}`);
388
+ }
389
+ } catch (error: any) {
390
+ const errorMessage = error instanceof Error ? error.message : String(error);
391
+ const output = (cmd.output ?? "text") as OutputFormat;
392
+ if (output === "json") {
393
+ process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
394
+ process.exit(1);
395
+ }
396
+ logBlank();
397
+ logError(`An error occurred: ${errorMessage}`);
398
+ logBlank();
399
+ process.exit(1);
400
+ }
401
+ });
402
+
403
+ presetCommand
404
+ .command("show")
405
+ .description("Show a preset")
406
+ .argument("<name>", "Preset name")
407
+ .option("--output <text|json>", "Output format", "text")
408
+ .option("--quiet", "Suppress non-essential output (recommended with --output json)")
409
+ .action(async (name: string, cmd: PresetCommandOptions) => {
410
+ try {
411
+ const output = (cmd.output ?? "text") as OutputFormat;
412
+ const quiet = Boolean(cmd.quiet || output === "json");
413
+ configureUi({ quiet });
414
+
415
+ showHeader();
416
+
417
+ const loaded = await loadPresets();
418
+ const entry = getPreset(loaded.data, name);
419
+ if (!entry) {
420
+ const names = listPresetNames(loaded.data);
421
+ throw new Error(
422
+ `Preset not found: "${name}". Presets file: ${loaded.filePath}. Available: ${names.length ? names.join(", ") : "(none)"}`
423
+ );
424
+ }
425
+
426
+ if (output === "json") {
427
+ process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name, preset: entry }, null, 2)}\n`);
428
+ return;
429
+ }
430
+
431
+ logInfo(`Preset: ${name}`);
432
+ if (entry.description) logDim(entry.description);
433
+ logBlank();
434
+
435
+ if (entry.activate) {
436
+ logInfo("activate:");
437
+ logDim(` subscriptionId: ${entry.activate.subscriptionId ?? "(unset)"}`);
438
+ logDim(` roleNames: ${(entry.activate.roleNames ?? []).join(", ") || "(unset)"}`);
439
+ logDim(` durationHours: ${entry.activate.durationHours ?? "(unset)"}`);
440
+ logDim(` justification: ${entry.activate.justification ?? "(unset)"}`);
441
+ logDim(` allowMultiple: ${entry.activate.allowMultiple ?? "(unset)"}`);
442
+ logBlank();
443
+ }
444
+
445
+ if (entry.deactivate) {
446
+ logInfo("deactivate:");
447
+ logDim(` subscriptionId: ${entry.deactivate.subscriptionId ?? "(unset)"}`);
448
+ logDim(` roleNames: ${(entry.deactivate.roleNames ?? []).join(", ") || "(unset)"}`);
449
+ logDim(` justification: ${entry.deactivate.justification ?? "(unset)"}`);
450
+ logDim(` allowMultiple: ${entry.deactivate.allowMultiple ?? "(unset)"}`);
451
+ }
452
+ } catch (error: any) {
453
+ const errorMessage = error instanceof Error ? error.message : String(error);
454
+ const output = (cmd.output ?? "text") as OutputFormat;
455
+ if (output === "json") {
456
+ process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
457
+ process.exit(1);
458
+ }
459
+ logBlank();
460
+ logError(`An error occurred: ${errorMessage}`);
461
+ logBlank();
462
+ process.exit(1);
463
+ }
464
+ });
465
+
466
+ presetCommand
467
+ .command("add")
468
+ .description("Add a preset (interactive wizard)")
469
+ .argument("[name]", "Preset name")
470
+ .option("--from-azure", "Pick subscription and role names from Azure (prompts for auth)")
471
+ .option("--no-from-azure", "Create preset without querying Azure")
472
+ .option("-y, --yes", "Skip confirmation prompts")
473
+ .option("--output <text|json>", "Output format", "text")
474
+ .option("--quiet", "Suppress non-essential output (recommended with --output json)")
475
+ .action(async (name: string | undefined, cmd: PresetCommandOptions, command: Command) => {
476
+ try {
477
+ const output = (cmd.output ?? "text") as OutputFormat;
478
+ const quiet = Boolean(cmd.quiet || output === "json");
479
+ configureUi({ quiet });
480
+
481
+ showHeader();
482
+
483
+ const loaded = await loadPresets();
484
+ const existingNames = listPresetNames(loaded.data);
485
+
486
+ if (name && getPreset(loaded.data, name)) {
487
+ if (!cmd.yes) {
488
+ const { overwrite } = await inquirer.prompt<{ overwrite: boolean }>([
489
+ {
490
+ type: "confirm",
491
+ name: "overwrite",
492
+ message: `Preset "${name}" already exists. Overwrite?`,
493
+ default: false,
494
+ },
495
+ ]);
496
+ if (!overwrite) {
497
+ throw new Error("Aborted.");
498
+ }
499
+ }
500
+ }
501
+
502
+ const fromAzureSource = getOptionValueSource(command, "fromAzure");
503
+ const wizardFromAzure = fromAzureSource === "cli" ? Boolean(cmd.fromAzure) : undefined;
504
+
505
+ const namesForValidation = name && getPreset(loaded.data, name) ? existingNames.filter((n) => n !== name) : existingNames;
506
+
507
+ const wizard = await runPresetAddWizard({
508
+ name,
509
+ fromAzure: wizardFromAzure,
510
+ existingNames: namesForValidation,
511
+ });
512
+
513
+ if (!cmd.yes) {
514
+ const { confirmSave } = await inquirer.prompt<{ confirmSave: boolean }>([
515
+ {
516
+ type: "confirm",
517
+ name: "confirmSave",
518
+ message: "Save this preset?",
519
+ default: true,
520
+ },
521
+ ]);
522
+ if (!confirmSave) {
523
+ throw new Error("Aborted.");
524
+ }
525
+ }
526
+
527
+ let next = upsertPreset(loaded.data, wizard.name, wizard.entry);
528
+
529
+ if (wizard.setDefaultFor === "activate" || wizard.setDefaultFor === "both") {
530
+ next = setDefaultPresetName(next, "activate", wizard.name);
531
+ }
532
+ if (wizard.setDefaultFor === "deactivate" || wizard.setDefaultFor === "both") {
533
+ next = setDefaultPresetName(next, "deactivate", wizard.name);
534
+ }
535
+
536
+ await savePresets(loaded.filePath, next);
537
+
538
+ if (output === "json") {
539
+ process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name: wizard.name }, null, 2)}\n`);
540
+ return;
541
+ }
542
+
543
+ logSuccess(`Preset saved: ${wizard.name}`);
544
+ logDim(`File: ${loaded.filePath}`);
545
+ } catch (error: any) {
546
+ const errorMessage = error instanceof Error ? error.message : String(error);
547
+ const output = (cmd.output ?? "text") as OutputFormat;
548
+ if (output === "json") {
549
+ process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
550
+ process.exit(1);
551
+ }
552
+ logBlank();
553
+ logError(`An error occurred: ${errorMessage}`);
554
+ logBlank();
555
+ process.exit(1);
556
+ }
557
+ });
558
+
559
+ presetCommand
560
+ .command("edit")
561
+ .description("Edit an existing preset (interactive wizard)")
562
+ .argument("<name>", "Preset name")
563
+ .option("--from-azure", "Pick subscription and role names from Azure (prompts for auth)")
564
+ .option("--no-from-azure", "Edit preset without querying Azure")
565
+ .option("-y, --yes", "Skip confirmation prompts")
566
+ .option("--output <text|json>", "Output format", "text")
567
+ .option("--quiet", "Suppress non-essential output (recommended with --output json)")
568
+ .action(async (name: string, cmd: PresetCommandOptions, command: Command) => {
569
+ try {
570
+ const output = (cmd.output ?? "text") as OutputFormat;
571
+ const quiet = Boolean(cmd.quiet || output === "json");
572
+ configureUi({ quiet });
573
+
574
+ showHeader();
575
+
576
+ const loaded = await loadPresets();
577
+ const existing = getPreset(loaded.data, name);
578
+ if (!existing) {
579
+ throw new Error(`Preset not found: "${name}"`);
580
+ }
581
+
582
+ const fromAzureSource = getOptionValueSource(command, "fromAzure");
583
+ const wizardFromAzure = fromAzureSource === "cli" ? Boolean(cmd.fromAzure) : undefined;
584
+
585
+ const wizard = await runPresetEditWizard({
586
+ name,
587
+ existingEntry: existing,
588
+ fromAzure: wizardFromAzure,
589
+ });
590
+
591
+ if (!cmd.yes) {
592
+ const { confirmSave } = await inquirer.prompt<{ confirmSave: boolean }>([
593
+ {
594
+ type: "confirm",
595
+ name: "confirmSave",
596
+ message: `Save changes to preset "${name}"?`,
597
+ default: true,
598
+ },
599
+ ]);
600
+ if (!confirmSave) {
601
+ throw new Error("Aborted.");
602
+ }
603
+ }
604
+
605
+ let next = upsertPreset(loaded.data, name, wizard.entry);
606
+ if (wizard.setDefaultFor === "activate" || wizard.setDefaultFor === "both") {
607
+ next = setDefaultPresetName(next, "activate", name);
608
+ }
609
+ if (wizard.setDefaultFor === "deactivate" || wizard.setDefaultFor === "both") {
610
+ next = setDefaultPresetName(next, "deactivate", name);
611
+ }
612
+
613
+ await savePresets(loaded.filePath, next);
614
+
615
+ if (output === "json") {
616
+ process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name }, null, 2)}\n`);
617
+ return;
618
+ }
619
+
620
+ logSuccess(`Preset updated: ${name}`);
621
+ logDim(`File: ${loaded.filePath}`);
622
+ } catch (error: any) {
623
+ const errorMessage = error instanceof Error ? error.message : String(error);
624
+ const output = (cmd.output ?? "text") as OutputFormat;
625
+ if (output === "json") {
626
+ process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
627
+ process.exit(1);
628
+ }
629
+ logBlank();
630
+ logError(`An error occurred: ${errorMessage}`);
631
+ logBlank();
632
+ process.exit(1);
633
+ }
634
+ });
635
+
636
+ presetCommand
637
+ .command("remove")
638
+ .description("Remove a preset")
639
+ .argument("<name>", "Preset name")
640
+ .option("-y, --yes", "Skip confirmation prompt")
641
+ .option("--output <text|json>", "Output format", "text")
642
+ .option("--quiet", "Suppress non-essential output (recommended with --output json)")
643
+ .action(async (name: string, cmd: PresetCommandOptions) => {
644
+ try {
645
+ const output = (cmd.output ?? "text") as OutputFormat;
646
+ const quiet = Boolean(cmd.quiet || output === "json");
647
+ configureUi({ quiet });
648
+
649
+ showHeader();
650
+
651
+ const loaded = await loadPresets();
652
+ const entry = getPreset(loaded.data, name);
653
+ if (!entry) {
654
+ throw new Error(`Preset not found: "${name}"`);
655
+ }
656
+
657
+ if (!cmd.yes) {
658
+ const { confirmRemove } = await inquirer.prompt<{ confirmRemove: boolean }>([
659
+ {
660
+ type: "confirm",
661
+ name: "confirmRemove",
662
+ message: `Remove preset "${name}"?`,
663
+ default: false,
664
+ },
665
+ ]);
666
+ if (!confirmRemove) {
667
+ throw new Error("Aborted.");
668
+ }
669
+ }
670
+
671
+ let next = removePreset(loaded.data, name);
672
+ if (next.defaults?.activatePreset === name) next = setDefaultPresetName(next, "activate", undefined);
673
+ if (next.defaults?.deactivatePreset === name) next = setDefaultPresetName(next, "deactivate", undefined);
674
+
675
+ await savePresets(loaded.filePath, next);
676
+
677
+ if (output === "json") {
678
+ process.stdout.write(`${JSON.stringify({ ok: true, filePath: loaded.filePath, name }, null, 2)}\n`);
679
+ return;
680
+ }
681
+
682
+ logSuccess(`Preset removed: ${name}`);
683
+ } catch (error: any) {
684
+ const errorMessage = error instanceof Error ? error.message : String(error);
685
+ const output = (cmd.output ?? "text") as OutputFormat;
686
+ if (output === "json") {
687
+ process.stdout.write(`${JSON.stringify({ ok: false, error: errorMessage }, null, 2)}\n`);
688
+ process.exit(1);
689
+ }
690
+ logBlank();
691
+ logError(`An error occurred: ${errorMessage}`);
692
+ logBlank();
693
+ process.exit(1);
694
+ }
695
+ });
696
+
216
697
  program.parse();