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
@@ -12,7 +12,7 @@ import {
12
12
  PlanRequestData,
13
13
  PlanResponseData,
14
14
  ResourceConfig,
15
- ResourceJson,
15
+ ResourceJson, SetVerbosityRequestData,
16
16
  ValidateRequestData,
17
17
  ValidateResponseData
18
18
  } from 'codify-schemas';
@@ -89,7 +89,7 @@ export class Plugin {
89
89
 
90
90
  const resource = this.resourceControllers.get(data.type)!;
91
91
 
92
- const schema = resource.settings.schema as JSONSchemaType<any> | undefined;
92
+ const schema = resource.parsedSettings.schema as JSONSchemaType<any> | undefined;
93
93
  const requiredPropertyNames = (
94
94
  resource.settings.importAndDestroy?.requiredParameters
95
95
  ?? (typeof resource.settings.allowMultiple === 'object' ? resource.settings.allowMultiple.identifyingParameters : null)
@@ -257,6 +257,10 @@ export class Plugin {
257
257
  }
258
258
  }
259
259
 
260
+ async setVerbosityLevel(data: SetVerbosityRequestData): Promise<void> {
261
+ VerbosityLevel.set(data.verbosityLevel);
262
+ }
263
+
260
264
  async kill() {
261
265
  await this.planPty.kill();
262
266
  }
@@ -20,8 +20,11 @@ EventEmitter.defaultMaxListeners = 1000;
20
20
  * without a tty (or even a stdin) attached so interactive commands will not work.
21
21
  */
22
22
  export class BackgroundPty implements IPty {
23
+ private historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
23
24
  private basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
24
- env: process.env, name: nanoid(6),
25
+ env: { ...process.env, ...this.historyIgnore },
26
+ cols: 10_000, // Set to a really large value to prevent wrapping
27
+ name: nanoid(6),
25
28
  handleFlowControl: true
26
29
  });
27
30
 
@@ -31,17 +34,19 @@ export class BackgroundPty implements IPty {
31
34
  this.initialize();
32
35
  }
33
36
 
34
- async spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
37
+ async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
35
38
  const spawnResult = await this.spawnSafe(cmd, options);
36
39
 
37
40
  if (spawnResult.status !== 'success') {
38
- throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
41
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join(' ') : cmd, spawnResult.exitCode, spawnResult.data);
39
42
  }
40
43
 
41
44
  return spawnResult;
42
45
  }
43
46
 
44
- async spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
47
+ async spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
48
+ cmd = Array.isArray(cmd) ? cmd.join('\\\n') : cmd;
49
+
45
50
  // cid is command id
46
51
  const cid = nanoid(10);
47
52
  debugLog(cid);
@@ -102,7 +107,7 @@ export class BackgroundPty implements IPty {
102
107
  }
103
108
  });
104
109
 
105
- console.log(`Running command ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
110
+ console.log(`Running command: ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
106
111
  this.basePty.write(`${command}\r`);
107
112
 
108
113
  }));
@@ -128,19 +133,6 @@ export class BackgroundPty implements IPty {
128
133
  let outputBuffer = '';
129
134
 
130
135
  return new Promise(resolve => {
131
- // zsh-specific commands
132
- switch (Utils.getShell()) {
133
- case Shell.ZSH: {
134
- this.basePty.write('setopt HIST_NO_STORE;\n');
135
- break;
136
- }
137
-
138
- default: {
139
- this.basePty.write('export HISTIGNORE=\'history*\';\n');
140
- break;
141
- }
142
- }
143
-
144
136
  this.basePty.write(' unset PS1;\n');
145
137
  this.basePty.write(' unset PS0;\n')
146
138
  this.basePty.write(' echo setup complete\\"\n')
package/src/pty/index.ts CHANGED
@@ -25,11 +25,17 @@ export enum SpawnStatus {
25
25
  *
26
26
  * @property {boolean} [interactive] - Indicates whether the spawned process needs
27
27
  * to be interactive. Only works within apply (not plan). Defaults to true.
28
+ *
29
+ * @property {boolean} [disableWrapping] - Forces the terminal width to 10_000 to disable wrapping.
30
+ * In applys, this is off by default while it is on during plans.
28
31
  */
29
32
  export interface SpawnOptions {
30
33
  cwd?: string;
31
- env?: Record<string, unknown>,
32
- interactive?: boolean,
34
+ env?: Record<string, unknown>;
35
+ interactive?: boolean;
36
+ requiresRoot?: boolean;
37
+ stdin?: boolean;
38
+ disableWrapping?: boolean;
33
39
  }
34
40
 
35
41
  export class SpawnError extends Error {
@@ -48,9 +54,9 @@ export class SpawnError extends Error {
48
54
  }
49
55
 
50
56
  export interface IPty {
51
- spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult>
57
+ spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
52
58
 
53
- spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult>
59
+ spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
54
60
 
55
61
  kill(): Promise<{ exitCode: number, signal?: number | undefined }>
56
62
  }
@@ -1,4 +1,7 @@
1
1
  import pty from '@homebridge/node-pty-prebuilt-multiarch';
2
+ import { Ajv } from 'ajv';
3
+ import { CommandRequestResponseData, CommandRequestResponseDataSchema, IpcMessageV2, MessageCmd } from 'codify-schemas';
4
+ import { nanoid } from 'nanoid';
2
5
  import { EventEmitter } from 'node:events';
3
6
  import stripAnsi from 'strip-ansi';
4
7
 
@@ -8,6 +11,11 @@ import { IPty, SpawnError, SpawnOptions, SpawnResult, SpawnStatus } from './inde
8
11
 
9
12
  EventEmitter.defaultMaxListeners = 1000;
10
13
 
14
+ const ajv = new Ajv({
15
+ strict: true,
16
+ });
17
+ const validateSudoRequestResponse = ajv.compile(CommandRequestResponseDataSchema);
18
+
11
19
  /**
12
20
  * The background pty is a specialized pty designed for speed. It can launch multiple tasks
13
21
  * in parallel by moving them to the background. It attaches unix FIFO pipes to each process
@@ -15,22 +23,32 @@ EventEmitter.defaultMaxListeners = 1000;
15
23
  * without a tty (or even a stdin) attached so interactive commands will not work.
16
24
  */
17
25
  export class SequentialPty implements IPty {
18
- async spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
26
+ async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
19
27
  const spawnResult = await this.spawnSafe(cmd, options);
20
28
 
21
29
  if (spawnResult.status !== 'success') {
22
- throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
30
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data);
23
31
  }
24
32
 
25
33
  return spawnResult;
26
34
  }
27
35
 
28
- async spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
29
- console.log(`Running command: ${cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
36
+ async spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
37
+ cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd;
38
+
39
+ if (cmd.includes('sudo')) {
40
+ throw new Error('Do not directly use sudo. Use the option { requiresRoot: true } instead')
41
+ }
42
+
43
+ // If sudo is required, we must delegate to the main codify process.
44
+ if (options?.stdin || options?.requiresRoot) {
45
+ return this.externalSpawn(cmd, options);
46
+ }
47
+
48
+ console.log(`Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
30
49
 
31
50
  return new Promise((resolve) => {
32
51
  const output: string[] = [];
33
-
34
52
  const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
35
53
 
36
54
  // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
@@ -39,14 +57,16 @@ export class SequentialPty implements IPty {
39
57
  ...process.env, ...options?.env,
40
58
  TERM_PROGRAM: 'codify',
41
59
  COMMAND_MODE: 'unix2003',
42
- COLORTERM: 'truecolor', ...historyIgnore
60
+ COLORTERM: 'truecolor',
61
+ ...historyIgnore
43
62
  }
44
63
 
45
64
  // Initial terminal dimensions
46
- const initialCols = process.stdout.columns ?? 80;
65
+ // Set to a really large value to prevent wrapping
66
+ const initialCols = options?.disableWrapping ? 10_000 : process.stdout.columns ?? 80
47
67
  const initialRows = process.stdout.rows ?? 24;
48
68
 
49
- const args = (options?.interactive ?? false) ? ['-i', '-c', cmd] : ['-c', cmd]
69
+ const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd]
50
70
 
51
71
  // Run the command in a pty for interactivity
52
72
  const mPty = pty.spawn(this.getDefaultShell(), args, {
@@ -64,23 +84,16 @@ export class SequentialPty implements IPty {
64
84
  output.push(data.toString());
65
85
  })
66
86
 
67
- const stdinListener = (data: any) => {
68
- mPty.write(data.toString());
69
- };
70
-
71
87
  const resizeListener = () => {
72
88
  const { columns, rows } = process.stdout;
73
- mPty.resize(columns, rows);
89
+ mPty.resize(columns, options?.disableWrapping ? 10_000 : rows);
74
90
  }
75
91
 
76
92
  // Listen to resize events for the terminal window;
77
93
  process.stdout.on('resize', resizeListener);
78
- // Listen for user input
79
- process.stdin.on('data', stdinListener);
80
94
 
81
95
  mPty.onExit((result) => {
82
96
  process.stdout.off('resize', resizeListener);
83
- process.stdin.off('data', stdinListener);
84
97
 
85
98
  resolve({
86
99
  status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
@@ -99,6 +112,39 @@ export class SequentialPty implements IPty {
99
112
  }
100
113
  }
101
114
 
115
+ // For safety reasons, requests that require sudo or are interactive must be run via the main client
116
+ async externalSpawn(
117
+ cmd: string,
118
+ opts: SpawnOptions
119
+ ): Promise<SpawnResult> {
120
+ return new Promise((resolve) => {
121
+ const requestId = nanoid(8);
122
+
123
+ const listener = (data: IpcMessageV2) => {
124
+ if (data.requestId === requestId) {
125
+ process.removeListener('message', listener);
126
+
127
+ if (!validateSudoRequestResponse(data.data)) {
128
+ throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
129
+ }
130
+
131
+ resolve(data.data as unknown as CommandRequestResponseData);
132
+ }
133
+ }
134
+
135
+ process.on('message', listener);
136
+
137
+ process.send!(<IpcMessageV2>{
138
+ cmd: MessageCmd.COMMAND_REQUEST,
139
+ data: {
140
+ command: cmd,
141
+ options: opts ?? {},
142
+ },
143
+ requestId
144
+ })
145
+ });
146
+ }
147
+
102
148
  private getDefaultShell(): string {
103
149
  return process.env.SHELL!;
104
150
  }
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { SequentialPty } from './seqeuntial-pty.js';
3
3
  import { VerbosityLevel } from '../utils/verbosity-level.js';
4
+ import { MessageStatus, SpawnStatus } from 'codify-schemas/src/types/index.js';
5
+ import { IpcMessageV2, MessageCmd } from 'codify-schemas';
4
6
 
5
7
  describe('SequentialPty tests', () => {
6
8
  it('Can launch a simple command', async () => {
@@ -33,8 +35,8 @@ describe('SequentialPty tests', () => {
33
35
  const resultFailed = await pty.spawnSafe('which sjkdhsakjdhjkash');
34
36
  expect(resultFailed).toMatchObject({
35
37
  status: 'error',
36
- exitCode: 127,
37
- data: 'zsh:1: command not found: which sjkdhsakjdhjkash' // This might change on different os or shells. Keep for now.
38
+ exitCode: 1,
39
+ data: 'sjkdhsakjdhjkash not found' // This might change on different os or shells. Keep for now.
38
40
  })
39
41
  });
40
42
 
@@ -49,13 +51,144 @@ describe('SequentialPty tests', () => {
49
51
  })
50
52
  });
51
53
 
52
- it('It can launch a command in interactive mode', async () => {
54
+
55
+ it('Can use multi-line commands', async () => {
53
56
  const pty = new SequentialPty();
54
57
 
55
- const resultSuccess = await pty.spawnSafe('ls', { interactive: true });
58
+ const resultSuccess = await pty.spawnSafe([
59
+ 'pwd',
60
+ '&& ls',
61
+ ], { cwd: '/tmp' });
56
62
  expect(resultSuccess).toMatchObject({
57
63
  status: 'success',
58
64
  exitCode: 0,
59
65
  })
60
66
  });
67
+
68
+
69
+ it('It can launch a command in interactive mode', async () => {
70
+ const originalSend = process.send;
71
+ process.send = (req: IpcMessageV2) => {
72
+ expect(req).toMatchObject({
73
+ cmd: MessageCmd.COMMAND_REQUEST,
74
+ requestId: expect.any(String),
75
+ data: {
76
+ command: 'ls',
77
+ options: {
78
+ cwd: '/tmp',
79
+ interactive: true,
80
+ }
81
+ }
82
+ })
83
+
84
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
85
+ const listeners = process.listeners('message');
86
+ listeners[2](({
87
+ cmd: MessageCmd.COMMAND_REQUEST,
88
+ requestId: req.requestId,
89
+ status: MessageStatus.SUCCESS,
90
+ data: {
91
+ status: SpawnStatus.SUCCESS,
92
+ exitCode: 0,
93
+ data: 'My data',
94
+ }
95
+ }))
96
+
97
+ return true;
98
+ }
99
+
100
+ const $ = new SequentialPty();
101
+ const resultSuccess = await $.spawnSafe('ls', { interactive: true, cwd: '/tmp' });
102
+
103
+ expect(resultSuccess).toMatchObject({
104
+ status: 'success',
105
+ exitCode: 0,
106
+ });
107
+
108
+ process.send = originalSend;
109
+ });
110
+
111
+ it('It can work with root (sudo)', async () => {
112
+ const originalSend = process.send;
113
+ process.send = (req: IpcMessageV2) => {
114
+ expect(req).toMatchObject({
115
+ cmd: MessageCmd.COMMAND_REQUEST,
116
+ requestId: expect.any(String),
117
+ data: {
118
+ command: 'ls',
119
+ options: {
120
+ interactive: true,
121
+ requiresRoot: true,
122
+ }
123
+ }
124
+ })
125
+
126
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
127
+ const listeners = process.listeners('message');
128
+ listeners[2](({
129
+ cmd: MessageCmd.COMMAND_REQUEST,
130
+ requestId: req.requestId,
131
+ status: MessageStatus.SUCCESS,
132
+ data: {
133
+ status: SpawnStatus.SUCCESS,
134
+ exitCode: 0,
135
+ data: 'My data',
136
+ }
137
+ }))
138
+
139
+ return true;
140
+ }
141
+
142
+ const $ = new SequentialPty();
143
+ const resultSuccess = await $.spawn('ls', { interactive: true, requiresRoot: true });
144
+
145
+ expect(resultSuccess).toMatchObject({
146
+ status: 'success',
147
+ exitCode: 0,
148
+ });
149
+
150
+ process.send = originalSend;
151
+ })
152
+
153
+ it('It can handle errors when in sudo', async () => {
154
+ const originalSend = process.send;
155
+ process.send = (req: IpcMessageV2) => {
156
+ expect(req).toMatchObject({
157
+ cmd: MessageCmd.COMMAND_REQUEST,
158
+ requestId: expect.any(String),
159
+ data: {
160
+ command: 'ls',
161
+ options: {
162
+ requiresRoot: true,
163
+ interactive: true,
164
+ }
165
+ }
166
+ })
167
+
168
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
169
+ const listeners = process.listeners('message');
170
+ listeners[2](({
171
+ cmd: MessageCmd.COMMAND_REQUEST,
172
+ requestId: req.requestId,
173
+ status: MessageStatus.SUCCESS,
174
+ data: {
175
+ status: SpawnStatus.ERROR,
176
+ exitCode: 127,
177
+ data: 'My data',
178
+ }
179
+ }))
180
+
181
+ return true;
182
+ }
183
+
184
+ const $ = new SequentialPty();
185
+ const resultSuccess = await $.spawnSafe('ls', { interactive: true, requiresRoot: true });
186
+
187
+ expect(resultSuccess).toMatchObject({
188
+ status: SpawnStatus.ERROR,
189
+ exitCode: 127,
190
+ });
191
+
192
+ process.send = originalSend;
193
+ })
61
194
  })
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ResourceSettings } from './resource-settings.js';
3
3
  import { ParsedResourceSettings } from './parsed-resource-settings.js';
4
4
  import { TestConfig } from '../utils/test-utils.test.js';
5
+ import { z } from 'zod';
6
+ import { OS } from 'codify-schemas';
5
7
 
6
8
  describe('Resource options parser tests', () => {
7
9
  it('Parses default values from options', () => {
@@ -159,4 +161,26 @@ describe('Resource options parser tests', () => {
159
161
 
160
162
  expect(() => new ParsedResourceSettings(option)).toThrowError()
161
163
  })
164
+
165
+ it('Can handle a zod schema', () => {
166
+
167
+ const schema = z.object({
168
+ propA: z.string(),
169
+ repository: z.string(),
170
+ })
171
+
172
+ const option: ResourceSettings<z.infer<typeof schema>> = {
173
+ id: 'typeId',
174
+ operatingSystems: [OS.Darwin],
175
+ schema,
176
+ importAndDestroy: {
177
+ defaultRefreshValues: {
178
+ repository: 'abc'
179
+ }
180
+ }
181
+ }
182
+
183
+ console.log(new ParsedResourceSettings(option))
184
+
185
+ })
162
186
  })
@@ -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,12 +8,12 @@ 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';
17
18
 
18
19
  export interface ParsedStatefulParameterSetting extends DefaultParameterSetting {
@@ -29,7 +30,7 @@ export type ParsedArrayParameterSetting = {
29
30
 
30
31
  export type ParsedParameterSetting =
31
32
  {
32
- isEqual: (desired: unknown, current: unknown) => boolean;
33
+ isEqual: (desired: unknown, current: unknown) => boolean;
33
34
  } & (DefaultParameterSetting
34
35
  | ParsedArrayParameterSetting
35
36
  | ParsedStatefulParameterSetting)
@@ -37,10 +38,13 @@ export type ParsedParameterSetting =
37
38
  export class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
38
39
  private cache = new Map<string, unknown>();
39
40
  id!: string;
41
+ description?: string;
42
+
40
43
  schema?: Partial<JSONSchemaType<T | any>>;
41
44
  allowMultiple?: {
45
+ identifyingParameters?: string[];
42
46
  matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
43
- requiredParameters?: string[]
47
+ findAllParameters?: () => Promise<Array<Partial<T>>>
44
48
  } | boolean;
45
49
 
46
50
  removeStatefulParametersBeforeDestroy?: boolean | undefined;
@@ -54,10 +58,22 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
54
58
 
55
59
  constructor(settings: ResourceSettings<T>) {
56
60
  this.settings = settings;
61
+ const { parameterSettings, schema, ...rest } = settings;
57
62
 
58
- const { parameterSettings, ...rest } = settings;
59
63
  Object.assign(this, rest);
60
64
 
65
+ if (schema) {
66
+ this.schema = schema instanceof ZodObject
67
+ ? z.toJSONSchema(schema.strict(), {
68
+ target: 'draft-7',
69
+ override(ctx) {
70
+ ctx.jsonSchema.title = settings.id;
71
+ ctx.jsonSchema.description = settings.description ?? `${settings.id} resource. Can be used to manage ${settings.id}`;
72
+ }
73
+ }) as JSONSchemaType<T>
74
+ : schema;
75
+ }
76
+
61
77
  this.validateSettings();
62
78
  }
63
79
 
@@ -199,7 +215,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
199
215
  throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`)
200
216
  }
201
217
 
202
- const schema = this.settings.schema as JSONSchemaType<any>;
218
+ const schema = this.schema as JSONSchemaType<any>;
203
219
  if (!this.settings.importAndDestroy && (schema?.oneOf
204
220
  && Array.isArray(schema.oneOf)
205
221
  && schema.oneOf.some((s) => s.required)
@@ -11,6 +11,7 @@ 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
  });