@aikidosec/safe-chain 1.0.15 → 1.0.16

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 (28) hide show
  1. package/README.md +10 -8
  2. package/bin/aikido-pnpm.js +8 -0
  3. package/bin/aikido-pnpx.js +8 -0
  4. package/docs/shell-integration.md +4 -4
  5. package/package.json +3 -1
  6. package/src/packagemanager/_shared/matchesCommand.js +13 -0
  7. package/src/packagemanager/currentPackageManager.js +8 -0
  8. package/src/packagemanager/pnpm/createPackageManager.js +46 -0
  9. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +28 -0
  10. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +88 -0
  11. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.spec.js +138 -0
  12. package/src/packagemanager/pnpm/runPnpmCommand.js +24 -0
  13. package/src/shell-integration/helpers.js +31 -31
  14. package/src/shell-integration/setup.js +26 -102
  15. package/src/shell-integration/shellDetection.js +17 -66
  16. package/src/shell-integration/supported-shells/bash.js +58 -0
  17. package/src/shell-integration/supported-shells/bash.spec.js +199 -0
  18. package/src/shell-integration/supported-shells/fish.js +61 -0
  19. package/src/shell-integration/supported-shells/fish.spec.js +199 -0
  20. package/src/shell-integration/supported-shells/powershell.js +61 -0
  21. package/src/shell-integration/supported-shells/powershell.spec.js +204 -0
  22. package/src/shell-integration/supported-shells/windowsPowershell.js +61 -0
  23. package/src/shell-integration/supported-shells/windowsPowershell.spec.js +204 -0
  24. package/src/shell-integration/supported-shells/zsh.js +58 -0
  25. package/src/shell-integration/supported-shells/zsh.spec.js +199 -0
  26. package/src/shell-integration/teardown.js +20 -99
  27. package/src/shell-integration/setup.spec.js +0 -304
  28. package/src/shell-integration/teardown.spec.js +0 -177
@@ -0,0 +1,204 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import path from "path";
6
+ import { knownAikidoTools } from "../helpers.js";
7
+
8
+ describe("Windows PowerShell shell integration", () => {
9
+ let mockStartupFile;
10
+ let windowsPowershell;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(
15
+ tmpdir(),
16
+ `test-windows-powershell-profile-${Date.now()}.ps1`
17
+ );
18
+
19
+ // Mock the helpers module
20
+ mock.module("../helpers.js", {
21
+ namedExports: {
22
+ doesExecutableExistOnSystem: () => true,
23
+ addLineToFile: (filePath, line) => {
24
+ if (!fs.existsSync(filePath)) {
25
+ fs.writeFileSync(filePath, "", "utf-8");
26
+ }
27
+ fs.appendFileSync(filePath, line + "\n", "utf-8");
28
+ },
29
+ removeLinesMatchingPattern: (filePath, pattern) => {
30
+ if (!fs.existsSync(filePath)) return;
31
+ const content = fs.readFileSync(filePath, "utf-8");
32
+ const lines = content.split("\n");
33
+ const filteredLines = lines.filter((line) => !pattern.test(line));
34
+ fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
35
+ },
36
+ },
37
+ });
38
+
39
+ // Mock child_process execSync
40
+ mock.module("child_process", {
41
+ namedExports: {
42
+ execSync: () => mockStartupFile,
43
+ },
44
+ });
45
+
46
+ // Import windowsPowershell module after mocking
47
+ windowsPowershell = (await import("./windowsPowershell.js")).default;
48
+ });
49
+
50
+ afterEach(() => {
51
+ // Clean up test files
52
+ if (fs.existsSync(mockStartupFile)) {
53
+ fs.unlinkSync(mockStartupFile);
54
+ }
55
+
56
+ // Reset mocks
57
+ mock.reset();
58
+ });
59
+
60
+ describe("isInstalled", () => {
61
+ it("should return true when windows powershell is installed", () => {
62
+ assert.strictEqual(windowsPowershell.isInstalled(), true);
63
+ });
64
+
65
+ it("should call doesExecutableExistOnSystem with correct parameter", () => {
66
+ // Test that the method calls the helper with the right executable name
67
+ assert.strictEqual(windowsPowershell.isInstalled(), true);
68
+ });
69
+ });
70
+
71
+ describe("setup", () => {
72
+ it("should add aliases for all provided tools", () => {
73
+ const tools = [
74
+ { tool: "npm", aikidoCommand: "aikido-npm" },
75
+ { tool: "npx", aikidoCommand: "aikido-npx" },
76
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
77
+ ];
78
+
79
+ const result = windowsPowershell.setup(tools);
80
+ assert.strictEqual(result, true);
81
+
82
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
83
+ assert.ok(
84
+ content.includes("Set-Alias npm aikido-npm # Safe-chain alias for npm")
85
+ );
86
+ assert.ok(
87
+ content.includes("Set-Alias npx aikido-npx # Safe-chain alias for npx")
88
+ );
89
+ assert.ok(
90
+ content.includes(
91
+ "Set-Alias yarn aikido-yarn # Safe-chain alias for yarn"
92
+ )
93
+ );
94
+ });
95
+
96
+ it("should handle empty tools array", () => {
97
+ const result = windowsPowershell.setup([]);
98
+ assert.strictEqual(result, true);
99
+
100
+ // File should be created during teardown call even if no tools are provided
101
+ if (fs.existsSync(mockStartupFile)) {
102
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
103
+ assert.strictEqual(content.trim(), "");
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("teardown", () => {
109
+ it("should remove npm, npx, and yarn aliases", () => {
110
+ const initialContent = [
111
+ "# Windows PowerShell profile",
112
+ "Set-Alias npm aikido-npm",
113
+ "Set-Alias npx aikido-npx",
114
+ "Set-Alias yarn aikido-yarn",
115
+ "Set-Alias ls Get-ChildItem",
116
+ "Set-Alias grep Select-String",
117
+ ].join("\n");
118
+
119
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
120
+
121
+ const result = windowsPowershell.teardown(knownAikidoTools);
122
+ assert.strictEqual(result, true);
123
+
124
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
125
+ assert.ok(!content.includes("Set-Alias npm "));
126
+ assert.ok(!content.includes("Set-Alias npx "));
127
+ assert.ok(!content.includes("Set-Alias yarn "));
128
+ assert.ok(content.includes("Set-Alias ls "));
129
+ assert.ok(content.includes("Set-Alias grep "));
130
+ });
131
+
132
+ it("should handle file that doesn't exist", () => {
133
+ if (fs.existsSync(mockStartupFile)) {
134
+ fs.unlinkSync(mockStartupFile);
135
+ }
136
+
137
+ const result = windowsPowershell.teardown(knownAikidoTools);
138
+ assert.strictEqual(result, true);
139
+ });
140
+
141
+ it("should handle file with no relevant aliases", () => {
142
+ const initialContent = [
143
+ "# Windows PowerShell profile",
144
+ "Set-Alias ls Get-ChildItem",
145
+ "$env:PATH += ';C:\\Tools'",
146
+ ].join("\n");
147
+
148
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
149
+
150
+ const result = windowsPowershell.teardown(knownAikidoTools);
151
+ assert.strictEqual(result, true);
152
+
153
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
154
+ assert.ok(content.includes("Set-Alias ls "));
155
+ assert.ok(content.includes("$env:PATH "));
156
+ });
157
+ });
158
+
159
+ describe("shell properties", () => {
160
+ it("should have correct name", () => {
161
+ assert.strictEqual(windowsPowershell.name, "Windows PowerShell");
162
+ });
163
+
164
+ it("should expose all required methods", () => {
165
+ assert.ok(typeof windowsPowershell.isInstalled === "function");
166
+ assert.ok(typeof windowsPowershell.setup === "function");
167
+ assert.ok(typeof windowsPowershell.teardown === "function");
168
+ assert.ok(typeof windowsPowershell.name === "string");
169
+ });
170
+ });
171
+
172
+ describe("integration tests", () => {
173
+ it("should handle complete setup and teardown cycle", () => {
174
+ const tools = [
175
+ { tool: "npm", aikidoCommand: "aikido-npm" },
176
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
177
+ ];
178
+
179
+ // Setup
180
+ windowsPowershell.setup(tools);
181
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
182
+ assert.ok(content.includes("Set-Alias npm aikido-npm"));
183
+ assert.ok(content.includes("Set-Alias yarn aikido-yarn"));
184
+
185
+ // Teardown
186
+ windowsPowershell.teardown(tools);
187
+ content = fs.readFileSync(mockStartupFile, "utf-8");
188
+ assert.ok(!content.includes("Set-Alias npm "));
189
+ assert.ok(!content.includes("Set-Alias yarn "));
190
+ });
191
+
192
+ it("should handle multiple setup calls", () => {
193
+ const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
194
+
195
+ windowsPowershell.setup(tools);
196
+ windowsPowershell.teardown(tools);
197
+ windowsPowershell.setup(tools);
198
+
199
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
200
+ const npmMatches = (content.match(/Set-Alias npm /g) || []).length;
201
+ assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,58 @@
1
+ import {
2
+ addLineToFile,
3
+ doesExecutableExistOnSystem,
4
+ removeLinesMatchingPattern,
5
+ } from "../helpers.js";
6
+ import { execSync } from "child_process";
7
+
8
+ const shellName = "Zsh";
9
+ const executableName = "zsh";
10
+ const startupFileCommand = "echo ${ZDOTDIR:-$HOME}/.zshrc";
11
+
12
+ function isInstalled() {
13
+ return doesExecutableExistOnSystem(executableName);
14
+ }
15
+
16
+ function teardown(tools) {
17
+ const startupFile = getStartupFile();
18
+
19
+ for (const { tool } of tools) {
20
+ // Remove any existing alias for the tool
21
+ removeLinesMatchingPattern(startupFile, new RegExp(`^alias\\s+${tool}=`));
22
+ }
23
+
24
+ return true;
25
+ }
26
+
27
+ function setup(tools) {
28
+ const startupFile = getStartupFile();
29
+
30
+ for (const { tool, aikidoCommand } of tools) {
31
+ addLineToFile(
32
+ startupFile,
33
+ `alias ${tool}="${aikidoCommand}" # Safe-chain alias for ${tool}`
34
+ );
35
+ }
36
+
37
+ return true;
38
+ }
39
+
40
+ function getStartupFile() {
41
+ try {
42
+ return execSync(startupFileCommand, {
43
+ encoding: "utf8",
44
+ shell: executableName,
45
+ }).trim();
46
+ } catch (error) {
47
+ throw new Error(
48
+ `Command failed: ${startupFileCommand}. Error: ${error.message}`
49
+ );
50
+ }
51
+ }
52
+
53
+ export default {
54
+ name: shellName,
55
+ isInstalled,
56
+ setup,
57
+ teardown,
58
+ };
@@ -0,0 +1,199 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from "node:test";
2
+ import assert from "node:assert";
3
+ import { tmpdir } from "node:os";
4
+ import fs from "node:fs";
5
+ import path from "path";
6
+ import { knownAikidoTools } from "../helpers.js";
7
+
8
+ describe("Zsh shell integration", () => {
9
+ let mockStartupFile;
10
+ let zsh;
11
+
12
+ beforeEach(async () => {
13
+ // Create temporary startup file for testing
14
+ mockStartupFile = path.join(tmpdir(), `test-zshrc-${Date.now()}`);
15
+
16
+ // Mock the helpers module
17
+ mock.module("../helpers.js", {
18
+ namedExports: {
19
+ doesExecutableExistOnSystem: () => true,
20
+ addLineToFile: (filePath, line) => {
21
+ if (!fs.existsSync(filePath)) {
22
+ fs.writeFileSync(filePath, "", "utf-8");
23
+ }
24
+ fs.appendFileSync(filePath, line + "\n", "utf-8");
25
+ },
26
+ removeLinesMatchingPattern: (filePath, pattern) => {
27
+ if (!fs.existsSync(filePath)) return;
28
+ const content = fs.readFileSync(filePath, "utf-8");
29
+ const lines = content.split("\n");
30
+ const filteredLines = lines.filter((line) => !pattern.test(line));
31
+ fs.writeFileSync(filePath, filteredLines.join("\n"), "utf-8");
32
+ },
33
+ },
34
+ });
35
+
36
+ // Mock child_process execSync
37
+ mock.module("child_process", {
38
+ namedExports: {
39
+ execSync: () => mockStartupFile,
40
+ },
41
+ });
42
+
43
+ // Import zsh module after mocking
44
+ zsh = (await import("./zsh.js")).default;
45
+ });
46
+
47
+ afterEach(() => {
48
+ // Clean up test files
49
+ if (fs.existsSync(mockStartupFile)) {
50
+ fs.unlinkSync(mockStartupFile);
51
+ }
52
+
53
+ // Reset mocks
54
+ mock.reset();
55
+ });
56
+
57
+ describe("isInstalled", () => {
58
+ it("should return true when zsh is installed", () => {
59
+ assert.strictEqual(zsh.isInstalled(), true);
60
+ });
61
+
62
+ it("should call doesExecutableExistOnSystem with correct parameter", () => {
63
+ // Test that the method calls the helper with the right executable name
64
+ assert.strictEqual(zsh.isInstalled(), true);
65
+ });
66
+ });
67
+
68
+ describe("setup", () => {
69
+ it("should add aliases for all provided tools", () => {
70
+ const tools = [
71
+ { tool: "npm", aikidoCommand: "aikido-npm" },
72
+ { tool: "npx", aikidoCommand: "aikido-npx" },
73
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
74
+ ];
75
+
76
+ const result = zsh.setup(tools);
77
+ assert.strictEqual(result, true);
78
+
79
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
80
+ assert.ok(
81
+ content.includes('alias npm="aikido-npm" # Safe-chain alias for npm')
82
+ );
83
+ assert.ok(
84
+ content.includes('alias npx="aikido-npx" # Safe-chain alias for npx')
85
+ );
86
+ assert.ok(
87
+ content.includes('alias yarn="aikido-yarn" # Safe-chain alias for yarn')
88
+ );
89
+ });
90
+
91
+ it("should handle empty tools array", () => {
92
+ const result = zsh.setup([]);
93
+ assert.strictEqual(result, true);
94
+
95
+ // File should be created during teardown call even if no tools are provided
96
+ if (fs.existsSync(mockStartupFile)) {
97
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
98
+ assert.strictEqual(content.trim(), "");
99
+ }
100
+ });
101
+ });
102
+
103
+ describe("teardown", () => {
104
+ it("should remove npm, npx, and yarn aliases", () => {
105
+ const initialContent = [
106
+ "#!/bin/zsh",
107
+ "alias npm='aikido-npm'",
108
+ "alias npx='aikido-npx'",
109
+ "alias yarn='aikido-yarn'",
110
+ "alias ls='ls --color=auto'",
111
+ "alias grep='grep --color=auto'",
112
+ ].join("\n");
113
+
114
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
115
+
116
+ const result = zsh.teardown(knownAikidoTools);
117
+ assert.strictEqual(result, true);
118
+
119
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
120
+ assert.ok(!content.includes("alias npm="));
121
+ assert.ok(!content.includes("alias npx="));
122
+ assert.ok(!content.includes("alias yarn="));
123
+ assert.ok(content.includes("alias ls="));
124
+ assert.ok(content.includes("alias grep="));
125
+ });
126
+
127
+ it("should handle file that doesn't exist", () => {
128
+ if (fs.existsSync(mockStartupFile)) {
129
+ fs.unlinkSync(mockStartupFile);
130
+ }
131
+
132
+ const result = zsh.teardown(knownAikidoTools);
133
+ assert.strictEqual(result, true);
134
+ });
135
+
136
+ it("should handle file with no relevant aliases", () => {
137
+ const initialContent = [
138
+ "#!/bin/zsh",
139
+ "alias ls='ls --color=auto'",
140
+ "export PATH=$PATH:~/bin",
141
+ ].join("\n");
142
+
143
+ fs.writeFileSync(mockStartupFile, initialContent, "utf-8");
144
+
145
+ const result = zsh.teardown(knownAikidoTools);
146
+ assert.strictEqual(result, true);
147
+
148
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
149
+ assert.ok(content.includes("alias ls="));
150
+ assert.ok(content.includes("export PATH="));
151
+ });
152
+ });
153
+
154
+ describe("shell properties", () => {
155
+ it("should have correct name", () => {
156
+ assert.strictEqual(zsh.name, "Zsh");
157
+ });
158
+
159
+ it("should expose all required methods", () => {
160
+ assert.ok(typeof zsh.isInstalled === "function");
161
+ assert.ok(typeof zsh.setup === "function");
162
+ assert.ok(typeof zsh.teardown === "function");
163
+ assert.ok(typeof zsh.name === "string");
164
+ });
165
+ });
166
+
167
+ describe("integration tests", () => {
168
+ it("should handle complete setup and teardown cycle", () => {
169
+ const tools = [
170
+ { tool: "npm", aikidoCommand: "aikido-npm" },
171
+ { tool: "yarn", aikidoCommand: "aikido-yarn" },
172
+ ];
173
+
174
+ // Setup
175
+ zsh.setup(tools);
176
+ let content = fs.readFileSync(mockStartupFile, "utf-8");
177
+ assert.ok(content.includes('alias npm="aikido-npm"'));
178
+ assert.ok(content.includes('alias yarn="aikido-yarn"'));
179
+
180
+ // Teardown
181
+ zsh.teardown(tools);
182
+ content = fs.readFileSync(mockStartupFile, "utf-8");
183
+ assert.ok(!content.includes("alias npm="));
184
+ assert.ok(!content.includes("alias yarn="));
185
+ });
186
+
187
+ it("should handle multiple setup calls", () => {
188
+ const tools = [{ tool: "npm", aikidoCommand: "aikido-npm" }];
189
+
190
+ zsh.setup(tools);
191
+ zsh.teardown(tools);
192
+ zsh.setup(tools);
193
+
194
+ const content = fs.readFileSync(mockStartupFile, "utf-8");
195
+ const npmMatches = (content.match(/alias npm="/g) || []).length;
196
+ assert.strictEqual(npmMatches, 1, "Should not duplicate aliases");
197
+ });
198
+ });
199
+ });
@@ -1,9 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { ui } from "../environment/userInteraction.js";
3
3
  import { detectShells } from "./shellDetection.js";
4
- import { getAliases } from "./helpers.js";
5
- import fs from "fs";
6
- import { EOL } from "os";
4
+ import { knownAikidoTools } from "./helpers.js";
7
5
 
8
6
  export async function teardown() {
9
7
  ui.writeInformation(
@@ -27,8 +25,26 @@ export async function teardown() {
27
25
 
28
26
  let updatedCount = 0;
29
27
  for (const shell of shells) {
30
- if (removeAliasesForShell(shell)) {
28
+ let success = false;
29
+ try {
30
+ success = shell.teardown(knownAikidoTools);
31
+ } catch {
32
+ success = false;
33
+ }
34
+
35
+ if (success) {
36
+ ui.writeInformation(
37
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.green(
38
+ "Teardown successful"
39
+ )}`
40
+ );
31
41
  updatedCount++;
42
+ } else {
43
+ ui.writeError(
44
+ `${chalk.bold("- " + shell.name + ":")} ${chalk.red(
45
+ "Teardown failed"
46
+ )}. Please check your ${shell.name} configuration.`
47
+ );
32
48
  }
33
49
  }
34
50
 
@@ -43,98 +59,3 @@ export async function teardown() {
43
59
  return;
44
60
  }
45
61
  }
46
-
47
- /**
48
- * This function removes aliases for the given shell.
49
- * It reads the shell's startup file (eg ~/.bashrc, ~/.zshrc, etc.),
50
- * and then removes the aliases for npm, npx, and yarn commands.
51
- * If the aliases don't exist, it will report that they were not found.
52
- * If the startup file does not exist, it will report that no aliases need to be removed.
53
- *
54
- * The shell startup script is loaded by the respective shell when it starts.
55
- * This means that the aliases will be removed from the shell after it is restarted.
56
- */
57
- function removeAliasesForShell(shell) {
58
- if (!shell.startupFile) {
59
- ui.writeError(
60
- `- ${chalk.bold(
61
- shell.name
62
- )}: no startup file found. Cannot remove aliases.`
63
- );
64
- return false;
65
- }
66
-
67
- if (!fs.existsSync(shell.startupFile)) {
68
- ui.writeInformation(
69
- `- ${chalk.bold(
70
- shell.name
71
- )}: startup file does not exist. No aliases to remove.`
72
- );
73
- return false;
74
- }
75
-
76
- const aliases = getAliases(shell.startupFile);
77
- const fileContent = fs.readFileSync(shell.startupFile, "utf-8");
78
- const { removedCount, notFoundCount } = removeAliasesFromFile(
79
- aliases,
80
- fileContent,
81
- shell.startupFile
82
- );
83
-
84
- let summary = "- " + chalk.bold(shell.name) + ": ";
85
-
86
- if (removedCount > 0) {
87
- summary += chalk.green(`${removedCount} aliases were removed`);
88
- }
89
- if (notFoundCount > 0) {
90
- if (removedCount > 0) {
91
- summary += ", ";
92
- }
93
- summary += chalk.yellow(`${notFoundCount} aliases were not found`);
94
- }
95
- if (removedCount === 0 && notFoundCount === 0) {
96
- summary += chalk.yellow("no aliases found to remove");
97
- }
98
-
99
- ui.writeInformation(summary);
100
- return removedCount > 0;
101
- }
102
-
103
- /**
104
- * This function removes the aliases from the startup file.
105
- * It searches for exact matches of each alias line and removes them.
106
- * eg: for bash it will remove 'alias npm="aikido-npm"' for npm from ~/.bashrc
107
- * @returns an object with the counts of removed and not found aliases.
108
- */
109
- export function removeAliasesFromFile(aliases, fileContent, startupFilePath) {
110
- let removedCount = 0;
111
- let notFoundCount = 0;
112
- let updatedContent = fileContent;
113
-
114
- for (const alias of aliases) {
115
- const lines = updatedContent.split(EOL);
116
- let aliasLineIndex = lines.findIndex((line) => line.trim() === alias);
117
-
118
- if (aliasLineIndex !== -1) {
119
- removedCount++;
120
-
121
- // Remove all occurrences of the alias line, in case it appears multiple times
122
- while (aliasLineIndex !== -1) {
123
- lines.splice(aliasLineIndex, 1);
124
- aliasLineIndex = lines.findIndex((line) => line.trim() === alias);
125
- }
126
- updatedContent = lines.join(EOL);
127
- } else {
128
- notFoundCount++;
129
- }
130
- }
131
-
132
- if (removedCount > 0) {
133
- fs.writeFileSync(startupFilePath, updatedContent, "utf-8");
134
- }
135
-
136
- return {
137
- removedCount,
138
- notFoundCount,
139
- };
140
- }