codify-plugin-lib 1.0.182-beta3 → 1.0.182-beta30

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 (40) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +2 -0
  3. package/dist/plugin/plugin.js +1 -1
  4. package/dist/pty/background-pty.d.ts +3 -2
  5. package/dist/pty/background-pty.js +6 -14
  6. package/dist/pty/index.d.ts +4 -2
  7. package/dist/pty/seqeuntial-pty.d.ts +3 -2
  8. package/dist/pty/seqeuntial-pty.js +44 -10
  9. package/dist/resource/parsed-resource-settings.d.ts +3 -1
  10. package/dist/resource/parsed-resource-settings.js +15 -2
  11. package/dist/resource/resource-controller.js +3 -3
  12. package/dist/resource/resource-settings.d.ts +8 -2
  13. package/dist/resource/resource-settings.js +1 -1
  14. package/dist/test.d.ts +1 -0
  15. package/dist/test.js +5 -0
  16. package/dist/utils/file-utils.d.ts +23 -0
  17. package/dist/utils/file-utils.js +186 -0
  18. package/dist/utils/functions.d.ts +12 -0
  19. package/dist/utils/functions.js +74 -0
  20. package/dist/utils/index.d.ts +4 -0
  21. package/dist/utils/index.js +30 -0
  22. package/package.json +4 -3
  23. package/src/index.ts +2 -0
  24. package/src/plugin/plugin.test.ts +31 -0
  25. package/src/plugin/plugin.ts +1 -1
  26. package/src/pty/background-pty.ts +9 -18
  27. package/src/pty/index.ts +6 -4
  28. package/src/pty/seqeuntial-pty.ts +59 -14
  29. package/src/pty/sequential-pty.test.ts +138 -5
  30. package/src/resource/parsed-resource-settings.test.ts +24 -0
  31. package/src/resource/parsed-resource-settings.ts +24 -7
  32. package/src/resource/resource-controller.test.ts +127 -1
  33. package/src/resource/resource-controller.ts +3 -4
  34. package/src/resource/resource-settings.test.ts +36 -0
  35. package/src/resource/resource-settings.ts +10 -3
  36. package/src/utils/file-utils.test.ts +7 -0
  37. package/src/utils/file-utils.ts +231 -0
  38. package/src/utils/{internal-utils.ts → functions.ts} +3 -3
  39. package/src/utils/index.ts +37 -0
  40. package/src/utils/internal-utils.test.ts +2 -1
@@ -1,5 +1,6 @@
1
1
  import { JSONSchemaType } from 'ajv';
2
2
  import { OS, StringIndexedObject } from 'codify-schemas';
3
+ import { ZodObject, z } from 'zod';
3
4
 
4
5
  import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
5
6
  import {
@@ -7,13 +8,14 @@ import {
7
8
  DefaultParameterSetting,
8
9
  InputTransformation,
9
10
  ParameterSetting,
11
+ ResourceSettings,
12
+ StatefulParameterSetting,
10
13
  resolveElementEqualsFn,
11
14
  resolveEqualsFn,
12
15
  resolveMatcher,
13
- resolveParameterTransformFn,
14
- ResourceSettings,
15
- StatefulParameterSetting
16
+ resolveParameterTransformFn
16
17
  } from './resource-settings.js';
18
+ import { JSONSchema } from '@apidevtools/json-schema-ref-parser';
17
19
 
18
20
  export interface ParsedStatefulParameterSetting extends DefaultParameterSetting {
19
21
  type: 'stateful',
@@ -29,7 +31,7 @@ export type ParsedArrayParameterSetting = {
29
31
 
30
32
  export type ParsedParameterSetting =
31
33
  {
32
- isEqual: (desired: unknown, current: unknown) => boolean;
34
+ isEqual: (desired: unknown, current: unknown) => boolean;
33
35
  } & (DefaultParameterSetting
34
36
  | ParsedArrayParameterSetting
35
37
  | ParsedStatefulParameterSetting)
@@ -37,10 +39,13 @@ export type ParsedParameterSetting =
37
39
  export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
38
40
  private cache = new Map<string, unknown>();
39
41
  id!: string;
42
+ description?: string;
43
+
40
44
  schema?: Partial<JSONSchemaType<T | any>>;
41
45
  allowMultiple?: {
46
+ identifyingParameters?: string[];
42
47
  matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
43
- requiredParameters?: string[]
48
+ findAllParameters?: () => Promise<Array<Partial<T>>>
44
49
  } | boolean;
45
50
 
46
51
  removeStatefulParametersBeforeDestroy?: boolean | undefined;
@@ -54,10 +59,22 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
54
59
 
55
60
  constructor(settings: ResourceSettings<T>) {
56
61
  this.settings = settings;
62
+ const { parameterSettings, schema, ...rest } = settings;
57
63
 
58
- const { parameterSettings, ...rest } = settings;
59
64
  Object.assign(this, rest);
60
65
 
66
+ if (schema) {
67
+ this.schema = schema instanceof ZodObject
68
+ ? z.toJSONSchema(schema.strict(), {
69
+ target: 'draft-7',
70
+ override(ctx) {
71
+ ctx.jsonSchema.title = settings.id;
72
+ ctx.jsonSchema.description = settings.description ?? `${settings.id} resource. Can be used to manage ${settings.id}`;
73
+ }
74
+ }) as JSONSchemaType<T>
75
+ : schema;
76
+ }
77
+
61
78
  this.validateSettings();
62
79
  }
63
80
 
@@ -199,7 +216,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
199
216
  throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`)
200
217
  }
201
218
 
202
- const schema = this.settings.schema as JSONSchemaType<any>;
219
+ const schema = this.schema as JSONSchemaType<any>;
203
220
  if (!this.settings.importAndDestroy && (schema?.oneOf
204
221
  && Array.isArray(schema.oneOf)
205
222
  && schema.oneOf.some((s) => s.required)
@@ -7,10 +7,11 @@ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
7
7
  import { ParameterChange } from '../plan/change-set.js';
8
8
  import { ResourceController } from './resource-controller.js';
9
9
  import { TestConfig, testPlan, TestResource, TestStatefulParameter } from '../utils/test-utils.test.js';
10
- import { tildify, untildify } from '../utils/internal-utils.js';
10
+ import { tildify, untildify } from '../utils/functions.js';
11
11
  import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
12
12
  import { Plan } from '../plan/plan.js';
13
13
  import os from 'node:os';
14
+ import { z } from 'zod';
14
15
 
15
16
  describe('Resource tests', () => {
16
17
 
@@ -952,4 +953,129 @@ describe('Resource tests', () => {
952
953
 
953
954
  process.env = oldProcessEnv;
954
955
  })
956
+
957
+ it('Can import and return all of the imported parameters (zod schema)', async () => {
958
+ const schema = z.object({
959
+ path: z
960
+ .string()
961
+ .describe(
962
+ 'A list of paths to add to the PATH environment variable'
963
+ ),
964
+ paths: z
965
+ .array(z.string())
966
+ .describe(
967
+ 'A list of paths to add to the PATH environment variable'
968
+ ),
969
+ prepend: z
970
+ .boolean()
971
+ .describe(
972
+ 'Whether to prepend the paths to the PATH environment variable'
973
+ ),
974
+ declarationsOnly: z
975
+ .boolean()
976
+ .describe(
977
+ 'Whether to only declare the paths in the PATH environment variable'
978
+ ),
979
+ })
980
+
981
+ const resource = new class extends TestResource {
982
+ getSettings(): ResourceSettings<any> {
983
+ return {
984
+ id: 'path',
985
+ schema,
986
+ operatingSystems: [OS.Darwin],
987
+ parameterSettings: {
988
+ path: { type: 'directory' },
989
+ paths: { canModify: true, type: 'array', itemType: 'directory' },
990
+ prepend: { default: false, setting: true },
991
+ declarationsOnly: { default: false, setting: true },
992
+ },
993
+ importAndDestroy: {
994
+ refreshMapper: (input, context) => {
995
+ if (Object.keys(input).length === 0) {
996
+ return { paths: [], declarationsOnly: true };
997
+ }
998
+
999
+ return input;
1000
+ }
1001
+ },
1002
+ allowMultiple: {
1003
+ matcher: (desired, current) => {
1004
+ if (desired.path) {
1005
+ return desired.path === current.path;
1006
+ }
1007
+
1008
+ const currentPaths = new Set(current.paths)
1009
+ return desired.paths?.some((p) => currentPaths.has(p));
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
1016
+ return {
1017
+ paths: [
1018
+ `${os.homedir()}/.pyenv/bin`,
1019
+ `${os.homedir()}/.bun/bin`,
1020
+ `${os.homedir()}/.deno/bin`,
1021
+ `${os.homedir()}/.jenv/bin`,
1022
+ `${os.homedir()}/a/random/path`,
1023
+ `${os.homedir()}/.nvm/.bin/2`,
1024
+ `${os.homedir()}/.nvm/.bin/3`
1025
+ ]
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ const oldProcessEnv = structuredClone(process.env);
1031
+
1032
+ process.env['PYENV_ROOT'] = `${os.homedir()}/.pyenv`
1033
+ process.env['BUN_INSTALL'] = `${os.homedir()}/.bun`
1034
+ process.env['DENO_INSTALL'] = `${os.homedir()}/.deno`
1035
+ process.env['JENV'] = `${os.homedir()}/.jenv`
1036
+ process.env['NVM_DIR'] = `${os.homedir()}/.nvm`
1037
+
1038
+ const controller = new ResourceController(resource);
1039
+ const importResult1 = await controller.import({ type: 'path' }, {});
1040
+ expect(importResult1).toMatchObject([
1041
+ {
1042
+ 'core': {
1043
+ 'type': 'path'
1044
+ },
1045
+ 'parameters': {
1046
+ 'paths': [
1047
+ '$PYENV_ROOT/bin',
1048
+ '$BUN_INSTALL/bin',
1049
+ '$DENO_INSTALL/bin',
1050
+ '$JENV/bin',
1051
+ '~/a/random/path',
1052
+ '$NVM_DIR/.bin/2',
1053
+ '$NVM_DIR/.bin/3'
1054
+ ]
1055
+ }
1056
+ }
1057
+ ])
1058
+
1059
+ const importResult2 = await controller.import({ type: 'path' }, { paths: ['$PYENV_ROOT/bin', '$BUN_INSTALL/bin'] });
1060
+ expect(importResult2).toMatchObject([
1061
+ {
1062
+ 'core': {
1063
+ 'type': 'path'
1064
+ },
1065
+ 'parameters': {
1066
+ 'paths': [
1067
+ '$PYENV_ROOT/bin',
1068
+ '$BUN_INSTALL/bin',
1069
+ '$DENO_INSTALL/bin',
1070
+ '$JENV/bin',
1071
+ '~/a/random/path',
1072
+ '$NVM_DIR/.bin/2',
1073
+ '$NVM_DIR/.bin/3'
1074
+ ]
1075
+ }
1076
+ }
1077
+ ])
1078
+
1079
+ process.env = oldProcessEnv;
1080
+ })
955
1081
  });
@@ -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> {
@@ -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 {
@@ -10,8 +11,9 @@ import {
10
11
  resolvePathWithVariables,
11
12
  tildify,
12
13
  untildify
13
- } from '../utils/internal-utils.js';
14
+ } from '../utils/functions.js';
14
15
  import { RefreshContext } from './resource.js';
16
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
15
17
 
16
18
  export interface InputTransformation {
17
19
  to: (input: any) => Promise<any> | any;
@@ -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
@@ -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
+ })
@@ -0,0 +1,231 @@
1
+ import * as fsSync from 'node:fs';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { Readable } from 'node:stream';
5
+ import { finished } from 'node:stream/promises';
6
+
7
+ import { Utils } from './index.js';
8
+
9
+ const SPACE_REGEX = /^\s*$/
10
+
11
+ export class FileUtils {
12
+ static async downloadFile(url: string, destination: string): Promise<void> {
13
+ console.log(`Downloading file from ${url} to ${destination}`);
14
+ const { body } = await fetch(url)
15
+
16
+ const dirname = path.dirname(destination);
17
+ if (!await fs.stat(dirname).then((s) => s.isDirectory()).catch(() => false)) {
18
+ await fs.mkdir(dirname, { recursive: true });
19
+ }
20
+
21
+ const ws = fsSync.createWriteStream(destination)
22
+ // Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that
23
+ await finished(Readable.fromWeb(body as never).pipe(ws));
24
+
25
+ console.log(`Finished downloading to ${destination}`);
26
+ }
27
+
28
+ static async addToShellRc(line: string): Promise<void> {
29
+ const lineToInsert = addLeadingSpacer(
30
+ addTrailingSpacer(line)
31
+ );
32
+
33
+ await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert)
34
+
35
+ function addLeadingSpacer(line: string): string {
36
+ return line.startsWith('\n')
37
+ ? line
38
+ : '\n' + line;
39
+ }
40
+
41
+ function addTrailingSpacer(line: string): string {
42
+ return line.endsWith('\n')
43
+ ? line
44
+ : line + '\n';
45
+ }
46
+ }
47
+
48
+ static async addAllToShellRc(lines: string[]): Promise<void> {
49
+ const formattedLines = '\n' + lines.join('\n') + '\n';
50
+ const shellRc = Utils.getPrimaryShellRc();
51
+
52
+ console.log(`Adding to ${path.basename(shellRc)}:
53
+ ${lines.join('\n')}`)
54
+
55
+ await fs.appendFile(shellRc, formattedLines)
56
+ }
57
+
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
+
69
+ const shellRc = Utils.getPrimaryShellRc();
70
+ console.log(`Saving path: ${value} to ${shellRc}`);
71
+
72
+ if (prepend) {
73
+ await fs.appendFile(shellRc, `\nexport PATH=$PATH:${value};`, { encoding: 'utf8' });
74
+ return;
75
+ }
76
+
77
+ await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
78
+ }
79
+
80
+ static async removeFromFile(filePath: string, search: string): Promise<void> {
81
+ const contents = await fs.readFile(filePath, 'utf8');
82
+ const newContents = contents.replaceAll(search, '');
83
+
84
+ await fs.writeFile(filePath, newContents, 'utf8');
85
+ }
86
+
87
+ static async removeLineFromFile(filePath: string, search: RegExp | string): Promise<void> {
88
+ const file = await fs.readFile(filePath, 'utf8')
89
+ const lines = file.split('\n');
90
+
91
+ let searchRegex;
92
+ let searchString;
93
+
94
+ if (typeof search === 'object') {
95
+ const startRegex = /^([\t ]*)?/;
96
+ const endRegex = /([\t ]*)?/;
97
+
98
+ // Augment regex with spaces criteria to make sure this function is not deleting lines that are comments or has other content.
99
+ searchRegex = search
100
+ ? new RegExp(
101
+ startRegex.source + search.source + endRegex.source,
102
+ search.flags
103
+ )
104
+ : search;
105
+ }
106
+
107
+ if (typeof search === 'string') {
108
+ searchString = search;
109
+ }
110
+
111
+ for (let counter = lines.length; counter >= 0; counter--) {
112
+ if (!lines[counter]) {
113
+ continue;
114
+ }
115
+
116
+ if (searchString && lines[counter].includes(searchString)) {
117
+ lines.splice(counter, 1);
118
+ continue;
119
+ }
120
+
121
+ if (searchRegex && lines[counter].search(searchRegex) !== -1) {
122
+ lines.splice(counter, 1);
123
+ }
124
+ }
125
+
126
+ await fs.writeFile(filePath, lines.join('\n'));
127
+ console.log(`Removed line: ${search} from ${filePath}`)
128
+ }
129
+
130
+ static async removeLineFromShellRc(search: RegExp | string): Promise<void> {
131
+ return FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
132
+ }
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
+
140
+ // Append the string to the end of a file ensuring at least 1 lines of space between.
141
+ // Ex result:
142
+ // something something;
143
+ //
144
+ // newline;
145
+ static appendToFileWithSpacing(file: string, textToInsert: string): string {
146
+ const lines = file.trimEnd().split(/\n/);
147
+ if (lines.length === 0) {
148
+ return textToInsert;
149
+ }
150
+
151
+ const endingNewLines = FileUtils.calculateEndingNewLines(lines);
152
+ const numNewLines = endingNewLines === -1
153
+ ? 0
154
+ : Math.max(0, 2 - endingNewLines);
155
+ return lines.join('\n') + '\n'.repeat(numNewLines) + textToInsert
156
+ }
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
+
208
+ // This is overly complicated but it can be used to insert into any
209
+ // position in the future
210
+ private static calculateEndingNewLines(lines: string[]): number {
211
+ let counter = 0;
212
+ while (true) {
213
+ const line = lines.at(-counter - 1);
214
+
215
+ if (!line) {
216
+ return -1
217
+ }
218
+
219
+ if (!SPACE_REGEX.test(line)) {
220
+ return counter;
221
+ }
222
+
223
+ counter++;
224
+
225
+ // Short circuit here because we don't need to check over 2;
226
+ if (counter > 2) {
227
+ return counter;
228
+ }
229
+ }
230
+ }
231
+ }
@@ -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) {
@@ -2,6 +2,8 @@ import { OS } from 'codify-schemas';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
+ import { getPty, SpawnStatus } from '../pty/index.js';
6
+
5
7
  export function isDebug(): boolean {
6
8
  return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
7
9
  }
@@ -40,6 +42,34 @@ export const Utils = {
40
42
  return os.platform() === 'linux';
41
43
  },
42
44
 
45
+ async isArmArch(): Promise<boolean> {
46
+ const $ = getPty();
47
+ if (!Utils.isMacOS()) {
48
+ // On Linux, check uname -m
49
+ const query = await $.spawn('uname -m');
50
+ return query.data.trim() === 'aarch64' || query.data.trim() === 'arm64';
51
+ }
52
+
53
+ const query = await $.spawn('sysctl -n machdep.cpu.brand_string');
54
+ return /M(\d)/.test(query.data);
55
+ },
56
+
57
+ async isHomebrewInstalled(): Promise<boolean> {
58
+ const $ = getPty();
59
+ const query = await $.spawnSafe('which brew', { interactive: true });
60
+ return query.status === SpawnStatus.SUCCESS;
61
+ },
62
+
63
+ async isRosetta2Installed(): Promise<boolean> {
64
+ if (!Utils.isMacOS()) {
65
+ return false;
66
+ }
67
+
68
+ const $ = getPty();
69
+ const query = await $.spawnSafe('arch -x86_64 /usr/bin/true 2> /dev/null', { interactive: true });
70
+ return query.status === SpawnStatus.SUCCESS;
71
+ },
72
+
43
73
  getShell(): Shell | undefined {
44
74
  const shell = process.env.SHELL || '';
45
75
 
@@ -138,6 +168,13 @@ export const Utils = {
138
168
  path.join(homeDir, '.profile'),
139
169
  ];
140
170
  },
171
+
172
+ async isDirectoryOnPath(directory: string): Promise<boolean> {
173
+ const $ = getPty();
174
+ const { data: pathQuery } = await $.spawn('echo $PATH', { interactive: true });
175
+ const lines = pathQuery.split(':');
176
+ return lines.includes(directory);
177
+ },
141
178
  };
142
179
 
143
180
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { addVariablesToPath, resolvePathWithVariables, splitUserConfig } from './internal-utils.js';
2
+ import { addVariablesToPath, resolvePathWithVariables, splitUserConfig } from './functions.js';
3
3
  import os from 'node:os';
4
4
 
5
5
  describe('Utils tests', () => {
@@ -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',