codify-plugin-lib 1.0.106 → 1.0.108

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.
@@ -2,11 +2,13 @@ import { Plugin } from '../plugin/plugin.js';
2
2
  export declare class MessageHandler {
3
3
  private ajv;
4
4
  private readonly plugin;
5
- private messageSchemaValidator;
5
+ private messageSchemaValidatorV1;
6
+ private messageSchemaValidatorV2;
6
7
  private requestValidators;
7
8
  private responseValidators;
8
9
  constructor(plugin: Plugin);
9
10
  onMessage(message: unknown): Promise<void>;
10
11
  private validateMessage;
12
+ private validateMessageV2;
11
13
  private handleErrors;
12
14
  }
@@ -1,6 +1,6 @@
1
1
  import { Ajv } from 'ajv';
2
2
  import addFormats from 'ajv-formats';
3
- import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
3
+ import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
4
4
  import { SudoError } from '../errors.js';
5
5
  const SupportedRequests = {
6
6
  'initialize': {
@@ -40,7 +40,8 @@ const SupportedRequests = {
40
40
  export class MessageHandler {
41
41
  ajv;
42
42
  plugin;
43
- messageSchemaValidator;
43
+ messageSchemaValidatorV1;
44
+ messageSchemaValidatorV2;
44
45
  requestValidators;
45
46
  responseValidators;
46
47
  constructor(plugin) {
@@ -48,7 +49,8 @@ export class MessageHandler {
48
49
  addFormats.default(this.ajv);
49
50
  this.ajv.addSchema(ResourceSchema);
50
51
  this.plugin = plugin;
51
- this.messageSchemaValidator = this.ajv.compile(IpcMessageSchema);
52
+ this.messageSchemaValidatorV1 = this.ajv.compile(IpcMessageSchema);
53
+ this.messageSchemaValidatorV2 = this.ajv.compile(IpcMessageV2Schema);
52
54
  this.requestValidators = new Map(Object.entries(SupportedRequests)
53
55
  .map(([k, v]) => [k, this.ajv.compile(v.requestValidator)]));
54
56
  this.responseValidators = new Map(Object.entries(SupportedRequests)
@@ -56,8 +58,8 @@ export class MessageHandler {
56
58
  }
57
59
  async onMessage(message) {
58
60
  try {
59
- if (!this.validateMessage(message)) {
60
- throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`);
61
+ if (!this.validateMessageV2(message) && !this.validateMessage(message)) {
62
+ throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidatorV1.errors, null, 2)}`);
61
63
  }
62
64
  if (!this.requestValidators.has(message.cmd)) {
63
65
  throw new Error(`Plugin: ${this.plugin}. Unsupported message: ${message.cmd}`);
@@ -74,6 +76,8 @@ export class MessageHandler {
74
76
  process.send({
75
77
  cmd: message.cmd + '_Response',
76
78
  data: result,
79
+ // @ts-expect-error TS2239
80
+ requestId: message.requestId || undefined,
77
81
  status: MessageStatus.SUCCESS,
78
82
  });
79
83
  }
@@ -82,7 +86,10 @@ export class MessageHandler {
82
86
  }
83
87
  }
84
88
  validateMessage(message) {
85
- return this.messageSchemaValidator(message);
89
+ return this.messageSchemaValidatorV1(message);
90
+ }
91
+ validateMessageV2(message) {
92
+ return this.messageSchemaValidatorV2(message);
86
93
  }
87
94
  handleErrors(message, e) {
88
95
  if (!message) {
@@ -91,11 +98,13 @@ export class MessageHandler {
91
98
  if (!message.hasOwnProperty('cmd')) {
92
99
  return;
93
100
  }
94
- // @ts-ignore
101
+ // @ts-expect-error TS2239
95
102
  const cmd = message.cmd + '_Response';
96
103
  if (e instanceof SudoError) {
97
104
  return process.send?.({
98
105
  cmd,
106
+ // @ts-expect-error TS2239
107
+ requestId: message.requestId || undefined,
99
108
  data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
100
109
  status: MessageStatus.ERROR,
101
110
  });
@@ -103,6 +112,8 @@ export class MessageHandler {
103
112
  const isDebug = process.env.DEBUG?.includes('*') ?? false;
104
113
  process.send?.({
105
114
  cmd,
115
+ // @ts-expect-error TS2239
116
+ requestId: message.requestId || undefined,
106
117
  data: isDebug ? e.stack : e.message,
107
118
  status: MessageStatus.ERROR,
108
119
  });
@@ -1,6 +1,6 @@
1
1
  import { StringIndexedObject } from 'codify-schemas';
2
2
  import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
3
- import { ArrayParameterSetting, DefaultParameterSetting, ResourceSettings } from './resource-settings.js';
3
+ import { ArrayParameterSetting, DefaultParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
4
4
  export interface ParsedStatefulParameterSetting extends DefaultParameterSetting {
5
5
  type: 'stateful';
6
6
  controller: StatefulParameterController<any, unknown>;
@@ -30,7 +30,7 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
30
30
  get statefulParameters(): Map<keyof T, StatefulParameterController<T, T[keyof T]>>;
31
31
  get parameterSettings(): Record<keyof T, ParsedParameterSetting>;
32
32
  get defaultValues(): Partial<Record<keyof T, unknown>>;
33
- get inputTransformations(): Partial<Record<keyof T, (a: unknown) => unknown>>;
33
+ get inputTransformations(): Partial<Record<keyof T, (a: unknown, parameter: ParameterSetting) => unknown>>;
34
34
  get statefulParameterOrder(): Map<keyof T, number>;
35
35
  private validateSettings;
36
36
  private validateParameterEqualsFn;
@@ -83,7 +83,7 @@ export class ParsedResourceSettings {
83
83
  return {};
84
84
  }
85
85
  return Object.fromEntries(Object.entries(this.settings.parameterSettings)
86
- .filter(([, v]) => resolveParameterTransformFn(v) !== undefined)
86
+ .filter(([_, v]) => resolveParameterTransformFn(v) !== undefined)
87
87
  .map(([k, v]) => [k, resolveParameterTransformFn(v)]));
88
88
  });
89
89
  }
@@ -230,7 +230,7 @@ ${JSON.stringify(refresh, null, 2)}
230
230
  if (config[key] === undefined || !inputTransformation) {
231
231
  continue;
232
232
  }
233
- config[key] = await inputTransformation(config[key]);
233
+ config[key] = await inputTransformation(config[key], this.settings.parameterSettings[key]);
234
234
  }
235
235
  if (this.settings.inputTransformation) {
236
236
  const { parameters, coreParameters } = splitUserConfig(config);
@@ -216,4 +216,4 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
216
216
  }
217
217
  export declare function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, current: unknown) => boolean;
218
218
  export declare function resolveFnFromEqualsFnOrString(fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined): ((a: unknown, b: unknown) => boolean) | undefined;
219
- export declare function resolveParameterTransformFn(parameter: ParameterSetting): ((input: any) => Promise<any> | any) | undefined;
219
+ export declare function resolveParameterTransformFn(parameter: ParameterSetting): ((input: any, parameter: ParameterSetting) => Promise<any> | any) | undefined;
@@ -35,6 +35,12 @@ export function resolveFnFromEqualsFnOrString(fnOrString) {
35
35
  }
36
36
  const ParameterTransformationDefaults = {
37
37
  'directory': (a) => path.resolve(untildify(String(a))),
38
+ 'stateful': (a, b) => {
39
+ const sp = b;
40
+ return (sp.definition?.getSettings()?.inputTransformation)
41
+ ? (sp.definition.getSettings().inputTransformation(a))
42
+ : a;
43
+ },
38
44
  'string': String,
39
45
  };
40
46
  export function resolveParameterTransformFn(parameter) {
@@ -73,6 +73,9 @@ export function untildify(pathWithTilde) {
73
73
  return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
74
74
  }
75
75
  export function areArraysEqual(isElementEqual, desired, current) {
76
+ if (!desired || !current) {
77
+ return false;
78
+ }
76
79
  if (!Array.isArray(desired) || !Array.isArray(current)) {
77
80
  throw new Error(`A non-array value:
78
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.106",
3
+ "version": "1.0.108",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "ajv": "^8.12.0",
16
16
  "ajv-formats": "^2.1.1",
17
- "codify-schemas": "1.0.52",
17
+ "codify-schemas": "1.0.53",
18
18
  "@npmcli/promise-spawn": "^7.0.1",
19
19
  "uuid": "^10.0.0",
20
20
  "lodash.isequal": "^4.5.0"
@@ -207,7 +207,6 @@ describe('Message handler tests', () => {
207
207
  it('handles errors for apply (destroy)', async () => {
208
208
  const resource = new TestResource()
209
209
  const plugin = testPlugin(resource);
210
-
211
210
  const handler = new MessageHandler(plugin);
212
211
 
213
212
  process.send = (message) => {
@@ -228,6 +227,71 @@ describe('Message handler tests', () => {
228
227
  }
229
228
  }
230
229
  })).rejects.to.not.throw;
230
+
231
+ process.send = undefined;
232
+ })
233
+
234
+ it('Supports ipc message v2 (success)', async () => {
235
+ const resource = new TestResource()
236
+ const plugin = testPlugin(resource);
237
+ const handler = new MessageHandler(plugin);
238
+
239
+ process.send = (message) => {
240
+ console.log(message)
241
+ expect(message).toMatchObject({
242
+ cmd: 'plan_Response',
243
+ requestId: 'abcdef',
244
+ status: MessageStatus.SUCCESS,
245
+ })
246
+ return true;
247
+ }
248
+
249
+ await expect(handler.onMessage({
250
+ cmd: 'plan',
251
+ requestId: 'abcdef',
252
+ data: {
253
+ desired: {
254
+ type: 'type',
255
+ name: 'name',
256
+ prop1: 'A',
257
+ prop2: 'B',
258
+ },
259
+ isStateful: false,
260
+ }
261
+ })).resolves.to.eq(undefined);
262
+
263
+ process.send = undefined;
264
+ })
265
+
266
+ it('Supports ipc message v2 (error)', async () => {
267
+ const resource = new TestResource()
268
+ const plugin = testPlugin(resource);
269
+ const handler = new MessageHandler(plugin);
270
+
271
+ process.send = (message) => {
272
+ expect(message).toMatchObject({
273
+ cmd: 'apply_Response',
274
+ requestId: 'abcdef',
275
+ status: MessageStatus.ERROR,
276
+ })
277
+ return true;
278
+ }
279
+
280
+ await expect(handler.onMessage({
281
+ cmd: 'apply', // Supposed to be a plan so that's why it throws
282
+ requestId: 'abcdef',
283
+ data: {
284
+ desired: {
285
+ type: 'type',
286
+ name: 'name',
287
+ prop1: 'A',
288
+ prop2: 'B',
289
+ },
290
+ isStateful: false,
291
+ }
292
+ })).resolves.to.eq(undefined);
293
+
294
+ process.send = undefined;
231
295
  })
232
296
  });
233
297
 
@@ -11,6 +11,8 @@ import {
11
11
  InitializeResponseDataSchema,
12
12
  IpcMessage,
13
13
  IpcMessageSchema,
14
+ IpcMessageV2,
15
+ IpcMessageV2Schema,
14
16
  MessageStatus,
15
17
  PlanRequestDataSchema,
16
18
  PlanResponseDataSchema,
@@ -61,7 +63,8 @@ const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) =
61
63
  export class MessageHandler {
62
64
  private ajv: Ajv;
63
65
  private readonly plugin: Plugin;
64
- private messageSchemaValidator: ValidateFunction;
66
+ private messageSchemaValidatorV1: ValidateFunction;
67
+ private messageSchemaValidatorV2: ValidateFunction;
65
68
  private requestValidators: Map<string, ValidateFunction>;
66
69
  private responseValidators: Map<string, ValidateFunction>;
67
70
 
@@ -71,7 +74,9 @@ export class MessageHandler {
71
74
  this.ajv.addSchema(ResourceSchema);
72
75
  this.plugin = plugin;
73
76
 
74
- this.messageSchemaValidator = this.ajv.compile(IpcMessageSchema);
77
+ this.messageSchemaValidatorV1 = this.ajv.compile(IpcMessageSchema);
78
+ this.messageSchemaValidatorV2 = this.ajv.compile(IpcMessageV2Schema);
79
+
75
80
  this.requestValidators = new Map(
76
81
  Object.entries(SupportedRequests)
77
82
  .map(([k, v]) => [k, this.ajv.compile(v.requestValidator)])
@@ -84,8 +89,8 @@ export class MessageHandler {
84
89
 
85
90
  async onMessage(message: unknown): Promise<void> {
86
91
  try {
87
- if (!this.validateMessage(message)) {
88
- throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`);
92
+ if (!this.validateMessageV2(message) && !this.validateMessage(message)) {
93
+ throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidatorV1.errors, null, 2)}`);
89
94
  }
90
95
 
91
96
  if (!this.requestValidators.has(message.cmd)) {
@@ -107,6 +112,8 @@ export class MessageHandler {
107
112
  process.send!({
108
113
  cmd: message.cmd + '_Response',
109
114
  data: result,
115
+ // @ts-expect-error TS2239
116
+ requestId: message.requestId || undefined,
110
117
  status: MessageStatus.SUCCESS,
111
118
  })
112
119
 
@@ -116,7 +123,11 @@ export class MessageHandler {
116
123
  }
117
124
 
118
125
  private validateMessage(message: unknown): message is IpcMessage {
119
- return this.messageSchemaValidator(message);
126
+ return this.messageSchemaValidatorV1(message);
127
+ }
128
+
129
+ private validateMessageV2(message: unknown): message is IpcMessageV2 {
130
+ return this.messageSchemaValidatorV2(message);
120
131
  }
121
132
 
122
133
  private handleErrors(message: unknown, e: Error) {
@@ -128,12 +139,14 @@ export class MessageHandler {
128
139
  return;
129
140
  }
130
141
 
131
- // @ts-ignore
142
+ // @ts-expect-error TS2239
132
143
  const cmd = message.cmd + '_Response';
133
144
 
134
145
  if (e instanceof SudoError) {
135
146
  return process.send?.({
136
147
  cmd,
148
+ // @ts-expect-error TS2239
149
+ requestId: message.requestId || undefined,
137
150
  data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
138
151
  status: MessageStatus.ERROR,
139
152
  })
@@ -143,6 +156,8 @@ export class MessageHandler {
143
156
 
144
157
  process.send?.({
145
158
  cmd,
159
+ // @ts-expect-error TS2239
160
+ requestId: message.requestId || undefined,
146
161
  data: isDebug ? e.stack : e.message,
147
162
  status: MessageStatus.ERROR,
148
163
  })
@@ -133,7 +133,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
133
133
  });
134
134
  }
135
135
 
136
- get inputTransformations(): Partial<Record<keyof T, (a: unknown) => unknown>> {
136
+ get inputTransformations(): Partial<Record<keyof T, (a: unknown, parameter: ParameterSetting) => unknown>> {
137
137
  return this.getFromCacheOrCreate('inputTransformations', () => {
138
138
  if (!this.settings.parameterSettings) {
139
139
  return {};
@@ -141,7 +141,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
141
141
 
142
142
  return Object.fromEntries(
143
143
  Object.entries(this.settings.parameterSettings)
144
- .filter(([, v]) => resolveParameterTransformFn(v!) !== undefined)
144
+ .filter(([_, v]) => resolveParameterTransformFn(v!) !== undefined)
145
145
  .map(([k, v]) => [k, resolveParameterTransformFn(v!)] as const)
146
146
  ) as Record<keyof T, (a: unknown) => unknown>;
147
147
  });
@@ -401,12 +401,19 @@ describe('Resource tests', () => {
401
401
  type: 'version'
402
402
  }
403
403
  }
404
+
404
405
  override async refresh(desired: any, config: Partial<any>): Promise<any> {
405
406
  return null;
406
407
  }
407
- override async add(valueToAdd: any, plan: Plan<any>): Promise<void> {}
408
- override async modify(newValue: any, previousValue: any, plan: Plan<any>): Promise<void> {}
409
- override async remove(valueToRemove: any, plan: Plan<any>): Promise<void> {}
408
+
409
+ override async add(valueToAdd: any, plan: Plan<any>): Promise<void> {
410
+ }
411
+
412
+ override async modify(newValue: any, previousValue: any, plan: Plan<any>): Promise<void> {
413
+ }
414
+
415
+ override async remove(valueToRemove: any, plan: Plan<any>): Promise<void> {
416
+ }
410
417
  }
411
418
 
412
419
  const parameter2 = new class extends ArrayStatefulParameter<any, any> {
@@ -420,8 +427,12 @@ describe('Resource tests', () => {
420
427
  override async refresh(desired: any[] | null, config: Partial<any>): Promise<any[] | null> {
421
428
  return null;
422
429
  }
423
- override async addItem(item: any, plan: Plan<any>): Promise<void> {}
424
- override async removeItem(item: any, plan: Plan<any>): Promise<void> {}
430
+
431
+ override async addItem(item: any, plan: Plan<any>): Promise<void> {
432
+ }
433
+
434
+ override async removeItem(item: any, plan: Plan<any>): Promise<void> {
435
+ }
425
436
  }
426
437
 
427
438
  const p1Spy = spy(parameter1);
@@ -471,8 +482,12 @@ describe('Resource tests', () => {
471
482
  async refresh(desired: any[] | null, config: Partial<any>): Promise<any[] | null> {
472
483
  return ['20']
473
484
  }
474
- async addItem(item: any, plan: Plan<any>): Promise<void> {}
475
- async removeItem(item: any, plan: Plan<any>): Promise<void> {}
485
+
486
+ async addItem(item: any, plan: Plan<any>): Promise<void> {
487
+ }
488
+
489
+ async removeItem(item: any, plan: Plan<any>): Promise<void> {
490
+ }
476
491
  })
477
492
 
478
493
  const resource = new class extends TestResource {
@@ -318,7 +318,7 @@ ${JSON.stringify(refresh, null, 2)}
318
318
  continue;
319
319
  }
320
320
 
321
- (config as Record<string, unknown>)[key] = await inputTransformation(config[key]);
321
+ (config as Record<string, unknown>)[key] = await inputTransformation(config[key], this.settings.parameterSettings![key]!);
322
322
  }
323
323
 
324
324
  if (this.settings.inputTransformation) {
@@ -818,4 +818,122 @@ describe('Resource parameter tests', () => {
818
818
  operation: ResourceOperation.RECREATE,
819
819
  })
820
820
  });
821
+
822
+ it('Transforms input parameters', async () => {
823
+ const resource = new class extends TestResource {
824
+ getSettings(): ResourceSettings<TestConfig> {
825
+ return {
826
+ id: 'resourceType',
827
+ parameterSettings: {
828
+ propD: {
829
+ type: 'array',
830
+ inputTransformation: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
831
+ Object.entries(h)
832
+ .map(([k, v]) => [
833
+ k,
834
+ typeof v === 'boolean'
835
+ ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
836
+ : v,
837
+ ])
838
+ )
839
+ )
840
+ }
841
+ }
842
+ }
843
+ }
844
+
845
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
846
+ expect(parameters.propD[0].AddKeysToAgent).to.eq('yes')
847
+ expect(parameters.propD[1].AddKeysToAgent).to.eq('yes')
848
+ expect(parameters.propD[1].UseKeychain).to.eq('yes')
849
+ expect(parameters.propD[2].PasswordAuthentication).to.eq('yes')
850
+
851
+ return null;
852
+ }
853
+ }
854
+
855
+ const controller = new ResourceController(resource);
856
+ await controller.plan({
857
+ type: 'resourceType',
858
+ propD: [
859
+ {
860
+ Host: 'new.com',
861
+ AddKeysToAgent: true,
862
+ IdentityFile: 'id_ed25519'
863
+ },
864
+ {
865
+ Host: 'github.com',
866
+ AddKeysToAgent: true,
867
+ UseKeychain: true,
868
+ },
869
+ {
870
+ Match: 'User bob,joe,phil',
871
+ PasswordAuthentication: true,
872
+ }
873
+ ]
874
+ });
875
+
876
+ })
877
+
878
+ it('Transforms input parameters for stateful parameters', async () => {
879
+ const sp = new class extends TestStatefulParameter {
880
+ getSettings(): ResourceSettings<TestConfig> {
881
+ return {
882
+ type: 'array',
883
+ inputTransformation: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
884
+ Object.entries(h)
885
+ .map(([k, v]) => [
886
+ k,
887
+ typeof v === 'boolean'
888
+ ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
889
+ : v,
890
+ ])
891
+ )
892
+ )
893
+ }
894
+ }
895
+
896
+ async refresh(desired: any): Promise<Partial<TestConfig> | null> {
897
+ expect(desired[0].AddKeysToAgent).to.eq('yes')
898
+ expect(desired[1].AddKeysToAgent).to.eq('yes')
899
+ expect(desired[1].UseKeychain).to.eq('yes')
900
+ expect(desired[2].PasswordAuthentication).to.eq('yes')
901
+
902
+ return null;
903
+ }
904
+ }
905
+
906
+ const resource = new class extends TestResource {
907
+ getSettings(): ResourceSettings<TestConfig> {
908
+ return {
909
+ id: 'resourceType',
910
+ parameterSettings: {
911
+ propD: { type: 'stateful', definition: sp }
912
+ }
913
+ }
914
+ }
915
+ }
916
+
917
+ const controller = new ResourceController(resource);
918
+ await controller.plan({
919
+ type: 'resourceType',
920
+ propD: [
921
+ {
922
+ Host: 'new.com',
923
+ AddKeysToAgent: true,
924
+ IdentityFile: 'id_ed25519'
925
+ },
926
+ {
927
+ Host: 'github.com',
928
+ AddKeysToAgent: true,
929
+ UseKeychain: true,
930
+ },
931
+ {
932
+ Match: 'User bob,joe,phil',
933
+ PasswordAuthentication: true,
934
+ }
935
+ ]
936
+ });
937
+
938
+ })
821
939
  })
@@ -303,13 +303,19 @@ export function resolveFnFromEqualsFnOrString(
303
303
  return fnOrString as ((a: unknown, b: unknown) => boolean) | undefined;
304
304
  }
305
305
 
306
- const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, (input: any) => Promise<any> | any>> = {
306
+ const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, (input: any, parameter: ParameterSetting) => Promise<any> | any>> = {
307
307
  'directory': (a: unknown) => path.resolve(untildify(String(a))),
308
+ 'stateful': (a: unknown, b: ParameterSetting) => {
309
+ const sp = b as StatefulParameterSetting;
310
+ return (sp.definition?.getSettings()?.inputTransformation)
311
+ ? (sp.definition.getSettings().inputTransformation!(a))
312
+ : a;
313
+ },
308
314
  'string': String,
309
315
  }
310
316
 
311
317
  export function resolveParameterTransformFn(
312
318
  parameter: ParameterSetting
313
- ): ((input: any) => Promise<any> | any) | undefined {
319
+ ): ((input: any, parameter: ParameterSetting) => Promise<any> | any) | undefined {
314
320
  return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined;
315
321
  }
@@ -114,6 +114,10 @@ export function areArraysEqual(
114
114
  desired: unknown,
115
115
  current: unknown
116
116
  ): boolean {
117
+ if (!desired || !current) {
118
+ return false;
119
+ }
120
+
117
121
  if (!Array.isArray(desired) || !Array.isArray(current)) {
118
122
  throw new Error(`A non-array value:
119
123