magus-cli 7.6.0 → 7.8.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.
Files changed (83) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/blackbox-parity.test.ts +29 -15
  3. package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
  4. package/src/__tests__/blind-780-cli-tools.test.ts +808 -0
  5. package/src/__tests__/blind-780-deprecated.test.ts +631 -0
  6. package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
  7. package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
  8. package/src/__tests__/blind-780-review2.test.ts +1203 -0
  9. package/src/__tests__/cli-update-view.test.ts +3 -3
  10. package/src/__tests__/deprecated-remove.test.ts +827 -0
  11. package/src/__tests__/doctor.test.ts +159 -1
  12. package/src/__tests__/failure-popup-review2.test.tsx +920 -0
  13. package/src/__tests__/failure-popup.test.tsx +419 -0
  14. package/src/__tests__/final-fixes-780.test.tsx +724 -0
  15. package/src/__tests__/format-update-outcome.test.ts +9 -34
  16. package/src/__tests__/gitignore-profile.test.ts +0 -4
  17. package/src/__tests__/models-screen-state.test.ts +18 -40
  18. package/src/__tests__/plugin-resolution.test.ts +52 -13
  19. package/src/__tests__/plugin-row-text.test.ts +30 -37
  20. package/src/__tests__/profile-dependencies.test.ts +7 -0
  21. package/src/__tests__/profile-startup-old-format.test.ts +0 -3
  22. package/src/__tests__/profile-startup.test.ts +40 -12
  23. package/src/__tests__/stale-load-repair.test.ts +31 -23
  24. package/src/__tests__/update-apply.test.ts +13 -29
  25. package/src/__tests__/update-plan.test.ts +23 -17
  26. package/src/__tests__/useStaleLoadModal.test.ts +1 -42
  27. package/src/__tests__/version-unknown-state.test.ts +15 -17
  28. package/src/cli/doctor.ts +151 -30
  29. package/src/cli/install.ts +5 -6
  30. package/src/cli/update-view.ts +58 -15
  31. package/src/cli/update.ts +27 -5
  32. package/src/services/catalog-cache-store.ts +8 -6
  33. package/src/services/claude-cli.ts +106 -19
  34. package/src/services/claude-settings.ts +84 -36
  35. package/src/services/gitignore-service.ts +0 -19
  36. package/src/services/plugin-env-needs.ts +69 -0
  37. package/src/services/plugin-manager.ts +31 -19
  38. package/src/services/plugin-resolution.ts +34 -42
  39. package/src/services/profile-absorb.ts +9 -0
  40. package/src/services/profile-activate.ts +1 -1
  41. package/src/services/profile-startup.ts +30 -27
  42. package/src/services/profile-state.ts +81 -2
  43. package/src/services/resolver.ts +95 -1
  44. package/src/services/update-engine.ts +326 -31
  45. package/src/services/update-plan.ts +293 -33
  46. package/src/types/index.ts +7 -0
  47. package/src/ui/App.tsx +186 -123
  48. package/src/ui/adapters/cliToolsAdapter.ts +203 -1
  49. package/src/ui/adapters/failureReport.ts +137 -0
  50. package/src/ui/adapters/pluginsAdapter.ts +299 -28
  51. package/src/ui/adapters/startupReports.ts +282 -0
  52. package/src/ui/components/modals/FailureListModal.tsx +231 -0
  53. package/src/ui/components/modals/MessageModal.tsx +7 -13
  54. package/src/ui/components/modals/ModalContainer.tsx +10 -5
  55. package/src/ui/components/modals/index.ts +1 -0
  56. package/src/ui/hooks/startupQueue.ts +56 -0
  57. package/src/ui/hooks/useGitignoreModal.ts +31 -31
  58. package/src/ui/hooks/useProfileStartupModal.ts +127 -122
  59. package/src/ui/hooks/useStaleLoadModal.ts +33 -53
  60. package/src/ui/keymap.ts +14 -1
  61. package/src/ui/renderers/mcpRenderers.tsx +110 -3
  62. package/src/ui/renderers/pluginRenderers.tsx +77 -6
  63. package/src/ui/renderers/pluginRowText.ts +25 -25
  64. package/src/ui/renderers/pluginStatusText.ts +18 -1
  65. package/src/ui/screens/AliasScreen.tsx +38 -44
  66. package/src/ui/screens/CliToolsScreen.tsx +65 -51
  67. package/src/ui/screens/EnvVarsScreen.tsx +21 -6
  68. package/src/ui/screens/GitignoreScreen.tsx +49 -75
  69. package/src/ui/screens/McpRegistryScreen.tsx +72 -40
  70. package/src/ui/screens/McpScreen.tsx +55 -24
  71. package/src/ui/screens/ModelsScreen.tsx +46 -86
  72. package/src/ui/screens/PluginsScreen.tsx +316 -245
  73. package/src/ui/screens/ProfilesScreen.tsx +88 -73
  74. package/src/ui/screens/SkillsScreen.tsx +83 -80
  75. package/src/ui/screens/StylesScreen.tsx +211 -275
  76. package/src/ui/state/AppContext.tsx +72 -7
  77. package/src/ui/state/DimensionsContext.tsx +3 -22
  78. package/src/ui/state/reducer.ts +25 -14
  79. package/src/ui/state/types.ts +61 -31
  80. package/src/utils/user-path.ts +60 -0
  81. package/src/__tests__/styles-status-line.test.ts +0 -72
  82. package/src/__tests__/version-snapshot.test.ts +0 -212
  83. package/src/services/version-snapshot.ts +0 -189
@@ -0,0 +1,231 @@
1
+ import type { FailureReport, FailureRow } from "../../state/types.js";
2
+ import { theme } from "../../theme.js";
3
+
4
+ /** Border (2) and horizontal padding (3 + 3). */
5
+ const CHROME = 8;
6
+ const MAX_WIDTH = 88;
7
+ const MIN_WIDTH = 40;
8
+ /** "✗ " before each row's subject. */
9
+ const MARK = "✗ ";
10
+ /**
11
+ * Rows of the popup that are not failure rows: border (2), padding (2), the
12
+ * headline and its gap (2), the footer and its gap (2), and a row of air
13
+ * between the popup and the terminal's edge (2).
14
+ */
15
+ const FIXED_ROWS = 10;
16
+
17
+ /**
18
+ * The popup's width for a terminal `terminalWidth` columns wide: as wide as the
19
+ * rows need, never wider than the terminal leaves room for.
20
+ */
21
+ export function failureListWidth(terminalWidth: number): number {
22
+ return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, terminalWidth - 4));
23
+ }
24
+
25
+ /**
26
+ * Whether a row's reason goes on its own line under the subject. The subject
27
+ * never wraps — it is what the user scans for — so a long one (several plugins
28
+ * sharing a reason) takes the line to itself and the reason goes beneath it,
29
+ * rather than being squeezed into a sliver beside it.
30
+ */
31
+ export function reasonBelow(subject: string, innerWidth: number): boolean {
32
+ return MARK.length + subject.length + 2 > innerWidth / 2;
33
+ }
34
+
35
+ /** Lines `text` takes when word-wrapped at `width` columns. */
36
+ function wrappedLines(text: string, width: number): number {
37
+ if (width <= 0) return text.length;
38
+ let lines = 1;
39
+ let used = 0;
40
+ for (const word of text.split(/\s+/).filter(Boolean)) {
41
+ const len = word.length;
42
+ if (used === 0) {
43
+ used = len;
44
+ } else if (used + 1 + len <= width) {
45
+ used += 1 + len;
46
+ } else {
47
+ lines++;
48
+ used = len;
49
+ }
50
+ while (used > width) {
51
+ lines++;
52
+ used -= width;
53
+ }
54
+ }
55
+ return lines;
56
+ }
57
+
58
+ /** Terminal rows one failure row takes at an inner width of `inner`. */
59
+ function rowHeight(row: FailureRow, inner: number): number {
60
+ if (reasonBelow(row.subject, inner)) {
61
+ const column = inner - MARK.length;
62
+ return (
63
+ 1 +
64
+ wrappedLines(row.reason, column) +
65
+ (row.fix ? wrappedLines(row.fix, column) : 0)
66
+ );
67
+ }
68
+ const column = inner - (MARK.length + row.subject.length + 2);
69
+ return (
70
+ wrappedLines(row.reason, column) +
71
+ (row.fix ? wrappedLines(row.fix, column) : 0)
72
+ );
73
+ }
74
+
75
+ /**
76
+ * The rows that fit a terminal `terminalHeight` rows tall, and how many did
77
+ * not. Without a height every row is drawn. A popup taller than the terminal
78
+ * would push its own headline and footer off screen, so it stops early and
79
+ * says `+N more` instead — and always shows at least one row.
80
+ */
81
+ export function visibleFailureRows(
82
+ report: FailureReport,
83
+ terminalWidth: number,
84
+ terminalHeight?: number,
85
+ ): { rows: FailureRow[]; hidden: number } {
86
+ if (terminalHeight === undefined) return { rows: report.rows, hidden: 0 };
87
+ const inner = failureListWidth(terminalWidth) - CHROME;
88
+ const fixRows = report.fix ? 1 + wrappedLines(report.fix, inner) : 0;
89
+ const budget = terminalHeight - FIXED_ROWS - fixRows;
90
+ const total = report.rows.reduce(
91
+ (sum, row) => sum + rowHeight(row, inner),
92
+ 0,
93
+ );
94
+ if (total <= budget) return { rows: report.rows, hidden: 0 };
95
+
96
+ // One row is kept back for the "+N more" line.
97
+ const rows: FailureRow[] = [];
98
+ let used = 0;
99
+ for (const row of report.rows) {
100
+ const height = rowHeight(row, inner);
101
+ if (rows.length > 0 && used + height > budget - 1) break;
102
+ rows.push(row);
103
+ used += height;
104
+ }
105
+ return { rows, hidden: report.rows.length - rows.length };
106
+ }
107
+
108
+ /**
109
+ * A row's reason and, when it has its own, its fix beneath it. A plain call,
110
+ * not a component, so the whole popup is one element tree.
111
+ */
112
+ function reasonColumn(row: FailureRow) {
113
+ return (
114
+ <box flexDirection="column" flexGrow={1} flexShrink={1}>
115
+ <text fg={theme.colors.muted} wrapMode="word">
116
+ {row.reason}
117
+ </text>
118
+ {row.fix ? (
119
+ <text fg={theme.colors.text} wrapMode="word">
120
+ {row.fix}
121
+ </text>
122
+ ) : null}
123
+ </box>
124
+ );
125
+ }
126
+
127
+ /**
128
+ * THE failure popup: what did not happen, counted; each thing that failed with
129
+ * why; and the one fix, when there is one. The shape is {@link FailureReport},
130
+ * built by a pure function per screen, so every failure reads the same way.
131
+ *
132
+ * Theme tokens only: the ✗ in danger, the subject in the text colour, the
133
+ * reason muted — the reason is the detail, the subject is what the user scans.
134
+ * A fix is in the text colour, under its row when only that row has it, or
135
+ * once under all of them. Reasons wrap; subjects do not. Given the terminal's
136
+ * height, it draws only the rows that fit and ends with `+N more`. Any key
137
+ * dismisses it (ModalContainer).
138
+ */
139
+ export function FailureListModal({
140
+ report,
141
+ terminalWidth,
142
+ terminalHeight,
143
+ }: {
144
+ report: FailureReport;
145
+ terminalWidth: number;
146
+ /** Cap the rows to this many terminal rows; every row is drawn without it. */
147
+ terminalHeight?: number;
148
+ }) {
149
+ const width = failureListWidth(terminalWidth);
150
+ const inner = width - CHROME;
151
+ const { rows, hidden } = visibleFailureRows(
152
+ report,
153
+ terminalWidth,
154
+ terminalHeight,
155
+ );
156
+
157
+ return (
158
+ <box
159
+ flexDirection="column"
160
+ border
161
+ borderStyle="rounded"
162
+ borderColor={theme.colors.dim}
163
+ backgroundColor={theme.surface.bg}
164
+ paddingLeft={3}
165
+ paddingRight={3}
166
+ paddingTop={1}
167
+ paddingBottom={1}
168
+ width={width}
169
+ >
170
+ <box marginBottom={1}>
171
+ <text fg={theme.colors.text}>
172
+ <span fg={theme.colors.danger}>{MARK}</span>
173
+ <strong>{report.headline}</strong>
174
+ </text>
175
+ </box>
176
+
177
+ {/*
178
+ * The index is part of the key: a report built with `group: false`
179
+ * keeps rows apart even when subject and reason match, and a repeated
180
+ * key makes React drop or reuse one of them.
181
+ */}
182
+ {rows.map((row, index) =>
183
+ reasonBelow(row.subject, inner) ? (
184
+ <box
185
+ key={`${row.subject}:${row.reason}:${index}`}
186
+ flexDirection="column"
187
+ >
188
+ <text fg={theme.colors.text}>
189
+ <span fg={theme.colors.danger}>{MARK}</span>
190
+ <span fg={theme.colors.text}>{row.subject}</span>
191
+ </text>
192
+ <box paddingLeft={MARK.length}>{reasonColumn(row)}</box>
193
+ </box>
194
+ ) : (
195
+ <box
196
+ key={`${row.subject}:${row.reason}:${index}`}
197
+ flexDirection="row"
198
+ >
199
+ <box flexShrink={0}>
200
+ <text fg={theme.colors.text}>
201
+ <span fg={theme.colors.danger}>{MARK}</span>
202
+ <span fg={theme.colors.text}>{`${row.subject} `}</span>
203
+ </text>
204
+ </box>
205
+ {reasonColumn(row)}
206
+ </box>
207
+ ),
208
+ )}
209
+
210
+ {hidden > 0 ? (
211
+ <box paddingLeft={MARK.length}>
212
+ <text fg={theme.colors.muted}>{`+${hidden} more`}</text>
213
+ </box>
214
+ ) : null}
215
+
216
+ {report.fix ? (
217
+ <box marginTop={1}>
218
+ <text fg={theme.colors.text} wrapMode="word">
219
+ {report.fix}
220
+ </text>
221
+ </box>
222
+ ) : null}
223
+
224
+ <box marginTop={1}>
225
+ <text fg={theme.colors.muted}>Press any key to continue</text>
226
+ </box>
227
+ </box>
228
+ );
229
+ }
230
+
231
+ export default FailureListModal;
@@ -6,20 +6,14 @@ interface MessageModalProps {
6
6
  title: string;
7
7
  /** Modal message */
8
8
  message: string;
9
- /** Message variant */
10
- variant: "info" | "success" | "error";
11
9
  }
12
10
 
13
- const variantConfig = {
14
- info: { icon: "ℹ", color: theme.colors.info },
15
- success: { icon: "✓", color: theme.colors.success },
16
- error: { icon: "✗", color: theme.colors.danger },
17
- } as const;
18
-
19
- /** Draws the message. Any key dismisses it — ModalContainer handles that. */
20
- export function MessageModal({ title, message, variant }: MessageModalProps) {
21
- const config = variantConfig[variant];
22
-
11
+ /**
12
+ * Draws information to read: help text, a command to run. Never a failure —
13
+ * those go in `FailureListModal`. Any key dismisses it — ModalContainer
14
+ * handles that.
15
+ */
16
+ export function MessageModal({ title, message }: MessageModalProps) {
23
17
  return (
24
18
  <box
25
19
  flexDirection="column"
@@ -34,7 +28,7 @@ export function MessageModal({ title, message, variant }: MessageModalProps) {
34
28
  width={60}
35
29
  >
36
30
  <box marginBottom={1}>
37
- <text fg={config.color}>{config.icon} </text>
31
+ <text fg={theme.colors.info}>ℹ </text>
38
32
  <text fg={theme.colors.text}>
39
33
  <strong>{title}</strong>
40
34
  </text>
@@ -5,6 +5,7 @@ import { type KeyPress, isScrollKey } from "../../input/key-dispatch.js";
5
5
  import { useApp, useModal } from "../../state/AppContext.js";
6
6
  import { useDimensions } from "../../state/DimensionsContext.js";
7
7
  import { ConfirmModal } from "./ConfirmModal.js";
8
+ import { FailureListModal } from "./FailureListModal.js";
8
9
  import { HelpOverlay, helpOverlaySize } from "./HelpOverlay.js";
9
10
  import { InputModal } from "./InputModal.js";
10
11
  import { LoadingModal } from "./LoadingModal.js";
@@ -37,7 +38,7 @@ const isPlain = (key: KeyPress) => !key.ctrl && !key.meta;
37
38
  * Only an <input> that was ALREADY focused when the key arrived:
38
39
  * the Enter that opens an input modal from a select never reaches
39
40
  * it (KeyRouter's `dispatchThenGuardFocus`).
40
- * - message: any key dismisses.
41
+ * - message, failures: any key dismisses.
41
42
  * - loading: keys are swallowed until the work finishes.
42
43
  * - help: ↑↓ / PgUp / PgDn / Ctrl+U / Ctrl+D scroll; Esc or `?` close.
43
44
  */
@@ -102,6 +103,7 @@ export function ModalContainer() {
102
103
  break;
103
104
 
104
105
  case "message":
106
+ case "failures":
105
107
  modal.onDismiss();
106
108
  break;
107
109
 
@@ -174,11 +176,14 @@ export function ModalContainer() {
174
176
  );
175
177
 
176
178
  case "message":
179
+ return <MessageModal title={modal.title} message={modal.message} />;
180
+
181
+ case "failures":
177
182
  return (
178
- <MessageModal
179
- title={modal.title}
180
- message={modal.message}
181
- variant={modal.variant}
183
+ <FailureListModal
184
+ report={modal.report}
185
+ terminalWidth={dimensions.terminalWidth}
186
+ terminalHeight={dimensions.terminalHeight}
182
187
  />
183
188
  );
184
189
 
@@ -2,5 +2,6 @@ export { ConfirmModal } from "./ConfirmModal.js";
2
2
  export { InputModal } from "./InputModal.js";
3
3
  export { SelectModal } from "./SelectModal.js";
4
4
  export { MessageModal } from "./MessageModal.js";
5
+ export { FailureListModal } from "./FailureListModal.js";
5
6
  export { LoadingModal } from "./LoadingModal.js";
6
7
  export { ModalContainer } from "./ModalContainer.js";
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What the app shows next at startup, when the modal slot is free — the ONE
3
+ * order every startup popup and question is asked in. Pure, so the order is
4
+ * tested without mounting the app; `App.tsx`'s startup effect acts on it.
5
+ *
6
+ * The order:
7
+ *
8
+ * 1. Failure reports, oldest first. A failure is something that already
9
+ * happened, and the question after it may be answered differently once
10
+ * it is known — so the profile gate's refusals, queued in the same tick as
11
+ * the profile question, are read BEFORE it and never replace it.
12
+ * 2. The gitignore question, once no newer check is pending.
13
+ * 3. The broken-plugins question.
14
+ * 4. The profile question — last, and asked again whenever the slot is free
15
+ * and it has not been answered (it cannot be dismissed).
16
+ *
17
+ * Nothing is shown while any modal is open: two needs arriving in one commit
18
+ * would otherwise both see an empty slot, and the later would replace the
19
+ * earlier after its need was cleared.
20
+ */
21
+
22
+ import type { FailureReport } from "../state/types.js";
23
+
24
+ export interface StartupQueueState<ProfileNeed> {
25
+ /** A modal (any modal) is on screen. */
26
+ modalOpen: boolean;
27
+ /** Failure reports not yet shown, oldest first. */
28
+ reports: readonly FailureReport[];
29
+ /** A gitignore answer is waiting and no newer check is running. */
30
+ gitignoreReady: boolean;
31
+ /** Plugins found loading an older version than installed, or null. */
32
+ brokenCount: number | null;
33
+ /** The unanswered profile question, or null. */
34
+ profileNeed: ProfileNeed | null;
35
+ }
36
+
37
+ export type StartupStep<ProfileNeed> =
38
+ | { kind: "report"; report: FailureReport; rest: FailureReport[] }
39
+ | { kind: "gitignore" }
40
+ | { kind: "stale-load"; brokenCount: number }
41
+ | { kind: "profile"; need: ProfileNeed }
42
+ | null;
43
+
44
+ export function nextStartupStep<ProfileNeed>(
45
+ s: StartupQueueState<ProfileNeed>,
46
+ ): StartupStep<ProfileNeed> {
47
+ if (s.modalOpen) return null;
48
+ const [report, ...rest] = s.reports;
49
+ if (report) return { kind: "report", report, rest };
50
+ if (s.gitignoreReady) return { kind: "gitignore" };
51
+ if (s.brokenCount !== null) {
52
+ return { kind: "stale-load", brokenCount: s.brokenCount };
53
+ }
54
+ if (s.profileNeed !== null) return { kind: "profile", need: s.profileNeed };
55
+ return null;
56
+ }
@@ -1,6 +1,7 @@
1
1
  import { useCallback } from "react";
2
+ import { stepFailureReport } from "../adapters/startupReports.js";
2
3
  import { useApp, useNavigation } from "../state/AppContext.js";
3
- import type { ModalState } from "../state/types.js";
4
+ import type { FailureReport, ModalState } from "../state/types.js";
4
5
 
5
6
  /**
6
7
  * Startup gitignore-violation modal: magus launches, detects a problem, and
@@ -88,8 +89,16 @@ export function useGitignoreModal() {
88
89
  violationCount: number;
89
90
  safeCount: number;
90
91
  generated?: readonly string[];
91
- /** Returns what it did, shown before returning to the app. */
92
- onFixSafe: () => Promise<string | undefined> | string | undefined;
92
+ /**
93
+ * Runs the fix and answers what it could not do — null when all of it
94
+ * applied. The Git State tab shows what changed; failures go into the
95
+ * one failure popup.
96
+ */
97
+ onFixSafe: () =>
98
+ | Promise<FailureReport | null | undefined>
99
+ | FailureReport
100
+ | null
101
+ | undefined;
93
102
  }) => {
94
103
  if (args.violationCount === 0) return;
95
104
  const dismiss = () => dispatch({ type: "HIDE_MODAL" });
@@ -101,9 +110,9 @@ export function useGitignoreModal() {
101
110
  // A loading modal, NOT HIDE_MODAL. The fix is async, and an empty
102
111
  // modal slot is App's signal that the next startup question may be
103
112
  // asked: hiding here let the profile question show during the await,
104
- // be marked as asked, and then be replaced by "Gitignore fixed" —
113
+ // be marked as asked, and then be replaced by the fix's result —
105
114
  // lost for the whole session. The slot stays occupied until the
106
- // result below is dismissed.
115
+ // fix settles.
107
116
  dispatch({
108
117
  type: "SHOW_MODAL",
109
118
  modal: { type: "loading", message: "Fixing gitignore…" },
@@ -111,37 +120,28 @@ export function useGitignoreModal() {
111
120
  // Called inside `.then`, not passed to `Promise.resolve`: a fixer
112
121
  // that throws synchronously must land in `.catch` below too, or
113
122
  // the loading modal above stays up and swallows every key.
123
+ const fail = (report: FailureReport) =>
124
+ dispatch({
125
+ type: "SHOW_MODAL",
126
+ modal: { type: "failures", report, onDismiss: dismiss },
127
+ });
114
128
  void Promise.resolve()
115
129
  .then(() => args.onFixSafe())
116
130
  .then((report) => {
117
- // Nothing to report still has to clear the loading modal: it
118
- // swallows every key, so leaving it would hang the app.
119
- if (!report) {
120
- dispatch({ type: "HIDE_MODAL" });
121
- return;
122
- }
123
- dispatch({
124
- type: "SHOW_MODAL",
125
- modal: {
126
- type: "message",
127
- title: "Gitignore fixed",
128
- message: report,
129
- variant: "success",
130
- onDismiss: dismiss,
131
- },
132
- });
131
+ // The loading modal must go — it swallows every key — and the
132
+ // empty slot lets the next startup question in. What the fix
133
+ // could not do takes the slot first, in the failure popup.
134
+ if (report) fail(report);
135
+ else dispatch({ type: "HIDE_MODAL" });
133
136
  })
134
137
  .catch((err) => {
135
- dispatch({
136
- type: "SHOW_MODAL",
137
- modal: {
138
- type: "message",
139
- title: "Safe fixes failed",
140
- message: err instanceof Error ? err.message : String(err),
141
- variant: "error",
142
- onDismiss: dismiss,
143
- },
144
- });
138
+ fail(
139
+ stepFailureReport(
140
+ "The gitignore fixes were not applied",
141
+ ".gitignore",
142
+ err,
143
+ ),
144
+ );
145
145
  });
146
146
  },
147
147
  onOpenTab: () => {