codify-plugin-lib 1.0.182-beta4 → 1.0.182-beta40
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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/plugin.d.ts +3 -3
- package/dist/plugin/plugin.js +18 -4
- package/dist/pty/background-pty.d.ts +3 -2
- package/dist/pty/background-pty.js +6 -14
- package/dist/pty/index.d.ts +4 -2
- package/dist/pty/seqeuntial-pty.d.ts +3 -2
- package/dist/pty/seqeuntial-pty.js +44 -10
- package/dist/resource/parsed-resource-settings.d.ts +3 -1
- package/dist/resource/parsed-resource-settings.js +15 -2
- package/dist/resource/resource-controller.js +5 -5
- package/dist/resource/resource-settings.d.ts +8 -2
- package/dist/resource/resource-settings.js +2 -2
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/utils/file-utils.d.ts +23 -0
- package/dist/utils/file-utils.js +186 -0
- package/dist/utils/functions.js +2 -2
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +30 -0
- package/dist/utils/load-resources.d.ts +1 -0
- package/dist/utils/load-resources.js +46 -0
- package/dist/utils/package-json-utils.d.ts +12 -0
- package/dist/utils/package-json-utils.js +34 -0
- package/package.json +4 -3
- package/src/index.ts +2 -1
- package/src/plugin/plugin.test.ts +31 -0
- package/src/plugin/plugin.ts +21 -4
- package/src/pty/background-pty.ts +9 -18
- package/src/pty/index.ts +6 -4
- package/src/pty/seqeuntial-pty.ts +59 -14
- package/src/pty/sequential-pty.test.ts +138 -5
- package/src/resource/parsed-resource-settings.test.ts +24 -0
- package/src/resource/parsed-resource-settings.ts +23 -7
- package/src/resource/resource-controller.test.ts +126 -0
- package/src/resource/resource-controller.ts +5 -6
- package/src/resource/resource-settings.test.ts +36 -0
- package/src/resource/resource-settings.ts +11 -4
- package/src/utils/file-utils.test.ts +7 -0
- package/src/utils/file-utils.ts +231 -0
- package/src/utils/functions.ts +3 -3
- package/src/utils/index.ts +37 -0
- package/src/utils/internal-utils.test.ts +1 -0
- package/src/utils/load-resources.ts +53 -0
- package/src/utils/package-json-utils.ts +40 -0
|
@@ -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
|
-
|
|
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,15 @@ export class SequentialPty implements IPty {
|
|
|
39
57
|
...process.env, ...options?.env,
|
|
40
58
|
TERM_PROGRAM: 'codify',
|
|
41
59
|
COMMAND_MODE: 'unix2003',
|
|
42
|
-
COLORTERM: 'truecolor',
|
|
60
|
+
COLORTERM: 'truecolor',
|
|
61
|
+
...historyIgnore
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
// Initial terminal dimensions
|
|
46
65
|
const initialCols = process.stdout.columns ?? 80;
|
|
47
66
|
const initialRows = process.stdout.rows ?? 24;
|
|
48
67
|
|
|
49
|
-
const args =
|
|
68
|
+
const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd]
|
|
50
69
|
|
|
51
70
|
// Run the command in a pty for interactivity
|
|
52
71
|
const mPty = pty.spawn(this.getDefaultShell(), args, {
|
|
@@ -64,10 +83,6 @@ export class SequentialPty implements IPty {
|
|
|
64
83
|
output.push(data.toString());
|
|
65
84
|
})
|
|
66
85
|
|
|
67
|
-
const stdinListener = (data: any) => {
|
|
68
|
-
mPty.write(data.toString());
|
|
69
|
-
};
|
|
70
|
-
|
|
71
86
|
const resizeListener = () => {
|
|
72
87
|
const { columns, rows } = process.stdout;
|
|
73
88
|
mPty.resize(columns, rows);
|
|
@@ -75,12 +90,9 @@ export class SequentialPty implements IPty {
|
|
|
75
90
|
|
|
76
91
|
// Listen to resize events for the terminal window;
|
|
77
92
|
process.stdout.on('resize', resizeListener);
|
|
78
|
-
// Listen for user input
|
|
79
|
-
process.stdin.on('data', stdinListener);
|
|
80
93
|
|
|
81
94
|
mPty.onExit((result) => {
|
|
82
95
|
process.stdout.off('resize', resizeListener);
|
|
83
|
-
process.stdin.off('data', stdinListener);
|
|
84
96
|
|
|
85
97
|
resolve({
|
|
86
98
|
status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
@@ -99,6 +111,39 @@ export class SequentialPty implements IPty {
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
114
|
+
// For safety reasons, requests that require sudo or are interactive must be run via the main client
|
|
115
|
+
async externalSpawn(
|
|
116
|
+
cmd: string,
|
|
117
|
+
opts: SpawnOptions
|
|
118
|
+
): Promise<SpawnResult> {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const requestId = nanoid(8);
|
|
121
|
+
|
|
122
|
+
const listener = (data: IpcMessageV2) => {
|
|
123
|
+
if (data.requestId === requestId) {
|
|
124
|
+
process.removeListener('message', listener);
|
|
125
|
+
|
|
126
|
+
if (!validateSudoRequestResponse(data.data)) {
|
|
127
|
+
throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
resolve(data.data as unknown as CommandRequestResponseData);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.on('message', listener);
|
|
135
|
+
|
|
136
|
+
process.send!(<IpcMessageV2>{
|
|
137
|
+
cmd: MessageCmd.COMMAND_REQUEST,
|
|
138
|
+
data: {
|
|
139
|
+
command: cmd,
|
|
140
|
+
options: opts ?? {},
|
|
141
|
+
},
|
|
142
|
+
requestId
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
102
147
|
private getDefaultShell(): string {
|
|
103
148
|
return process.env.SHELL!;
|
|
104
149
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { SequentialPty } from './seqeuntial-pty.js';
|
|
3
|
-
import { VerbosityLevel } from '../utils/
|
|
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:
|
|
37
|
-
data: '
|
|
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
|
-
|
|
54
|
+
|
|
55
|
+
it('Can use multi-line commands', async () => {
|
|
53
56
|
const pty = new SequentialPty();
|
|
54
57
|
|
|
55
|
-
const resultSuccess = await pty.spawnSafe(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
});
|
|
@@ -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.
|
|
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.
|
|
48
|
+
this.schemaValidator = this.ajv.compile(this.parsedSettings.schema);
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
this.parsedSettings = new ParsedResourceSettings<T>(this.settings);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
async initialize(): Promise<void> {
|
|
@@ -526,8 +525,8 @@ ${JSON.stringify(refresh, null, 2)}
|
|
|
526
525
|
}
|
|
527
526
|
|
|
528
527
|
private getAllParameterKeys(): string[] {
|
|
529
|
-
return this.
|
|
530
|
-
? Object.keys((this.
|
|
528
|
+
return this.parsedSettings.schema
|
|
529
|
+
? Object.keys((this.parsedSettings.schema as any)?.properties)
|
|
531
530
|
: Object.keys(this.parsedSettings.parameterSettings);
|
|
532
531
|
}
|
|
533
532
|
|
|
@@ -13,6 +13,7 @@ import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './res
|
|
|
13
13
|
import { ResourceController } from './resource-controller.js';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { z } from 'zod';
|
|
16
17
|
|
|
17
18
|
describe('Resource parameter tests', () => {
|
|
18
19
|
it('Generates a resource plan that includes stateful parameters (create)', async () => {
|
|
@@ -1174,4 +1175,39 @@ describe('Resource parameter tests', () => {
|
|
|
1174
1175
|
expect(from2).to.eq('$HOME/abc/def')
|
|
1175
1176
|
|
|
1176
1177
|
})
|
|
1178
|
+
|
|
1179
|
+
it('Can match directories 2', async () => {
|
|
1180
|
+
|
|
1181
|
+
const schema = z.object({
|
|
1182
|
+
propA: z.string(),
|
|
1183
|
+
propB: z.number(),
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const resource = new class extends TestResource {
|
|
1187
|
+
getSettings(): ResourceSettings<z.infer<typeof schema>> {
|
|
1188
|
+
return {
|
|
1189
|
+
id: 'resourceType',
|
|
1190
|
+
schema,
|
|
1191
|
+
operatingSystems: [OS.Darwin],
|
|
1192
|
+
parameterSettings: {
|
|
1193
|
+
propA: { type: 'directory' }
|
|
1194
|
+
},
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
const controller = new ResourceController(resource);
|
|
1200
|
+
const transformations = controller.parsedSettings.inputTransformations.propA;
|
|
1201
|
+
|
|
1202
|
+
const to = transformations!.to('$HOME/abc/def')
|
|
1203
|
+
expect(to).to.eq(os.homedir() + '/abc/def')
|
|
1204
|
+
|
|
1205
|
+
const from = transformations!.from(os.homedir() + '/abc/def')
|
|
1206
|
+
expect(from).to.eq('~/abc/def')
|
|
1207
|
+
|
|
1208
|
+
const from2 = transformations!.from(os.homedir() + '/abc/def', '$HOME/abc/def')
|
|
1209
|
+
expect(from2).to.eq('$HOME/abc/def')
|
|
1210
|
+
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1177
1213
|
})
|