@codifycli/plugin-core 1.0.0-beta1

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 (152) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +30 -0
  3. package/.github/workflows/release.yaml +19 -0
  4. package/.github/workflows/unit-test-ci.yaml +18 -0
  5. package/.prettierrc.json +1 -0
  6. package/bin/build.js +189 -0
  7. package/dist/bin/build.d.ts +1 -0
  8. package/dist/bin/build.js +80 -0
  9. package/dist/bin/deploy-plugin.d.ts +2 -0
  10. package/dist/bin/deploy-plugin.js +8 -0
  11. package/dist/common/errors.d.ts +8 -0
  12. package/dist/common/errors.js +24 -0
  13. package/dist/entities/change-set.d.ts +24 -0
  14. package/dist/entities/change-set.js +152 -0
  15. package/dist/entities/errors.d.ts +4 -0
  16. package/dist/entities/errors.js +7 -0
  17. package/dist/entities/plan-types.d.ts +25 -0
  18. package/dist/entities/plan-types.js +1 -0
  19. package/dist/entities/plan.d.ts +15 -0
  20. package/dist/entities/plan.js +127 -0
  21. package/dist/entities/plugin.d.ts +16 -0
  22. package/dist/entities/plugin.js +80 -0
  23. package/dist/entities/resource-options.d.ts +31 -0
  24. package/dist/entities/resource-options.js +76 -0
  25. package/dist/entities/resource-types.d.ts +11 -0
  26. package/dist/entities/resource-types.js +1 -0
  27. package/dist/entities/resource.d.ts +42 -0
  28. package/dist/entities/resource.js +303 -0
  29. package/dist/entities/stateful-parameter.d.ts +29 -0
  30. package/dist/entities/stateful-parameter.js +46 -0
  31. package/dist/entities/transform-parameter.d.ts +4 -0
  32. package/dist/entities/transform-parameter.js +2 -0
  33. package/dist/errors.d.ts +4 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/index.d.ts +20 -0
  36. package/dist/index.js +26 -0
  37. package/dist/messages/handlers.d.ts +14 -0
  38. package/dist/messages/handlers.js +134 -0
  39. package/dist/messages/sender.d.ts +11 -0
  40. package/dist/messages/sender.js +57 -0
  41. package/dist/plan/change-set.d.ts +53 -0
  42. package/dist/plan/change-set.js +153 -0
  43. package/dist/plan/plan-types.d.ts +23 -0
  44. package/dist/plan/plan-types.js +1 -0
  45. package/dist/plan/plan.d.ts +66 -0
  46. package/dist/plan/plan.js +328 -0
  47. package/dist/plugin/plugin.d.ts +24 -0
  48. package/dist/plugin/plugin.js +200 -0
  49. package/dist/pty/background-pty.d.ts +21 -0
  50. package/dist/pty/background-pty.js +127 -0
  51. package/dist/pty/index.d.ts +50 -0
  52. package/dist/pty/index.js +20 -0
  53. package/dist/pty/promise-queue.d.ts +5 -0
  54. package/dist/pty/promise-queue.js +26 -0
  55. package/dist/pty/seqeuntial-pty.d.ts +17 -0
  56. package/dist/pty/seqeuntial-pty.js +119 -0
  57. package/dist/pty/vitest.config.d.ts +2 -0
  58. package/dist/pty/vitest.config.js +11 -0
  59. package/dist/resource/config-parser.d.ts +11 -0
  60. package/dist/resource/config-parser.js +21 -0
  61. package/dist/resource/parsed-resource-settings.d.ts +47 -0
  62. package/dist/resource/parsed-resource-settings.js +196 -0
  63. package/dist/resource/resource-controller.d.ts +36 -0
  64. package/dist/resource/resource-controller.js +402 -0
  65. package/dist/resource/resource-settings.d.ts +303 -0
  66. package/dist/resource/resource-settings.js +147 -0
  67. package/dist/resource/resource.d.ts +144 -0
  68. package/dist/resource/resource.js +44 -0
  69. package/dist/resource/stateful-parameter.d.ts +165 -0
  70. package/dist/resource/stateful-parameter.js +94 -0
  71. package/dist/scripts/deploy.d.ts +1 -0
  72. package/dist/scripts/deploy.js +2 -0
  73. package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
  74. package/dist/stateful-parameter/stateful-parameter-controller.js +81 -0
  75. package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
  76. package/dist/stateful-parameter/stateful-parameter.js +43 -0
  77. package/dist/test.d.ts +1 -0
  78. package/dist/test.js +5 -0
  79. package/dist/utils/codify-spawn.d.ts +29 -0
  80. package/dist/utils/codify-spawn.js +136 -0
  81. package/dist/utils/debug.d.ts +2 -0
  82. package/dist/utils/debug.js +10 -0
  83. package/dist/utils/file-utils.d.ts +23 -0
  84. package/dist/utils/file-utils.js +186 -0
  85. package/dist/utils/functions.d.ts +12 -0
  86. package/dist/utils/functions.js +74 -0
  87. package/dist/utils/index.d.ts +46 -0
  88. package/dist/utils/index.js +271 -0
  89. package/dist/utils/internal-utils.d.ts +12 -0
  90. package/dist/utils/internal-utils.js +74 -0
  91. package/dist/utils/load-resources.d.ts +1 -0
  92. package/dist/utils/load-resources.js +46 -0
  93. package/dist/utils/package-json-utils.d.ts +12 -0
  94. package/dist/utils/package-json-utils.js +34 -0
  95. package/dist/utils/pty-local-storage.d.ts +2 -0
  96. package/dist/utils/pty-local-storage.js +2 -0
  97. package/dist/utils/spawn-2.d.ts +5 -0
  98. package/dist/utils/spawn-2.js +7 -0
  99. package/dist/utils/spawn.d.ts +29 -0
  100. package/dist/utils/spawn.js +124 -0
  101. package/dist/utils/utils.d.ts +18 -0
  102. package/dist/utils/utils.js +86 -0
  103. package/dist/utils/verbosity-level.d.ts +5 -0
  104. package/dist/utils/verbosity-level.js +9 -0
  105. package/package.json +59 -0
  106. package/rollup.config.js +24 -0
  107. package/src/common/errors.test.ts +43 -0
  108. package/src/common/errors.ts +31 -0
  109. package/src/errors.ts +8 -0
  110. package/src/index.test.ts +6 -0
  111. package/src/index.ts +30 -0
  112. package/src/messages/handlers.test.ts +329 -0
  113. package/src/messages/handlers.ts +181 -0
  114. package/src/messages/sender.ts +69 -0
  115. package/src/plan/change-set.test.ts +280 -0
  116. package/src/plan/change-set.ts +236 -0
  117. package/src/plan/plan-types.ts +27 -0
  118. package/src/plan/plan.test.ts +413 -0
  119. package/src/plan/plan.ts +499 -0
  120. package/src/plugin/plugin.test.ts +533 -0
  121. package/src/plugin/plugin.ts +291 -0
  122. package/src/pty/background-pty.test.ts +69 -0
  123. package/src/pty/background-pty.ts +154 -0
  124. package/src/pty/index.test.ts +129 -0
  125. package/src/pty/index.ts +66 -0
  126. package/src/pty/promise-queue.ts +33 -0
  127. package/src/pty/seqeuntial-pty.ts +151 -0
  128. package/src/pty/sequential-pty.test.ts +194 -0
  129. package/src/resource/config-parser.ts +42 -0
  130. package/src/resource/parsed-resource-settings.test.ts +186 -0
  131. package/src/resource/parsed-resource-settings.ts +307 -0
  132. package/src/resource/resource-controller-stateful-mode.test.ts +253 -0
  133. package/src/resource/resource-controller.test.ts +1081 -0
  134. package/src/resource/resource-controller.ts +563 -0
  135. package/src/resource/resource-settings.test.ts +1213 -0
  136. package/src/resource/resource-settings.ts +545 -0
  137. package/src/resource/resource.ts +157 -0
  138. package/src/stateful-parameter/stateful-parameter-controller.test.ts +244 -0
  139. package/src/stateful-parameter/stateful-parameter-controller.ts +111 -0
  140. package/src/stateful-parameter/stateful-parameter.ts +160 -0
  141. package/src/utils/debug.ts +11 -0
  142. package/src/utils/file-utils.test.ts +7 -0
  143. package/src/utils/file-utils.ts +231 -0
  144. package/src/utils/functions.ts +103 -0
  145. package/src/utils/index.ts +340 -0
  146. package/src/utils/internal-utils.test.ts +52 -0
  147. package/src/utils/pty-local-storage.ts +3 -0
  148. package/src/utils/test-utils.test.ts +96 -0
  149. package/src/utils/verbosity-level.ts +11 -0
  150. package/tsconfig.json +26 -0
  151. package/tsconfig.test.json +9 -0
  152. package/vitest.config.ts +10 -0
@@ -0,0 +1,545 @@
1
+ import { JSONSchemaType } from 'ajv';
2
+ import { LinuxDistro, OS, StringIndexedObject } from 'codify-schemas';
3
+ import isObjectsEqual from 'lodash.isequal'
4
+ import path from 'node:path';
5
+ import { ZodObject } from 'zod';
6
+
7
+ import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
8
+ import {
9
+ addVariablesToPath,
10
+ areArraysEqual,
11
+ resolvePathWithVariables,
12
+ tildify,
13
+ untildify
14
+ } from '../utils/functions.js';
15
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
16
+ import { RefreshContext } from './resource.js';
17
+
18
+ export interface InputTransformation {
19
+ to: (input: any) => Promise<any> | any;
20
+ from: (current: any, original: any) => Promise<any> | any;
21
+ }
22
+
23
+ /**
24
+ * The configuration and settings for a resource.
25
+ */
26
+ export interface ResourceSettings<T extends StringIndexedObject> {
27
+
28
+ /**
29
+ * The typeId of the resource.
30
+ */
31
+ id: string;
32
+
33
+ /**
34
+ * List of supported operating systems
35
+ */
36
+ operatingSystems: Array<OS>;
37
+
38
+ /**
39
+ * List of supported linux distros
40
+ */
41
+ linuxDistros?: Array<LinuxDistro>;
42
+
43
+ /**
44
+ * Schema to validate user configs with. Must be in the format JSON Schema draft07
45
+ */
46
+ schema?: Partial<JSONSchemaType<T | any>> | ZodObject;
47
+
48
+ /**
49
+ * Mark the resource as sensitive. Defaults to false. This prevents the resource from automatically being imported by init and import.
50
+ * This differs from the parameter level sensitivity which also prevents the parameter value from being displayed in the plan.
51
+ */
52
+ isSensitive?: boolean;
53
+
54
+ /**
55
+ * An optional description of the resource. This does not affect the behavior of the resource.
56
+ */
57
+ description?: string;
58
+
59
+ /**
60
+ * Allow multiple of the same resource to unique. Set truthy if
61
+ * multiples are allowed, for example for applications, there can be multiple copy of the same application installed
62
+ * on the system. Or there can be multiple git repos. Defaults to false.
63
+ */
64
+ allowMultiple?: {
65
+
66
+ /**
67
+ * A set of parameters that uniquely identifies a resource. The value of these parameters is used to determine which
68
+ * resource is which when multiple can exist at the same time. Defaults to the required parameters inside the json
69
+ * schema.
70
+ *
71
+ * For example:
72
+ * If paramA is required, then if resource1.paramA === resource2.paramA then are the same resource.
73
+ * If resource1.paramA !== resource1.paramA, then they are different.
74
+ */
75
+ identifyingParameters?: string[];
76
+
77
+ /**
78
+ * If multiple copies are allowed then a matcher must be defined to match the desired
79
+ * config with one of the resources currently existing on the system. Return null if there is no match.
80
+ *
81
+ * @param current An array of resources found installed on the system
82
+ * @param desired The desired config to match.
83
+ *
84
+ * @return The matched resource.
85
+ */
86
+ matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
87
+
88
+ /**
89
+ * This method if supported by the resource returns an array of parameters that represent all of the possible
90
+ * instances of a resource on the system. An example of this is for the git-repository resource, this method returns
91
+ * a list of directories which are git repositories.
92
+ */
93
+ findAllParameters?: () => Promise<Array<Partial<T>>>
94
+ } | boolean
95
+
96
+ /**
97
+ * If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
98
+ * if the stateful parameter needs to be first uninstalled (cleanup) before the overall resource can be
99
+ * uninstalled. Defaults to false.
100
+ */
101
+ removeStatefulParametersBeforeDestroy?: boolean;
102
+
103
+ /**
104
+ * An array of type ids of resources that this resource depends on. This affects the order in which multiple resources are
105
+ * planned and applied.
106
+ */
107
+ dependencies?: string[];
108
+
109
+ /**
110
+ * Options for configuring parameters operations including overriding the equals function, adding default values
111
+ * and applying any input transformations. Use parameter settings to define stateful parameters as well.
112
+ */
113
+ parameterSettings?: Partial<Record<keyof T, ParameterSetting>>;
114
+
115
+ /**
116
+ * A config level transformation that is only applied to the user supplied desired config. This transformation is allowed
117
+ * to add, remove or modify keys as well as values. Changing this transformation for existing libraries will mess up existing states.
118
+ *
119
+ * @param desired
120
+ */
121
+ transformation?: InputTransformation;
122
+
123
+ /**
124
+ * Customize the import and destory behavior of the resource. By default, <code>codify import</code> and <code>codify destroy</code> will call
125
+ * `refresh()` with every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
126
+ * in the schema and will prompt the user for these values before performing the import or destroy.
127
+ *
128
+ * <b>Example:</b><br>
129
+ * Resource `alias` with parameters
130
+ *
131
+ * ```
132
+ * { alias <b>(*required)</b>: string; value: string; }
133
+ * ```
134
+ *
135
+ * When the user calls `codify import alias`, they will first be prompted to enter the value for `alias`. Refresh
136
+ * is then called with `refresh({ alias: 'user-input', value: null })`. The result returned to the user will then be:
137
+ *
138
+ * ```
139
+ * { type: 'alias', alias: 'user-input', value: 'git push' }
140
+ * ```
141
+ */
142
+ importAndDestroy?: {
143
+ /**
144
+ * Can this resources be imported? If set to false then the codifyCLI will skip over/not consider this
145
+ * resource valid for imports. Defaults to false.
146
+ *
147
+ * Resources that can't be imported in the core library for example are: action resources
148
+ */
149
+ preventImport?: boolean;
150
+
151
+ /**
152
+ * Can this resources be destroyed? If set to false then the codifyCLI will skip over/not consider this
153
+ * resource valid for destroys. Defaults to false.
154
+ */
155
+ preventDestroy?: boolean;
156
+
157
+ /**
158
+ * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
159
+ * from the identifyingParameters for allowMultiple. The `requiredParameters` parameter must be declared if a complex required is declared in
160
+ * the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`).
161
+ * <br>
162
+ * The user will be prompted for the required parameters before the import starts. This is done because for most resources
163
+ * the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
164
+ * chooses which alias the resource is managing).
165
+ *
166
+ * See {@link importAndDestroy} for more information on how importing works.
167
+ */
168
+ requiredParameters?: Array<Partial<keyof T>>;
169
+
170
+ /**
171
+ * Customize which keys will be refreshed in the import. Typically, `refresh()` statements only refresh
172
+ * the parameters provided as the input. Use `refreshKeys` to control which parameter keys are passed in.
173
+ * <br>
174
+ * By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
175
+ * in value can be customized using {@link defaultRefreshValues}
176
+ *
177
+ * See {@link importAndDestroy} for more information on how importing works.
178
+ */
179
+ refreshKeys?: Array<Partial<keyof T>>;
180
+
181
+ /**
182
+ * Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
183
+ *
184
+ * See {@link importAndDestroy} for more information on how importing works.
185
+ */
186
+ defaultRefreshValues?: Partial<T>;
187
+
188
+ /**
189
+ * A custom function that maps the input to what gets passed to refresh for imports. If this is set, then refreshKeys and
190
+ * defaultRefreshValues are ignored.
191
+ *
192
+ * @param input
193
+ * @param context
194
+ */
195
+ refreshMapper?: (input: Partial<T>, context: RefreshContext<T>) => Partial<T>;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * The type of parameter. This value is mainly used to determine a pre-set equality method for comparing the current
201
+ * config with desired config. Certain types will have additional options to help support it. For example the type
202
+ * stateful requires a stateful parameter definition and type array takes an isElementEqual method.
203
+ */
204
+ export type ParameterSettingType =
205
+ 'any'
206
+ | 'array'
207
+ | 'boolean'
208
+ | 'directory'
209
+ | 'number'
210
+ | 'object'
211
+ | 'setting'
212
+ | 'stateful'
213
+ | 'string'
214
+ | 'version';
215
+
216
+ /**
217
+ * Typing information for the parameter setting. This represents a setting on a specific parameter within a
218
+ * resource. Options for configuring parameters operations including overriding the equals function, adding default values
219
+ * and applying any input transformations. See {@link DefaultParameterSetting } for more information.
220
+ * Use parameter settings to define stateful parameters as well.
221
+ */
222
+
223
+ export type ParameterSetting =
224
+ ArrayParameterSetting
225
+ | DefaultParameterSetting
226
+ | StatefulParameterSetting
227
+
228
+ /**
229
+ * The parent class for parameter settings. The options are applicable to array parameter settings
230
+ * as well.
231
+ */
232
+ export interface DefaultParameterSetting {
233
+ /**
234
+ * The type of the value of this parameter. See {@link ParameterSettingType} for the available options. This value
235
+ * is mainly used to determine the equality method when performing diffing.
236
+ */
237
+ type?: ParameterSettingType;
238
+
239
+ /**
240
+ * Mark the field as sensitive. Defaults to false. This has two side effects:
241
+ * 1. When displaying this field in the plan, it will be replaced with asterisks
242
+ * 2. When importing, resources with sensitive fields will be skipped unless the user explicitly allows it.
243
+ */
244
+ isSensitive?: boolean;
245
+
246
+ /**
247
+ * Default value for the parameter. If a value is not provided in the config, then this value will be used.
248
+ */
249
+ default?: unknown;
250
+
251
+ /**
252
+ * A transformation of the input value for this parameter. Two transformations need to be provided: to (from desired to
253
+ * the internal type), and from (from the internal type back to desired). All transformations need to be bi-directional
254
+ * to support imports properly
255
+ *
256
+ * @param input The original parameter value from the desired config.
257
+ */
258
+ transformation?: InputTransformation;
259
+
260
+ /**
261
+ * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
262
+ * This value will override the pre-set equality function from the type. Return true if the desired value is
263
+ * equivalent to the current value.
264
+ *
265
+ * @param desired The desired value.
266
+ * @param current The current value.
267
+ *
268
+ * @return Return true if equal
269
+ */
270
+ isEqual?: ((desired: any, current: any) => boolean) | ParameterSettingType;
271
+
272
+ /**
273
+ * Chose if the resource can be modified instead of re-created when there is a change to this parameter.
274
+ * Defaults to false (re-create).
275
+ *
276
+ * Examples:
277
+ * 1. Settings like git user name and git user email that have setter calls and don't require the re-installation of git
278
+ * 2. AWS profile secret keys that can be updated without the re-installation of AWS CLI
279
+ */
280
+ canModify?: boolean
281
+
282
+ /**
283
+ * This option allows the plan to skip this parameter entirely as it is used for setting purposes only. The value
284
+ * of this parameter is used to configure the resource or other parameters.
285
+ *
286
+ * Examples:
287
+ * 1. homebrew.onlyPlanUserInstalled option will tell homebrew to filter by --installed-on-request. But the value,
288
+ * of the parameter itself (true or false) does not have an impact on the plan
289
+ */
290
+ setting?: boolean
291
+ }
292
+
293
+ /**
294
+ * Array type specific settings. See {@link DefaultParameterSetting } for a full list of options.
295
+ */
296
+ export interface ArrayParameterSetting extends DefaultParameterSetting {
297
+ type: 'array'
298
+
299
+ /**
300
+ * An element level equality function for arrays. The diffing algorithm will take isElementEqual and use it in a
301
+ * O(n^2) equality comparison to determine if the overall array is equal. This value will override the pre-set equality
302
+ * function for arrays (desired === current). Return true if the desired element is equivalent to the current element.
303
+ *
304
+ * @param desired An element of the desired array
305
+ * @param current An element of the current array
306
+ *
307
+ * @return Return true if desired is equivalent to current.
308
+ */
309
+ isElementEqual?: ((desired: any, current: any) => boolean) | ParameterSettingType;
310
+
311
+ /**
312
+ * Filter the contents of the refreshed array by the desired. This way items currently on the system but not
313
+ * in desired don't show up in the plan.
314
+ *
315
+ * <b>For example, for the nvm resource:</b>
316
+ * <ul>
317
+ * <li>Desired (20.18.0, 18.9.0, 16.3.1)</li>
318
+ * <li>Current (20.18.0, 22.1.3, 12.1.0)</li>
319
+ * </ul>
320
+ *
321
+ * Without filtering the plan will be:
322
+ * (~20.18.0, +18.9.0, +16.3.1, -22.1.3, -12.1.0)<br>
323
+ * With filtering the plan is: (~20.18.0, +18.9.0, +16.3.1)
324
+ *
325
+ * As you can see, filtering prevents items currently installed on the system from being removed.
326
+ *
327
+ * Defaults to true.
328
+ */
329
+ filterInStatelessMode?: ((desired: any[], current: any[]) => any[]) | boolean,
330
+
331
+ /**
332
+ * The type of the array item. See {@link ParameterSettingType} for the available options. This value
333
+ * is mainly used to determine the equality method when performing diffing.
334
+ */
335
+ itemType?: ParameterSettingType,
336
+ }
337
+
338
+ /**
339
+ * Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
340
+ * state but is still tied to the overall state of the resource. For example 'homebrew' is represented
341
+ * as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
342
+ * modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
343
+ *
344
+ */
345
+ export interface StatefulParameterSetting extends DefaultParameterSetting {
346
+ type: 'stateful',
347
+
348
+ /**
349
+ * The stateful parameter definition. A stateful parameter is a sub-resource that can hold its own
350
+ * state but is still tied to the overall state of the resource. For example 'homebrew' is represented
351
+ * as a resource and taps, formulas and casks are represented as a stateful parameter. A formula can be installed,
352
+ * modified and removed (has state) but it is still tied to the overall lifecycle of homebrew.
353
+ */
354
+ definition: ArrayStatefulParameter<any, unknown> | StatefulParameter<any, unknown>,
355
+
356
+ /**
357
+ * The order multiple stateful parameters should be applied in. The order is applied in ascending order (1, 2, 3...).
358
+ */
359
+ order?: number,
360
+ }
361
+
362
+ const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
363
+ 'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
364
+ 'directory'(a: unknown, b: unknown) {
365
+ let transformedA = resolvePathWithVariables(untildify(String(a)))
366
+ let transformedB = resolvePathWithVariables(untildify(String(b)))
367
+
368
+ if (transformedA.startsWith('.')) { // Only relative paths start with '.'
369
+ transformedA = path.resolve(transformedA)
370
+ }
371
+
372
+ if (transformedB.startsWith('.')) { // Only relative paths start with '.'
373
+ transformedB = path.resolve(transformedB)
374
+ }
375
+
376
+ // macOS has case-insensitive filesystem by default, Linux is case-sensitive
377
+ const isCaseSensitive = process.platform === 'linux';
378
+ if (!isCaseSensitive) {
379
+ transformedA = transformedA.toLowerCase();
380
+ transformedB = transformedB.toLowerCase();
381
+ }
382
+
383
+ return transformedA === transformedB;
384
+ },
385
+ 'number': (a: unknown, b: unknown) => Number(a) === Number(b),
386
+ 'string': (a: unknown, b: unknown) => String(a) === String(b),
387
+ 'version': (desired: unknown, current: unknown) => String(current).includes(String(desired)),
388
+ 'object': isObjectsEqual,
389
+ }
390
+
391
+ export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, current: unknown) => boolean {
392
+ // Setting parameters do not impact the plan
393
+ if (parameter.setting) {
394
+ return () => true;
395
+ }
396
+
397
+ const isEqual = resolveFnFromEqualsFnOrString(parameter.isEqual);
398
+
399
+ if (parameter.type === 'array') {
400
+ return isEqual ?? areArraysEqual.bind(areArraysEqual, resolveElementEqualsFn(parameter as ArrayParameterSetting))
401
+ }
402
+
403
+ if (parameter.type === 'stateful') {
404
+ return resolveEqualsFn((parameter as StatefulParameterSetting).definition.getSettings())
405
+ }
406
+
407
+ return isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b));
408
+ }
409
+
410
+ export function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean {
411
+ if (parameter.isElementEqual) {
412
+ const elementEq = resolveFnFromEqualsFnOrString(parameter.isElementEqual);
413
+ if (elementEq) {
414
+ return elementEq;
415
+ }
416
+ }
417
+
418
+ if (parameter.itemType && ParameterEqualsDefaults[parameter.itemType]) {
419
+ return ParameterEqualsDefaults[parameter.itemType]!
420
+ }
421
+
422
+ return (a, b) => a === b;
423
+ }
424
+
425
+ // This resolves the fn if it is a string.
426
+ // A string can be specified to use a default equals method
427
+ export function resolveFnFromEqualsFnOrString(
428
+ fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined,
429
+ ): ((a: unknown, b: unknown) => boolean) | undefined {
430
+
431
+ if (fnOrString && typeof fnOrString === 'string') {
432
+ if (!ParameterEqualsDefaults[fnOrString]) {
433
+ throw new Error(`isEqual of type ${fnOrString} was not found`)
434
+ }
435
+
436
+ return ParameterEqualsDefaults[fnOrString]!
437
+ }
438
+
439
+ return fnOrString as ((a: unknown, b: unknown) => boolean) | undefined;
440
+ }
441
+
442
+ const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, InputTransformation>> = {
443
+ 'directory': {
444
+ to: (a: unknown) => resolvePathWithVariables((untildify(String(a)))),
445
+ from(a: unknown, original) {
446
+ if (ParameterEqualsDefaults.directory!(a, original)) {
447
+ return original;
448
+ }
449
+
450
+ return tildify(addVariablesToPath(String(a)))
451
+ },
452
+ },
453
+ 'string': {
454
+ to: String,
455
+ from: String,
456
+ },
457
+ 'boolean': {
458
+ to: Boolean,
459
+ from: Boolean,
460
+ }
461
+ }
462
+
463
+ export function resolveParameterTransformFn(
464
+ parameter: ParameterSetting
465
+ ): InputTransformation | undefined {
466
+
467
+ if (parameter.type === 'stateful' && !parameter.transformation) {
468
+ const sp = (parameter as StatefulParameterSetting).definition.getSettings();
469
+ if (sp.transformation) {
470
+ return (parameter as StatefulParameterSetting).definition?.getSettings()?.transformation
471
+ }
472
+
473
+ return sp.type ? ParameterTransformationDefaults[sp.type] : undefined;
474
+ }
475
+
476
+ if (parameter.type === 'array'
477
+ && (parameter as ArrayParameterSetting).itemType
478
+ && ParameterTransformationDefaults[(parameter as ArrayParameterSetting).itemType!]
479
+ && !parameter.transformation
480
+ ) {
481
+ const itemType = (parameter as ArrayParameterSetting).itemType!;
482
+ const itemTransformation = ParameterTransformationDefaults[itemType]!;
483
+
484
+ return {
485
+ to(input: unknown[]) {
486
+ return input.map((i) => itemTransformation.to(i))
487
+ },
488
+ from(input: unknown[], original) {
489
+ return input.map((i, idx) => {
490
+ const originalElement = Array.isArray(original)
491
+ ? original.find((o) => resolveElementEqualsFn(parameter as ArrayParameterSetting)(o, i)) ?? original[idx]
492
+ : original;
493
+
494
+ return itemTransformation.from(i, originalElement);
495
+ })
496
+ }
497
+ }
498
+ }
499
+
500
+ return parameter.transformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined;
501
+ }
502
+
503
+ export function resolveMatcher<T extends StringIndexedObject>(
504
+ settings: ParsedResourceSettings<T>
505
+ ): (desired: Partial<T>, current: Partial<T>) => boolean {
506
+ return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
507
+ ? ((desired: Partial<T>, current: Partial<T>) => {
508
+ if (!desired || !current) {
509
+ return false;
510
+ }
511
+
512
+ if (!settings.allowMultiple) {
513
+ throw new Error(`Matching only works when allow multiple is enabled. Type: ${settings.id}`)
514
+ }
515
+
516
+ const requiredParameters = typeof settings.allowMultiple === 'object'
517
+ ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? []
518
+ : (settings.schema?.required as string[]) ?? []
519
+
520
+ return requiredParameters.every((key) => {
521
+ const currentParameter = current[key];
522
+ const desiredParameter = desired[key];
523
+
524
+ // If both desired and current don't have a certain parameter then we assume they are the same
525
+ if (!currentParameter && !desiredParameter) {
526
+ return true;
527
+ }
528
+
529
+ if (!currentParameter) {
530
+ console.warn(`Unable to find required parameter for current ${currentParameter}`)
531
+ return false;
532
+ }
533
+
534
+ if (!desiredParameter) {
535
+ console.warn(`Unable to find required parameter for current ${currentParameter}`)
536
+ return false;
537
+ }
538
+
539
+ const parameterSetting = settings.parameterSettings?.[key];
540
+ const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null
541
+ return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
542
+ })
543
+ })
544
+ : settings.allowMultiple.matcher
545
+ }
@@ -0,0 +1,157 @@
1
+ import { StringIndexedObject, } from 'codify-schemas';
2
+
3
+ import { ParameterChange } from '../plan/change-set.js';
4
+ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
5
+ import { ResourceSettings } from './resource-settings.js';
6
+
7
+ export interface RefreshContext<T extends StringIndexedObject> {
8
+ isStateful: boolean;
9
+ commandType: 'destroy' | 'import' | 'plan' | 'validationPlan';
10
+ originalDesiredConfig: Partial<T> | null;
11
+ }
12
+
13
+ /**
14
+ * A resource represents an object on the system (application, CLI tool, or setting)
15
+ * that has state and can be created and destroyed. Examples of resources include CLI tools
16
+ * like homebrew, docker, and xcode-tools; applications like Google Chrome, Zoom, and OpenVPN;
17
+ * and settings like AWS profiles, git configs and system preference settings.
18
+ */
19
+ export abstract class Resource<T extends StringIndexedObject> {
20
+
21
+ /**
22
+ * Return the settings for the resource. Consult the typing for {@link ResourceSettings} for
23
+ * a description of the options.
24
+ *
25
+ * **Parameters**:
26
+ * - id: The id of the resource. This translates to the `type` id parameter in codify.json configs
27
+ * - schema: A JSON schema used to validate user input
28
+ * - allowMultiple: Allow multiple copies of the resource to exist at the same time. If true then,
29
+ * a matcher must be defined that matches a user defined config and a single resource on the system.
30
+ * - removeStatefulParametersBeforeDestory: Call the delete methods of stateful parameters before destorying
31
+ * the base resource. Defaults to false.
32
+ * - dependencies: Specify the ids of any resources that this resource depends on
33
+ * - parameterSettings: Parameter specific settings. Use this to define custom equals functions, default values
34
+ * and input transformations
35
+ * - inputTransformation: Transform the input value.
36
+ *
37
+ * @return ResourceSettings The resource settings
38
+ */
39
+ abstract getSettings(): ResourceSettings<T>;
40
+
41
+ async initialize(): Promise<void> {
42
+ };
43
+
44
+ /**
45
+ * Add custom validation logic in-addition to the default schema validation.
46
+ * In this method throw an error if the object did not validate. The message of the
47
+ * error will be shown to the user.
48
+ * @param parameters
49
+ */
50
+ async validate(parameters: Partial<T>): Promise<void> {
51
+ };
52
+
53
+ /**
54
+ * Return the status of the resource on the system. If multiple resources exist, then return all instances of
55
+ * the resource back. Query for the individual parameters specified in the parameter param.
56
+ * Return null if the resource does not exist.
57
+ *
58
+ * Example (Android Studios Resource):
59
+ * 1. Receive Input:
60
+ * ```
61
+ * {
62
+ * name: 'Android Studios.app'
63
+ * directory: '/Application',
64
+ * version: '2023.2'
65
+ * }
66
+ * ```
67
+ * 2. Query the system for any installed Android studio versions.
68
+ * 3. In this example we find that there is an 2023.2 version installed and an
69
+ * additional 2024.3-beta version installed as well.
70
+ * 4. We would return:
71
+ * ```
72
+ * [
73
+ * { name: 'Android Studios.app', directory: '/Application', version: '2023.2' },
74
+ * { name: 'Android Studios Preview.app', directory: '/Application', version: '2024.3' },
75
+ * ]
76
+ * ```
77
+ *
78
+ * @param parameters The parameters to refresh. In stateless mode this will be the parameters
79
+ * of the desired config. In stateful mode, this will be parameters of the state config + the desired
80
+ * config of any new parameters.
81
+ *
82
+ * @param context Context surrounding the request
83
+ *
84
+ * @return A config or an array of configs representing the status of the resource on the
85
+ * system currently
86
+ */
87
+ abstract refresh(parameters: Partial<T>, context: RefreshContext<T>): Promise<Array<Partial<T>> | Partial<T> | null>;
88
+
89
+ /**
90
+ * Create the resource (install) based on the parameters passed in. Only the desired parameters will
91
+ * be non-null because in a CREATE plan, the current value is null.
92
+ *
93
+ * Example (Android Studios Resource):
94
+ * 1. We receive a plan of:
95
+ * ```
96
+ * Plan {
97
+ * desiredConfig: {
98
+ * name: 'Android Studios.app',
99
+ * directory: '/Application',
100
+ * version: '2023.2'
101
+ * }
102
+ * currentConfig: null,
103
+ * }
104
+ * ```
105
+ * 2. Install version Android Studios 2023.2 and then return.
106
+ *
107
+ * @param plan The plan of what to install. Use only the desiredConfig because currentConfig is null.
108
+ */
109
+ abstract create(plan: CreatePlan<T>): Promise<void>;
110
+
111
+ /**
112
+ * Modify a single parameter of a resource. Modify is optional to override and is only called
113
+ * when a resourceSetting was set to `canModify = true`. This method should only modify
114
+ * a single parameter at a time as specified by the first parameter: ParameterChange.
115
+ *
116
+ * Example (AWS Profile Resource):
117
+ * 1. We receive a parameter change of:
118
+ * ```
119
+ * {
120
+ * name: 'awsAccessKeyId',
121
+ * operation: ParameterOperation.MODIFY,
122
+ * newValue: '123456',
123
+ * previousValue: 'abcdef'
124
+ * }
125
+ * ```
126
+ * 2. Use an if statement to only apply this operation for the parameter `awsAccessKeyId`
127
+ * 3. Update the value of the `aws_access_key_id` to the `newValue` specified in the parameter change
128
+ *
129
+ * @param pc ParameterChange, the parameter name and values to modify on the resource
130
+ * @param plan The overall plan that triggered the modify operation
131
+ */
132
+ async modify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void> {
133
+ };
134
+
135
+ /**
136
+ * Destroy the resource (uninstall) based on the parameters passed in. Only the current parameters will
137
+ * be non-null because in a DESTROY plan, the desired value is null. This method will only be called in
138
+ * stateful mode.
139
+ *
140
+ * Example (Android Studios Resource):
141
+ * 1. We receive a plan of:
142
+ * ```
143
+ * Plan {
144
+ * currentConfig: {
145
+ * name: 'Android Studios.app',
146
+ * directory: '/Application',
147
+ * version: '2022.4'
148
+ * },
149
+ * desiredConfig: null
150
+ * }
151
+ * ```
152
+ * 2. Uninstall version Android Studios 2022.4 and then return.
153
+ *
154
+ * @param plan The plan of what to uninstall. Use only the currentConfig because desiredConfig is null.
155
+ */
156
+ abstract destroy(plan: DestroyPlan<T>): Promise<void>;
157
+ }