@paradigma-inc/flywheel 0.1.84 → 0.1.86

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.
@@ -0,0 +1,680 @@
1
+ import {
2
+ mkdir,
3
+ readFile,
4
+ rename,
5
+ rm,
6
+ unlink,
7
+ writeFile,
8
+ } from "node:fs/promises";
9
+ import path from "node:path";
10
+
11
+ import { generateCompletionScript as generateUsageCompletionScript } from "./completion.mjs";
12
+
13
+ const SUPPORTED_INSTALL_SHELLS = [
14
+ "bash",
15
+ "zsh",
16
+ "fish",
17
+ "powershell",
18
+ "nu",
19
+ "nushell",
20
+ ];
21
+
22
+ const SUPPORTED_INSTALL_SHELL_LIST = SUPPORTED_INSTALL_SHELLS.join(", ");
23
+
24
+ export const MANAGED_COMPLETION_MARKER = [
25
+ "# Generated by `flywheel completion install`; do not edit this file directly.",
26
+ "# Re-run `flywheel completion install <shell>` to refresh it.",
27
+ ].join("\n");
28
+
29
+ const PROFILE_BLOCK_START = "# >>> flywheel completion >>>";
30
+ const PROFILE_BLOCK_OWNER = "# managed by `flywheel completion install`";
31
+ const PROFILE_BLOCK_END = "# <<< flywheel completion <<<";
32
+
33
+ function pathForPlatform(platform = process.platform) {
34
+ return platform === "win32" ? path.win32 : path;
35
+ }
36
+
37
+ function homeDir(env, platform) {
38
+ const home =
39
+ platform === "win32"
40
+ ? env.USERPROFILE || env.HOME
41
+ : env.HOME || env.USERPROFILE;
42
+ if (!home) {
43
+ throw new Error(
44
+ "HOME or USERPROFILE is required to resolve completion paths.",
45
+ );
46
+ }
47
+ return home;
48
+ }
49
+
50
+ function xdgDataHome(env, platform, pathApi) {
51
+ if (platform === "win32") {
52
+ return (
53
+ env.APPDATA || pathApi.join(homeDir(env, platform), "AppData", "Roaming")
54
+ );
55
+ }
56
+ return (
57
+ env.XDG_DATA_HOME || pathApi.join(homeDir(env, platform), ".local", "share")
58
+ );
59
+ }
60
+
61
+ function xdgConfigHome(env, platform, pathApi) {
62
+ if (platform === "win32") {
63
+ return (
64
+ env.APPDATA || pathApi.join(homeDir(env, platform), "AppData", "Roaming")
65
+ );
66
+ }
67
+ return env.XDG_CONFIG_HOME || pathApi.join(homeDir(env, platform), ".config");
68
+ }
69
+
70
+ function resolveDirectory(value, cwd, pathApi) {
71
+ return pathApi.isAbsolute(value) ? value : pathApi.resolve(cwd, value);
72
+ }
73
+
74
+ function pathsEqual(left, right, platform, pathApi) {
75
+ const normalizedLeft = pathApi.normalize(left);
76
+ const normalizedRight = pathApi.normalize(right);
77
+ return platform === "win32"
78
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
79
+ : normalizedLeft === normalizedRight;
80
+ }
81
+
82
+ function readFileIfPresent(filePath) {
83
+ return readFile(filePath, "utf8").catch((error) => {
84
+ if (error && error.code === "ENOENT") return null;
85
+ throw error;
86
+ });
87
+ }
88
+
89
+ function escapeDoubleQuotedShell(value) {
90
+ return String(value).replace(/(["\\$`])/g, "\\$1");
91
+ }
92
+
93
+ function escapeSingleQuoted(value) {
94
+ return String(value).replaceAll("'", "''");
95
+ }
96
+
97
+ function escapeFishSingleQuoted(value) {
98
+ return String(value).replace(/([\\'])/g, "\\$1");
99
+ }
100
+
101
+ function quoteNuString(value) {
102
+ const text = String(value);
103
+ let hashes = "#";
104
+ while (text.includes(`'${hashes}`)) {
105
+ hashes += "#";
106
+ }
107
+ return `r${hashes}'${text}'${hashes}`;
108
+ }
109
+
110
+ function ensureTrailingNewline(value) {
111
+ return value.endsWith("\n") ? value : `${value}\n`;
112
+ }
113
+
114
+ function quoteCliHintArgument(value) {
115
+ const text = String(value);
116
+ if (/^[A-Za-z0-9_/:=+.,@%-]+$/.test(text)) {
117
+ return text;
118
+ }
119
+ return `'${text.replaceAll("'", "'\\''")}'`;
120
+ }
121
+
122
+ function profileUpdateHintCommand({ shell, target, requestedDir }) {
123
+ const commandParts = [
124
+ "flywheel",
125
+ "completion",
126
+ "install",
127
+ quoteCliHintArgument(shell),
128
+ ];
129
+ if (requestedDir) {
130
+ commandParts.push("--dir", quoteCliHintArgument(target.dir));
131
+ }
132
+ commandParts.push("--update-profile", "--yes");
133
+ return commandParts.join(" ");
134
+ }
135
+
136
+ function renderProfileBlock(blockText) {
137
+ return [
138
+ PROFILE_BLOCK_START,
139
+ PROFILE_BLOCK_OWNER,
140
+ ensureTrailingNewline(blockText).trimEnd(),
141
+ PROFILE_BLOCK_END,
142
+ "",
143
+ ].join("\n");
144
+ }
145
+
146
+ async function writeFileAtomically(filePath, text, pathApi) {
147
+ const dir = pathApi.dirname(filePath);
148
+ const base = pathApi.basename(filePath);
149
+ const tempPath = pathApi.join(
150
+ dir,
151
+ `.${base}.${process.pid}.${Date.now()}.tmp`,
152
+ );
153
+ await mkdir(dir, { recursive: true });
154
+ try {
155
+ await writeFile(tempPath, text);
156
+ await rename(tempPath, filePath);
157
+ } catch (error) {
158
+ await rm(tempPath, { force: true }).catch(() => {});
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ async function applyProfileInstall({ profilePath, blockText, pathApi }) {
164
+ const existing = await readFileIfPresent(profilePath);
165
+ await mkdir(pathApi.dirname(profilePath), { recursive: true });
166
+ await writeFile(
167
+ profilePath,
168
+ upsertManagedProfileBlock(existing ?? "", blockText),
169
+ );
170
+ }
171
+
172
+ async function applyProfileUninstall(profilePath) {
173
+ const existing = await readFileIfPresent(profilePath);
174
+ if (existing === null) return;
175
+ await writeFile(profilePath, removeManagedProfileBlock(existing));
176
+ }
177
+
178
+ async function confirmProfileChange({
179
+ confirmProfileUpdate,
180
+ shell,
181
+ profilePath,
182
+ action,
183
+ }) {
184
+ if (!confirmProfileUpdate) {
185
+ return false;
186
+ }
187
+ return await confirmProfileUpdate({
188
+ shell,
189
+ profilePath,
190
+ action,
191
+ message:
192
+ action === "install"
193
+ ? `Update ${shell} profile file at ${profilePath}?`
194
+ : `Remove Flywheel completion block from ${profilePath}?`,
195
+ default: false,
196
+ });
197
+ }
198
+
199
+ function defaultWriters({ stdout, stderr }) {
200
+ return {
201
+ writeStdout: stdout ?? ((chunk) => process.stdout.write(chunk)),
202
+ writeStderr: stderr ?? ((chunk) => process.stderr.write(chunk)),
203
+ };
204
+ }
205
+
206
+ function generatorOptionsForShell(shell) {
207
+ return {
208
+ shell,
209
+ includeBashCompletionLib: shell === "bash",
210
+ };
211
+ }
212
+
213
+ function defaultCompletionInstallDir({ shell, env, platform, pathApi }) {
214
+ const dataHome = xdgDataHome(env, platform, pathApi);
215
+ const configHome = xdgConfigHome(env, platform, pathApi);
216
+ if (shell === "bash") {
217
+ return pathApi.join(
218
+ env.BASH_COMPLETION_USER_DIR || pathApi.join(dataHome, "bash-completion"),
219
+ "completions",
220
+ );
221
+ }
222
+ if (shell === "zsh") {
223
+ return pathApi.join(dataHome, "zsh", "site-functions");
224
+ }
225
+ if (shell === "fish") {
226
+ return pathApi.join(configHome, "fish", "completions");
227
+ }
228
+ if (shell === "powershell") {
229
+ return platform === "win32"
230
+ ? pathApi.join(dataHome, "Flywheel", "Completions")
231
+ : pathApi.join(dataHome, "flywheel", "completions");
232
+ }
233
+ return pathApi.join(configHome, "nushell", "autoload");
234
+ }
235
+
236
+ function resolveTargetAndProfile({ shell, dir, env, platform, cwd }) {
237
+ const target = resolveCompletionInstallTarget({
238
+ shell,
239
+ dir,
240
+ env,
241
+ platform,
242
+ cwd,
243
+ });
244
+ const profile = resolveProfileUpdate({
245
+ shell: target.shell,
246
+ completionPath: target.path,
247
+ completionDir: target.dir,
248
+ defaultTarget: target.defaultTarget,
249
+ env,
250
+ platform,
251
+ });
252
+ return { target, profile };
253
+ }
254
+
255
+ function assertNonInteractiveProfileAllowed({
256
+ updateProfile,
257
+ dryRun,
258
+ yes,
259
+ isInteractive,
260
+ profile,
261
+ }) {
262
+ if (
263
+ updateProfile === true &&
264
+ dryRun !== true &&
265
+ yes !== true &&
266
+ isInteractive !== true &&
267
+ profile.required
268
+ ) {
269
+ throw new Error("--update-profile requires --yes in non-interactive mode");
270
+ }
271
+ }
272
+
273
+ export function normalizeInstallShell(value) {
274
+ if (value === "nushell") {
275
+ return "nu";
276
+ }
277
+ if (SUPPORTED_INSTALL_SHELLS.includes(value)) {
278
+ return value;
279
+ }
280
+ throw new Error(
281
+ `Unsupported shell "${value}". Supported shells: ${SUPPORTED_INSTALL_SHELL_LIST}.`,
282
+ );
283
+ }
284
+
285
+ export function completionInstallFileName(shell) {
286
+ switch (normalizeInstallShell(shell)) {
287
+ case "bash":
288
+ return "flywheel";
289
+ case "zsh":
290
+ return "_flywheel";
291
+ case "fish":
292
+ return "flywheel.fish";
293
+ case "powershell":
294
+ return "flywheel.ps1";
295
+ case "nu":
296
+ return "flywheel.nu";
297
+ default:
298
+ throw new Error(`Unsupported shell "${shell}".`);
299
+ }
300
+ }
301
+
302
+ export function resolveCompletionInstallTarget({
303
+ shell,
304
+ dir,
305
+ env = process.env,
306
+ platform = process.platform,
307
+ cwd = process.cwd(),
308
+ } = {}) {
309
+ const normalizedShell = normalizeInstallShell(shell);
310
+ const pathApi = pathForPlatform(platform);
311
+ const fileName = completionInstallFileName(normalizedShell);
312
+ const defaultTargetDir = defaultCompletionInstallDir({
313
+ shell: normalizedShell,
314
+ env,
315
+ platform,
316
+ pathApi,
317
+ });
318
+ if (dir) {
319
+ const resolvedDir = resolveDirectory(String(dir), cwd, pathApi);
320
+ return {
321
+ shell: normalizedShell,
322
+ path: pathApi.join(resolvedDir, fileName),
323
+ dir: resolvedDir,
324
+ defaultTarget: pathsEqual(
325
+ resolvedDir,
326
+ defaultTargetDir,
327
+ platform,
328
+ pathApi,
329
+ ),
330
+ };
331
+ }
332
+
333
+ return {
334
+ shell: normalizedShell,
335
+ path: pathApi.join(defaultTargetDir, fileName),
336
+ dir: defaultTargetDir,
337
+ defaultTarget: true,
338
+ };
339
+ }
340
+
341
+ export function resolveProfileUpdate({
342
+ shell,
343
+ completionPath,
344
+ completionDir,
345
+ defaultTarget,
346
+ env = process.env,
347
+ platform = process.platform,
348
+ } = {}) {
349
+ const normalizedShell = normalizeInstallShell(shell);
350
+ const pathApi = pathForPlatform(platform);
351
+ const home = homeDir(env, platform);
352
+ const configHome = xdgConfigHome(env, platform, pathApi);
353
+
354
+ if (normalizedShell === "fish" && defaultTarget) {
355
+ return { required: false, message: "No profile update required for fish." };
356
+ }
357
+ if (normalizedShell === "fish") {
358
+ const escapedCompletionDir = escapeFishSingleQuoted(completionDir);
359
+ return {
360
+ required: true,
361
+ profilePath: pathApi.join(configHome, "fish", "config.fish"),
362
+ blockText: `if not contains -- '${escapedCompletionDir}' $fish_complete_path\n set -a fish_complete_path '${escapedCompletionDir}'\nend\n`,
363
+ };
364
+ }
365
+ if (normalizedShell === "nu" && defaultTarget) {
366
+ return {
367
+ required: false,
368
+ message: "No profile update required for nu autoload target.",
369
+ };
370
+ }
371
+ if (normalizedShell === "bash") {
372
+ const escaped = escapeDoubleQuotedShell(completionPath);
373
+ return {
374
+ required: true,
375
+ profilePath: pathApi.join(home, ".bashrc"),
376
+ blockText: `if [ -f "${escaped}" ]; then\n . "${escaped}"\nfi\n`,
377
+ };
378
+ }
379
+ if (normalizedShell === "zsh") {
380
+ return {
381
+ required: true,
382
+ profilePath: pathApi.join(env.ZDOTDIR || home, ".zshrc"),
383
+ blockText: `fpath=("${escapeDoubleQuotedShell(completionDir)}" $fpath)\nautoload -Uz compinit\ncompinit\n`,
384
+ };
385
+ }
386
+ if (normalizedShell === "powershell") {
387
+ const profilePath =
388
+ platform === "win32"
389
+ ? pathApi.join(home, "Documents", "PowerShell", "profile.ps1")
390
+ : pathApi.join(configHome, "powershell", "profile.ps1");
391
+ return {
392
+ required: true,
393
+ profilePath,
394
+ blockText: `. '${escapeSingleQuoted(completionPath)}'\n`,
395
+ };
396
+ }
397
+ return {
398
+ required: true,
399
+ profilePath:
400
+ platform === "win32"
401
+ ? pathApi.join(configHome, "nushell", "config.nu")
402
+ : pathApi.join(configHome, "nushell", "config.nu"),
403
+ blockText: `source ${quoteNuString(completionPath)}\n`,
404
+ };
405
+ }
406
+
407
+ export function buildManagedCompletionFile({ shell, generatedScript }) {
408
+ const normalizedShell = normalizeInstallShell(shell);
409
+ const script = ensureTrailingNewline(String(generatedScript));
410
+ if (normalizedShell !== "zsh" || !script.startsWith("#compdef")) {
411
+ return `${MANAGED_COMPLETION_MARKER}\n${script}`;
412
+ }
413
+ const firstLineEnd = script.indexOf("\n");
414
+ if (firstLineEnd === -1) {
415
+ return `${script}\n${MANAGED_COMPLETION_MARKER}\n`;
416
+ }
417
+ return `${script.slice(0, firstLineEnd + 1)}${MANAGED_COMPLETION_MARKER}\n${script.slice(firstLineEnd + 1)}`;
418
+ }
419
+
420
+ export function isManagedCompletionFile({ shell, text }) {
421
+ const normalizedShell = normalizeInstallShell(shell);
422
+ if (String(text).startsWith(MANAGED_COMPLETION_MARKER)) {
423
+ return true;
424
+ }
425
+ if (normalizedShell !== "zsh") {
426
+ return false;
427
+ }
428
+ const firstLineEnd = String(text).indexOf("\n");
429
+ if (firstLineEnd === -1) {
430
+ return false;
431
+ }
432
+ const firstLine = String(text).slice(0, firstLineEnd);
433
+ const remaining = String(text).slice(firstLineEnd + 1);
434
+ return (
435
+ firstLine.startsWith("#compdef") &&
436
+ remaining.startsWith(MANAGED_COMPLETION_MARKER)
437
+ );
438
+ }
439
+
440
+ export function upsertManagedProfileBlock(existingText, blockText) {
441
+ const renderedBlock = renderProfileBlock(blockText);
442
+ const start = existingText.indexOf(PROFILE_BLOCK_START);
443
+ if (start !== -1) {
444
+ const end = existingText.indexOf(PROFILE_BLOCK_END, start);
445
+ if (end === -1) {
446
+ throw new Error(
447
+ "Found an unterminated Flywheel completion profile block.",
448
+ );
449
+ }
450
+ let removeEnd = end + PROFILE_BLOCK_END.length;
451
+ if (existingText.slice(removeEnd, removeEnd + 2) === "\r\n") {
452
+ removeEnd += 2;
453
+ } else if (existingText[removeEnd] === "\n") {
454
+ removeEnd += 1;
455
+ }
456
+ return `${existingText.slice(0, start)}${renderedBlock}${existingText.slice(removeEnd)}`;
457
+ }
458
+
459
+ const separator =
460
+ existingText === "" || existingText.endsWith("\n") ? "" : "\n";
461
+ return `${existingText}${separator}${renderedBlock}`;
462
+ }
463
+
464
+ export function removeManagedProfileBlock(existingText) {
465
+ const start = existingText.indexOf(PROFILE_BLOCK_START);
466
+ if (start === -1) {
467
+ return existingText;
468
+ }
469
+ const end = existingText.indexOf(PROFILE_BLOCK_END, start);
470
+ if (end === -1) {
471
+ throw new Error("Found an unterminated Flywheel completion profile block.");
472
+ }
473
+ let removeEnd = end + PROFILE_BLOCK_END.length;
474
+ if (existingText.slice(removeEnd, removeEnd + 2) === "\r\n") {
475
+ removeEnd += 2;
476
+ } else if (existingText[removeEnd] === "\n") {
477
+ removeEnd += 1;
478
+ }
479
+ return `${existingText.slice(0, start)}${existingText.slice(removeEnd)}`;
480
+ }
481
+
482
+ export async function installCompletion({
483
+ shell,
484
+ dir,
485
+ force = false,
486
+ dryRun = false,
487
+ updateProfile = false,
488
+ yes = false,
489
+ isInteractive = process.stdin.isTTY === true && process.stdout.isTTY === true,
490
+ env = process.env,
491
+ platform = process.platform,
492
+ cwd = process.cwd(),
493
+ stdout,
494
+ stderr,
495
+ confirmProfileUpdate,
496
+ generateCompletionScript = generateUsageCompletionScript,
497
+ } = {}) {
498
+ const normalizedShell = normalizeInstallShell(shell);
499
+ const pathApi = pathForPlatform(platform);
500
+ const { writeStderr } = defaultWriters({ stdout, stderr });
501
+ const { target, profile } = resolveTargetAndProfile({
502
+ shell: normalizedShell,
503
+ dir,
504
+ env,
505
+ platform,
506
+ cwd,
507
+ });
508
+
509
+ assertNonInteractiveProfileAllowed({
510
+ updateProfile,
511
+ dryRun,
512
+ yes,
513
+ isInteractive,
514
+ profile,
515
+ });
516
+
517
+ const existing = await readFileIfPresent(target.path);
518
+ if (
519
+ existing !== null &&
520
+ !isManagedCompletionFile({ shell: normalizedShell, text: existing }) &&
521
+ force !== true
522
+ ) {
523
+ throw new Error(
524
+ `Completion file already exists and is not managed by Flywheel: ${target.path}. Re-run with --force to replace it.`,
525
+ );
526
+ }
527
+
528
+ const generatedScript = await generateCompletionScript(
529
+ generatorOptionsForShell(normalizedShell),
530
+ );
531
+ const managedText = buildManagedCompletionFile({
532
+ shell: normalizedShell,
533
+ generatedScript,
534
+ });
535
+
536
+ if (dryRun) {
537
+ writeStderr(
538
+ `Would install ${normalizedShell} completion to ${target.path}\n`,
539
+ );
540
+ if (updateProfile && profile.required) {
541
+ writeStderr(`Would update profile file ${profile.profilePath}\n`);
542
+ } else if (updateProfile && !profile.required) {
543
+ writeStderr(`${profile.message}\n`);
544
+ }
545
+ return { shell: normalizedShell, path: target.path, dryRun: true };
546
+ }
547
+
548
+ await writeFileAtomically(target.path, managedText, pathApi);
549
+ writeStderr(`Installed ${normalizedShell} completion to ${target.path}\n`);
550
+
551
+ if (!updateProfile) {
552
+ if (profile.required) {
553
+ const command = profileUpdateHintCommand({
554
+ shell,
555
+ target,
556
+ requestedDir: dir,
557
+ });
558
+ writeStderr(
559
+ `Profile not updated. To enable profile wiring, run: ${command}\n`,
560
+ );
561
+ }
562
+ return { shell: normalizedShell, path: target.path };
563
+ }
564
+
565
+ if (!profile.required) {
566
+ writeStderr(`${profile.message}\n`);
567
+ return { shell: normalizedShell, path: target.path };
568
+ }
569
+
570
+ const shouldUpdate =
571
+ yes === true ||
572
+ (isInteractive === true &&
573
+ (await confirmProfileChange({
574
+ confirmProfileUpdate,
575
+ shell: normalizedShell,
576
+ profilePath: profile.profilePath,
577
+ action: "install",
578
+ })));
579
+ if (shouldUpdate) {
580
+ await applyProfileInstall({ ...profile, pathApi });
581
+ writeStderr(
582
+ `Updated ${normalizedShell} profile file at ${profile.profilePath}\n`,
583
+ );
584
+ } else {
585
+ writeStderr(`Profile not updated: ${profile.profilePath}\n`);
586
+ }
587
+ return { shell: normalizedShell, path: target.path };
588
+ }
589
+
590
+ export async function uninstallCompletion({
591
+ shell,
592
+ dir,
593
+ dryRun = false,
594
+ updateProfile = false,
595
+ yes = false,
596
+ isInteractive = process.stdin.isTTY === true && process.stdout.isTTY === true,
597
+ env = process.env,
598
+ platform = process.platform,
599
+ cwd = process.cwd(),
600
+ stdout,
601
+ stderr,
602
+ confirmProfileUpdate,
603
+ } = {}) {
604
+ const normalizedShell = normalizeInstallShell(shell);
605
+ const { writeStderr } = defaultWriters({ stdout, stderr });
606
+ const { target, profile } = resolveTargetAndProfile({
607
+ shell: normalizedShell,
608
+ dir,
609
+ env,
610
+ platform,
611
+ cwd,
612
+ });
613
+
614
+ assertNonInteractiveProfileAllowed({
615
+ updateProfile,
616
+ dryRun,
617
+ yes,
618
+ isInteractive,
619
+ profile,
620
+ });
621
+
622
+ const existing = await readFileIfPresent(target.path);
623
+ if (
624
+ existing !== null &&
625
+ !isManagedCompletionFile({ shell: normalizedShell, text: existing })
626
+ ) {
627
+ throw new Error(
628
+ `Completion file exists but is not managed by Flywheel: ${target.path}. Refusing to remove it.`,
629
+ );
630
+ }
631
+
632
+ if (dryRun) {
633
+ writeStderr(
634
+ `Would remove ${normalizedShell} completion at ${target.path}\n`,
635
+ );
636
+ if (updateProfile && profile.required) {
637
+ writeStderr(`Would remove profile block from ${profile.profilePath}\n`);
638
+ } else if (updateProfile && !profile.required) {
639
+ writeStderr(`${profile.message}\n`);
640
+ }
641
+ return { shell: normalizedShell, path: target.path, dryRun: true };
642
+ }
643
+
644
+ if (existing !== null) {
645
+ await unlink(target.path);
646
+ writeStderr(`Removed ${normalizedShell} completion at ${target.path}\n`);
647
+ } else {
648
+ writeStderr(
649
+ `No managed ${normalizedShell} completion found at ${target.path}\n`,
650
+ );
651
+ }
652
+
653
+ if (!updateProfile) {
654
+ return { shell: normalizedShell, path: target.path };
655
+ }
656
+
657
+ if (!profile.required) {
658
+ writeStderr(`${profile.message}\n`);
659
+ return { shell: normalizedShell, path: target.path };
660
+ }
661
+
662
+ const shouldUpdate =
663
+ yes === true ||
664
+ (isInteractive === true &&
665
+ (await confirmProfileChange({
666
+ confirmProfileUpdate,
667
+ shell: normalizedShell,
668
+ profilePath: profile.profilePath,
669
+ action: "uninstall",
670
+ })));
671
+ if (shouldUpdate) {
672
+ await applyProfileUninstall(profile.profilePath);
673
+ writeStderr(
674
+ `Removed Flywheel completion block from ${profile.profilePath}\n`,
675
+ );
676
+ } else {
677
+ writeStderr(`Profile not updated: ${profile.profilePath}\n`);
678
+ }
679
+ return { shell: normalizedShell, path: target.path };
680
+ }