codify-plugin-lib 1.0.182-beta6 → 1.0.182-beta60

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 (50) hide show
  1. package/bin/build.js +189 -0
  2. package/dist/bin/build.js +0 -0
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.js +4 -0
  5. package/dist/messages/handlers.js +10 -2
  6. package/dist/plugin/plugin.d.ts +2 -1
  7. package/dist/plugin/plugin.js +4 -1
  8. package/dist/pty/background-pty.d.ts +3 -2
  9. package/dist/pty/background-pty.js +7 -14
  10. package/dist/pty/index.d.ts +8 -2
  11. package/dist/pty/seqeuntial-pty.d.ts +3 -2
  12. package/dist/pty/seqeuntial-pty.js +47 -12
  13. package/dist/resource/parsed-resource-settings.d.ts +3 -1
  14. package/dist/resource/parsed-resource-settings.js +15 -2
  15. package/dist/resource/resource-controller.js +5 -5
  16. package/dist/resource/resource-settings.d.ts +8 -2
  17. package/dist/resource/resource-settings.js +2 -2
  18. package/dist/test.d.ts +1 -0
  19. package/dist/test.js +5 -0
  20. package/dist/utils/file-utils.d.ts +14 -7
  21. package/dist/utils/file-utils.js +65 -51
  22. package/dist/utils/functions.js +2 -2
  23. package/dist/utils/index.d.ts +12 -0
  24. package/dist/utils/index.js +111 -0
  25. package/dist/utils/load-resources.d.ts +1 -0
  26. package/dist/utils/load-resources.js +46 -0
  27. package/dist/utils/package-json-utils.d.ts +12 -0
  28. package/dist/utils/package-json-utils.js +34 -0
  29. package/package.json +5 -4
  30. package/rollup.config.js +24 -0
  31. package/src/index.ts +4 -0
  32. package/src/messages/handlers.test.ts +23 -0
  33. package/src/messages/handlers.ts +11 -2
  34. package/src/plugin/plugin.test.ts +31 -0
  35. package/src/plugin/plugin.ts +6 -2
  36. package/src/pty/background-pty.ts +10 -18
  37. package/src/pty/index.ts +10 -4
  38. package/src/pty/seqeuntial-pty.ts +62 -16
  39. package/src/pty/sequential-pty.test.ts +137 -4
  40. package/src/resource/parsed-resource-settings.test.ts +24 -0
  41. package/src/resource/parsed-resource-settings.ts +23 -7
  42. package/src/resource/resource-controller.test.ts +126 -0
  43. package/src/resource/resource-controller.ts +5 -6
  44. package/src/resource/resource-settings.test.ts +36 -0
  45. package/src/resource/resource-settings.ts +11 -4
  46. package/src/utils/file-utils.test.ts +7 -0
  47. package/src/utils/file-utils.ts +70 -55
  48. package/src/utils/functions.ts +3 -3
  49. package/src/utils/index.ts +138 -0
  50. package/src/utils/internal-utils.test.ts +1 -0
@@ -36,18 +36,17 @@ export class ResourceController<T extends StringIndexedObject> {
36
36
 
37
37
  this.typeId = this.settings.id;
38
38
  this.dependencies = this.settings.dependencies ?? [];
39
+ this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
39
40
 
40
- if (this.settings.schema) {
41
+ if (this.parsedSettings.schema) {
41
42
  this.ajv = new Ajv({
42
43
  allErrors: true,
43
44
  strict: true,
44
45
  strictRequired: false,
45
46
  allowUnionTypes: true
46
47
  })
47
- this.schemaValidator = this.ajv.compile(this.settings.schema);
48
+ this.schemaValidator = this.ajv.compile(this.parsedSettings.schema);
48
49
  }
49
-
50
- this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
51
50
  }
52
51
 
53
52
  async initialize(): Promise<void> {
@@ -526,8 +525,8 @@ ${JSON.stringify(refresh, null, 2)}
526
525
  }
527
526
 
528
527
  private getAllParameterKeys(): string[] {
529
- return this.settings.schema
530
- ? Object.keys((this.settings.schema as any)?.properties)
528
+ return this.parsedSettings.schema
529
+ ? Object.keys((this.parsedSettings.schema as any)?.properties)
531
530
  : Object.keys(this.parsedSettings.parameterSettings);
532
531
  }
533
532
 
@@ -13,6 +13,7 @@ import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './res
13
13
  import { ResourceController } from './resource-controller.js';
14
14
  import os from 'node:os';
15
15
  import path from 'node:path';
16
+ import { z } from 'zod';
16
17
 
17
18
  describe('Resource parameter tests', () => {
18
19
  it('Generates a resource plan that includes stateful parameters (create)', async () => {
@@ -1174,4 +1175,39 @@ describe('Resource parameter tests', () => {
1174
1175
  expect(from2).to.eq('$HOME/abc/def')
1175
1176
 
1176
1177
  })
1178
+
1179
+ it('Can match directories 2', async () => {
1180
+
1181
+ const schema = z.object({
1182
+ propA: z.string(),
1183
+ propB: z.number(),
1184
+ });
1185
+
1186
+ const resource = new class extends TestResource {
1187
+ getSettings(): ResourceSettings<z.infer<typeof schema>> {
1188
+ return {
1189
+ id: 'resourceType',
1190
+ schema,
1191
+ operatingSystems: [OS.Darwin],
1192
+ parameterSettings: {
1193
+ propA: { type: 'directory' }
1194
+ },
1195
+ }
1196
+ }
1197
+ };
1198
+
1199
+ const controller = new ResourceController(resource);
1200
+ const transformations = controller.parsedSettings.inputTransformations.propA;
1201
+
1202
+ const to = transformations!.to('$HOME/abc/def')
1203
+ expect(to).to.eq(os.homedir() + '/abc/def')
1204
+
1205
+ const from = transformations!.from(os.homedir() + '/abc/def')
1206
+ expect(from).to.eq('~/abc/def')
1207
+
1208
+ const from2 = transformations!.from(os.homedir() + '/abc/def', '$HOME/abc/def')
1209
+ expect(from2).to.eq('$HOME/abc/def')
1210
+
1211
+ })
1212
+
1177
1213
  })
@@ -2,6 +2,7 @@ import { JSONSchemaType } from 'ajv';
2
2
  import { OS, StringIndexedObject } from 'codify-schemas';
3
3
  import isObjectsEqual from 'lodash.isequal'
4
4
  import path from 'node:path';
5
+ import { ZodObject } from 'zod';
5
6
 
6
7
  import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
7
8
  import {
@@ -11,6 +12,7 @@ import {
11
12
  tildify,
12
13
  untildify
13
14
  } from '../utils/functions.js';
15
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
14
16
  import { RefreshContext } from './resource.js';
15
17
 
16
18
  export interface InputTransformation {
@@ -36,7 +38,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
36
38
  /**
37
39
  * Schema to validate user configs with. Must be in the format JSON Schema draft07
38
40
  */
39
- schema?: Partial<JSONSchemaType<T | any>>;
41
+ schema?: Partial<JSONSchemaType<T | any>> | ZodObject;
40
42
 
41
43
  /**
42
44
  * Mark the resource as sensitive. Defaults to false. This prevents the resource from automatically being imported by init and import.
@@ -44,6 +46,11 @@ export interface ResourceSettings<T extends StringIndexedObject> {
44
46
  */
45
47
  isSensitive?: boolean;
46
48
 
49
+ /**
50
+ * An optional description of the resource. This does not affect the behavior of the resource.
51
+ */
52
+ description?: string;
53
+
47
54
  /**
48
55
  * Allow multiple of the same resource to unique. Set truthy if
49
56
  * multiples are allowed, for example for applications, there can be multiple copy of the same application installed
@@ -349,7 +356,7 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
349
356
 
350
357
  const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
351
358
  'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
352
- 'directory': (a: unknown, b: unknown) => {
359
+ 'directory'(a: unknown, b: unknown) {
353
360
  let transformedA = resolvePathWithVariables(untildify(String(a)))
354
361
  let transformedB = resolvePathWithVariables(untildify(String(b)))
355
362
 
@@ -430,7 +437,7 @@ export function resolveFnFromEqualsFnOrString(
430
437
  const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, InputTransformation>> = {
431
438
  'directory': {
432
439
  to: (a: unknown) => resolvePathWithVariables((untildify(String(a)))),
433
- from: (a: unknown, original) => {
440
+ from(a: unknown, original) {
434
441
  if (ParameterEqualsDefaults.directory!(a, original)) {
435
442
  return original;
436
443
  }
@@ -489,7 +496,7 @@ export function resolveParameterTransformFn(
489
496
  }
490
497
 
491
498
  export function resolveMatcher<T extends StringIndexedObject>(
492
- settings: ResourceSettings<T>
499
+ settings: ParsedResourceSettings<T>
493
500
  ): (desired: Partial<T>, current: Partial<T>) => boolean {
494
501
  return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
495
502
  ? ((desired: Partial<T>, current: Partial<T>) => {
@@ -0,0 +1,7 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ describe('File utils tests', { timeout: 100_000_000 }, () => {
4
+ it('Can download a file', async () => {
5
+ // await FileUtils.downloadFile('https://download.jetbrains.com/webstorm/WebStorm-2025.3.1-aarch64.dmg?_gl=1*1huoi7o*_gcl_aw*R0NMLjE3NjU3NDAwMTcuQ2p3S0NBaUEzZm5KQmhBZ0Vpd0F5cW1ZNVhLVENlbHJOcTk2YXdjZVlfMS1wdE91MXc0WDk2bFJkVDM3QURhUFNJMUtwNVVSVUhxWTJob0NuZ0FRQXZEX0J3RQ..*_gcl_au*MjA0MDQ0MjE2My4xNzYzNjQzNzMz*FPAU*MjA0MDQ0MjE2My4xNzYzNjQzNzMz*_ga*MTYxMDg4MTkzMi4xNzYzNjQzNzMz*_ga_9J976DJZ68*czE3NjYzNjI5ODAkbzEyJGcxJHQxNzY2MzYzMDQwJGo2MCRsMCRoMA..', path.join(process.cwd(), 'google.html'));
6
+ })
7
+ })
@@ -25,7 +25,7 @@ export class FileUtils {
25
25
  console.log(`Finished downloading to ${destination}`);
26
26
  }
27
27
 
28
- static async addToStartupFile(line: string): Promise<void> {
28
+ static async addToShellRc(line: string): Promise<void> {
29
29
  const lineToInsert = addLeadingSpacer(
30
30
  addTrailingSpacer(line)
31
31
  );
@@ -45,7 +45,7 @@ export class FileUtils {
45
45
  }
46
46
  }
47
47
 
48
- static async addAllToStartupFile(lines: string[]): Promise<void> {
48
+ static async addAllToShellRc(lines: string[]): Promise<void> {
49
49
  const formattedLines = '\n' + lines.join('\n') + '\n';
50
50
  const shellRc = Utils.getPrimaryShellRc();
51
51
 
@@ -55,7 +55,17 @@ ${lines.join('\n')}`)
55
55
  await fs.appendFile(shellRc, formattedLines)
56
56
  }
57
57
 
58
- static async addPathToPrimaryShellRc(value: string, prepend: boolean): Promise<void> {
58
+ /**
59
+ * This method adds a directory path to the shell rc file if it doesn't already exist.
60
+ *
61
+ * @param value - The directory path to add.
62
+ * @param prepend - Whether to prepend the path to the existing PATH variable.
63
+ */
64
+ static async addPathToShellRc(value: string, prepend: boolean): Promise<void> {
65
+ if (await Utils.isDirectoryOnPath(value)) {
66
+ return;
67
+ }
68
+
59
69
  const shellRc = Utils.getPrimaryShellRc();
60
70
  console.log(`Saving path: ${value} to ${shellRc}`);
61
71
 
@@ -67,56 +77,6 @@ ${lines.join('\n')}`)
67
77
  await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
68
78
  }
69
79
 
70
- static async dirExists(path: string): Promise<boolean> {
71
- let stat;
72
- try {
73
- stat = await fs.stat(path);
74
- return stat.isDirectory();
75
- } catch {
76
- return false;
77
- }
78
- }
79
-
80
- static async fileExists(path: string): Promise<boolean> {
81
- let stat;
82
- try {
83
- stat = await fs.stat(path);
84
- return stat.isFile();
85
- } catch {
86
- return false;
87
- }
88
- }
89
-
90
- static async exists(path: string): Promise<boolean> {
91
- try {
92
- await fs.stat(path);
93
- return true;
94
- } catch {
95
- return false;
96
- }
97
- }
98
-
99
- static async checkDirExistsOrThrowIfFile(path: string): Promise<boolean> {
100
- let stat;
101
- try {
102
- stat = await fs.stat(path);
103
- } catch {
104
- return false;
105
- }
106
-
107
- if (stat.isDirectory()) {
108
- return true;
109
- }
110
-
111
- throw new Error(`Directory ${path} already exists and is a file`);
112
- }
113
-
114
- static async createDirIfNotExists(path: string): Promise<void> {
115
- if (!fsSync.existsSync(path)) {
116
- await fs.mkdir(path, { recursive: true });
117
- }
118
- }
119
-
120
80
  static async removeFromFile(filePath: string, search: string): Promise<void> {
121
81
  const contents = await fs.readFile(filePath, 'utf8');
122
82
  const newContents = contents.replaceAll(search, '');
@@ -124,7 +84,6 @@ ${lines.join('\n')}`)
124
84
  await fs.writeFile(filePath, newContents, 'utf8');
125
85
  }
126
86
 
127
-
128
87
  static async removeLineFromFile(filePath: string, search: RegExp | string): Promise<void> {
129
88
  const file = await fs.readFile(filePath, 'utf8')
130
89
  const lines = file.split('\n');
@@ -168,10 +127,16 @@ ${lines.join('\n')}`)
168
127
  console.log(`Removed line: ${search} from ${filePath}`)
169
128
  }
170
129
 
171
- static async removeLineFromPrimaryShellRc(search: RegExp | string): Promise<void> {
130
+ static async removeLineFromShellRc(search: RegExp | string): Promise<void> {
172
131
  return FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
173
132
  }
174
133
 
134
+ static async removeAllLinesFromShellRc(searches: Array<RegExp | string>): Promise<void> {
135
+ for (const search of searches) {
136
+ await FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
137
+ }
138
+ }
139
+
175
140
  // Append the string to the end of a file ensuring at least 1 lines of space between.
176
141
  // Ex result:
177
142
  // something something;
@@ -190,6 +155,56 @@ ${lines.join('\n')}`)
190
155
  return lines.join('\n') + '\n'.repeat(numNewLines) + textToInsert
191
156
  }
192
157
 
158
+ static async dirExists(path: string): Promise<boolean> {
159
+ let stat;
160
+ try {
161
+ stat = await fs.stat(path);
162
+ return stat.isDirectory();
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ static async fileExists(path: string): Promise<boolean> {
169
+ let stat;
170
+ try {
171
+ stat = await fs.stat(path);
172
+ return stat.isFile();
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ static async exists(path: string): Promise<boolean> {
179
+ try {
180
+ await fs.stat(path);
181
+ return true;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ static async checkDirExistsOrThrowIfFile(path: string): Promise<boolean> {
188
+ let stat;
189
+ try {
190
+ stat = await fs.stat(path);
191
+ } catch {
192
+ return false;
193
+ }
194
+
195
+ if (stat.isDirectory()) {
196
+ return true;
197
+ }
198
+
199
+ throw new Error(`Directory ${path} already exists and is a file`);
200
+ }
201
+
202
+ static async createDirIfNotExists(path: string): Promise<void> {
203
+ if (!fsSync.existsSync(path)) {
204
+ await fs.mkdir(path, { recursive: true });
205
+ }
206
+ }
207
+
193
208
  // This is overly complicated but it can be used to insert into any
194
209
  // position in the future
195
210
  private static calculateEndingNewLines(lines: string[]): number {
@@ -9,10 +9,11 @@ export function splitUserConfig<T extends StringIndexedObject>(
9
9
  type: config.type,
10
10
  ...(config.name ? { name: config.name } : {}),
11
11
  ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
12
+ ...(config.os ? { os: config.os } : {}),
12
13
  };
13
14
 
14
15
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
- const { type, name, dependsOn, ...parameters } = config;
16
+ const { type, name, dependsOn, os, ...parameters } = config;
16
17
 
17
18
  return {
18
19
  parameters: parameters as T,
@@ -35,8 +36,7 @@ export function tildify(pathWithTilde: string) {
35
36
  }
36
37
 
37
38
  export function resolvePathWithVariables(pathWithVariables: string) {
38
- // @ts-expect-error Ignore this for now
39
- return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b])
39
+ return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]!)
40
40
  }
41
41
 
42
42
  export function addVariablesToPath(pathWithoutVariables: string) {
@@ -1,7 +1,10 @@
1
1
  import { OS } from 'codify-schemas';
2
+ import * as fs from 'node:fs/promises';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
4
5
 
6
+ import { SpawnStatus, getPty } from '../pty/index.js';
7
+
5
8
  export function isDebug(): boolean {
6
9
  return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
7
10
  }
@@ -40,6 +43,34 @@ export const Utils = {
40
43
  return os.platform() === 'linux';
41
44
  },
42
45
 
46
+ async isArmArch(): Promise<boolean> {
47
+ const $ = getPty();
48
+ if (!Utils.isMacOS()) {
49
+ // On Linux, check uname -m
50
+ const query = await $.spawn('uname -m');
51
+ return query.data.trim() === 'aarch64' || query.data.trim() === 'arm64';
52
+ }
53
+
54
+ const query = await $.spawn('sysctl -n machdep.cpu.brand_string');
55
+ return /M(\d)/.test(query.data);
56
+ },
57
+
58
+ async isHomebrewInstalled(): Promise<boolean> {
59
+ const $ = getPty();
60
+ const query = await $.spawnSafe('which brew', { interactive: true });
61
+ return query.status === SpawnStatus.SUCCESS;
62
+ },
63
+
64
+ async isRosetta2Installed(): Promise<boolean> {
65
+ if (!Utils.isMacOS()) {
66
+ return false;
67
+ }
68
+
69
+ const $ = getPty();
70
+ const query = await $.spawnSafe('arch -x86_64 /usr/bin/true 2> /dev/null', { interactive: true });
71
+ return query.status === SpawnStatus.SUCCESS;
72
+ },
73
+
43
74
  getShell(): Shell | undefined {
44
75
  const shell = process.env.SHELL || '';
45
76
 
@@ -138,6 +169,113 @@ export const Utils = {
138
169
  path.join(homeDir, '.profile'),
139
170
  ];
140
171
  },
172
+
173
+ async isDirectoryOnPath(directory: string): Promise<boolean> {
174
+ const $ = getPty();
175
+ const { data: pathQuery } = await $.spawn('echo $PATH', { interactive: true });
176
+ const lines = pathQuery.split(':');
177
+ return lines.includes(directory);
178
+ },
179
+
180
+ async assertBrewInstalled(): Promise<void> {
181
+ const $ = getPty();
182
+ const brewCheck = await $.spawnSafe('which brew', { interactive: true });
183
+ if (brewCheck.status === SpawnStatus.ERROR) {
184
+ throw new Error(
185
+ `Homebrew is not installed. Cannot install git-lfs without Homebrew installed.
186
+
187
+ Brew can be installed using Codify:
188
+ {
189
+ "type": "homebrew",
190
+ }`
191
+ );
192
+ }
193
+ },
194
+
195
+ /**
196
+ * Installs a package via the system package manager. This will use Homebrew on macOS and apt on Ubuntu/Debian or dnf on Fedora.
197
+ * @param packageName
198
+ */
199
+ async installViaPkgMgr(packageName: string): Promise<boolean> {
200
+ const $ = getPty();
201
+
202
+ if (Utils.isMacOS()) {
203
+ await this.assertBrewInstalled();
204
+ const { status } = await $.spawnSafe(`brew install ${packageName}`, { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1 } });
205
+ return status === SpawnStatus.SUCCESS;
206
+ }
207
+
208
+ if (Utils.isLinux()) {
209
+ const isAptInstalled = await $.spawnSafe('which apt');
210
+ if (isAptInstalled.status === SpawnStatus.SUCCESS) {
211
+ const { status } = await $.spawnSafe(`apt install ${packageName}`, { interactive: true, requiresRoot: true, env: { DEBIAN_FRONTEND: 'noninteractive' } });
212
+ return status === SpawnStatus.SUCCESS;
213
+ }
214
+
215
+ const isDnfInstalled = await $.spawnSafe('which dnf');
216
+ if (isDnfInstalled.status === SpawnStatus.SUCCESS) {
217
+ const { status } = await $.spawnSafe(`dnf install ${packageName} -y`, { interactive: true, requiresRoot: true });
218
+ return status === SpawnStatus.SUCCESS;
219
+ }
220
+
221
+ const isYumInstalled = await $.spawnSafe('which yum');
222
+ if (isYumInstalled.status === SpawnStatus.SUCCESS) {
223
+ const { status } = await $.spawnSafe(`yum install ${packageName} -y`, { interactive: true, requiresRoot: true });
224
+ return status === SpawnStatus.SUCCESS;
225
+ }
226
+
227
+ return false;
228
+ }
229
+
230
+ return false;
231
+ },
232
+
233
+ async uninstallViaPkgMgr(packageName: string): Promise<boolean> {
234
+ const $ = getPty();
235
+
236
+ if (Utils.isMacOS()) {
237
+ await this.assertBrewInstalled();
238
+ const { status } = await $.spawnSafe(`brew uninstall --zap ${packageName}`, { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1 } });
239
+ return status === SpawnStatus.SUCCESS;
240
+ }
241
+
242
+ if (Utils.isLinux()) {
243
+ const isAptInstalled = await $.spawnSafe('which apt');
244
+ if (isAptInstalled.status === SpawnStatus.SUCCESS) {
245
+ const { status } = await $.spawnSafe(`apt remove --purge ${packageName}`, { interactive: true, requiresRoot: true, env: { DEBIAN_FRONTEND: 'noninteractive' } });
246
+ return status === SpawnStatus.SUCCESS;
247
+ }
248
+
249
+ const isDnfInstalled = await $.spawnSafe('which dnf');
250
+ if (isDnfInstalled.status === SpawnStatus.SUCCESS) {
251
+ const { status } = await $.spawnSafe(`dnf autoremove ${packageName} -y`, { interactive: true, requiresRoot: true });
252
+ return status === SpawnStatus.SUCCESS;
253
+ }
254
+
255
+ const isYumInstalled = await $.spawnSafe('which yum');
256
+ if (isYumInstalled.status === SpawnStatus.SUCCESS) {
257
+ const { status } = await $.spawnSafe(`yum autoremove ${packageName} -y`, { interactive: true, requiresRoot: true });
258
+ return status === SpawnStatus.SUCCESS;
259
+ }
260
+
261
+ return false;
262
+ }
263
+
264
+ return false;
265
+ },
266
+
267
+
268
+ async linuxDistro(): Promise<'arch' | 'centos' | 'debian' | 'fedora' | 'rhel' | 'ubuntu' | undefined> {
269
+ const osRelease = await fs.readFile('/etc/os-release', 'utf8');
270
+ const lines = osRelease.split('\n');
271
+ for (const line of lines) {
272
+ if (line.startsWith('ID=')) {
273
+ return line.slice(3).trim() as any;
274
+ }
275
+ }
276
+
277
+ return undefined;
278
+ }
141
279
  };
142
280
 
143
281
 
@@ -8,6 +8,7 @@ describe('Utils tests', () => {
8
8
  type: 'type',
9
9
  name: 'name',
10
10
  dependsOn: ['a', 'b', 'c'],
11
+ os: ['linux'],
11
12
  propA: 'propA',
12
13
  propB: 'propB',
13
14
  propC: 'propC',