@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,66 @@
1
+ import { ptyLocalStorage } from '../utils/pty-local-storage.js';
2
+
3
+ export interface SpawnResult {
4
+ status: 'error' | 'success';
5
+ exitCode: number;
6
+ data: string;
7
+ }
8
+
9
+ export enum SpawnStatus {
10
+ SUCCESS = 'success',
11
+ ERROR = 'error',
12
+ }
13
+
14
+ /**
15
+ * Represents the configuration options for spawning a child process.
16
+ *
17
+ * @interface SpawnOptions
18
+ *
19
+ * @property {string} [cwd] - Specifies the working directory of the child process.
20
+ * If not provided, the current working directory of the parent process is used.
21
+ *
22
+ * @property {Record<string, unknown>} [env] - Defines environment key-value pairs
23
+ * that will be available to the child process. If not specified, the child process
24
+ * will inherit the environment variables of the parent process.
25
+ *
26
+ * @property {boolean} [interactive] - Indicates whether the spawned process needs
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.
31
+ */
32
+ export interface SpawnOptions {
33
+ cwd?: string;
34
+ env?: Record<string, unknown>;
35
+ interactive?: boolean;
36
+ requiresRoot?: boolean;
37
+ stdin?: boolean;
38
+ disableWrapping?: boolean;
39
+ }
40
+
41
+ export class SpawnError extends Error {
42
+ data: string;
43
+ cmd: string;
44
+ exitCode: number;
45
+
46
+ constructor(cmd: string, exitCode: number, data: string) {
47
+ super(`Spawn Error: on command "${cmd}" with exit code: ${exitCode}\nOutput:\n${data}`);
48
+
49
+ this.data = data;
50
+ this.cmd = cmd;
51
+ this.exitCode = exitCode;
52
+ }
53
+
54
+ }
55
+
56
+ export interface IPty {
57
+ spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
58
+
59
+ spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
60
+
61
+ kill(): Promise<{ exitCode: number, signal?: number | undefined }>
62
+ }
63
+
64
+ export function getPty(): IPty {
65
+ return ptyLocalStorage.getStore() as IPty;
66
+ }
@@ -0,0 +1,33 @@
1
+ import { nanoid } from 'nanoid';
2
+ import EventEmitter from 'node:events';
3
+
4
+ export class PromiseQueue {
5
+ // Cid stands for command id;
6
+ private queue: Array<{ cid: string, fn: () => Promise<any> | any }> = [];
7
+ private eventBus = new EventEmitter()
8
+
9
+ async run<T>(fn: () => Promise<T> | T): Promise<T> {
10
+ const cid = nanoid();
11
+ this.queue.push({ cid, fn })
12
+
13
+ if (this.queue.length !== 1) {
14
+ await new Promise((resolve) => {
15
+ const listener = () => {
16
+ if (this.queue[0].cid === cid) {
17
+ this.eventBus.removeListener('dequeue', listener);
18
+ resolve(null);
19
+ }
20
+ }
21
+
22
+ this.eventBus.on('dequeue', listener);
23
+ });
24
+ }
25
+
26
+ const result = await fn();
27
+
28
+ this.queue.shift();
29
+ this.eventBus.emit('dequeue');
30
+
31
+ return result;
32
+ }
33
+ }
@@ -0,0 +1,151 @@
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';
5
+ import { EventEmitter } from 'node:events';
6
+ import stripAnsi from 'strip-ansi';
7
+
8
+ import { Shell, Utils } from '../utils/index.js';
9
+ import { VerbosityLevel } from '../utils/verbosity-level.js';
10
+ import { IPty, SpawnError, SpawnOptions, SpawnResult, SpawnStatus } from './index.js';
11
+
12
+ EventEmitter.defaultMaxListeners = 1000;
13
+
14
+ const ajv = new Ajv({
15
+ strict: true,
16
+ });
17
+ const validateSudoRequestResponse = ajv.compile(CommandRequestResponseDataSchema);
18
+
19
+ /**
20
+ * The background pty is a specialized pty designed for speed. It can launch multiple tasks
21
+ * in parallel by moving them to the background. It attaches unix FIFO pipes to each process
22
+ * to listen to stdout and stderr. One limitation of the BackgroundPty is that the tasks run
23
+ * without a tty (or even a stdin) attached so interactive commands will not work.
24
+ */
25
+ export class SequentialPty implements IPty {
26
+ async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
27
+ const spawnResult = await this.spawnSafe(cmd, options);
28
+
29
+ if (spawnResult.status !== 'success') {
30
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data);
31
+ }
32
+
33
+ return spawnResult;
34
+ }
35
+
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})` : ''))
49
+
50
+ return new Promise((resolve) => {
51
+ const output: string[] = [];
52
+ const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
53
+
54
+ // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
55
+ // in the response.
56
+ const env = {
57
+ ...process.env, ...options?.env,
58
+ TERM_PROGRAM: 'codify',
59
+ COMMAND_MODE: 'unix2003',
60
+ COLORTERM: 'truecolor',
61
+ ...historyIgnore
62
+ }
63
+
64
+ // Initial terminal dimensions
65
+ // Set to a really large value to prevent wrapping
66
+ const initialCols = options?.disableWrapping ? 10_000 : process.stdout.columns ?? 80
67
+ const initialRows = process.stdout.rows ?? 24;
68
+
69
+ const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd]
70
+
71
+ // Run the command in a pty for interactivity
72
+ const mPty = pty.spawn(this.getDefaultShell(), args, {
73
+ ...options,
74
+ cols: initialCols,
75
+ rows: initialRows,
76
+ env
77
+ });
78
+
79
+ mPty.onData((data) => {
80
+ if (VerbosityLevel.get() > 0) {
81
+ process.stdout.write(data);
82
+ }
83
+
84
+ output.push(data.toString());
85
+ })
86
+
87
+ const resizeListener = () => {
88
+ const { columns, rows } = process.stdout;
89
+ mPty.resize(columns, options?.disableWrapping ? 10_000 : rows);
90
+ }
91
+
92
+ // Listen to resize events for the terminal window;
93
+ process.stdout.on('resize', resizeListener);
94
+
95
+ mPty.onExit((result) => {
96
+ process.stdout.off('resize', resizeListener);
97
+
98
+ resolve({
99
+ status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
100
+ exitCode: result.exitCode,
101
+ data: stripAnsi(output.join('\n').trim()),
102
+ })
103
+ })
104
+ })
105
+ }
106
+
107
+ async kill(): Promise<{ exitCode: number, signal?: number | undefined }> {
108
+ // No-op here. Each pty instance is stand alone and tied to the parent process. Everything should be killed as expected.
109
+ return {
110
+ exitCode: 0,
111
+ signal: 0,
112
+ }
113
+ }
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
+
148
+ private getDefaultShell(): string {
149
+ return process.env.SHELL!;
150
+ }
151
+ }
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SequentialPty } from './seqeuntial-pty.js';
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';
6
+
7
+ describe('SequentialPty tests', () => {
8
+ it('Can launch a simple command', async () => {
9
+ const pty = new SequentialPty();
10
+
11
+ VerbosityLevel.set(1);
12
+
13
+ const result = await pty.spawnSafe('ls');
14
+ expect(result).toMatchObject({
15
+ status: 'success',
16
+ exitCode: 0,
17
+ data: expect.any(String),
18
+ })
19
+
20
+ const exitCode = await pty.kill();
21
+ expect(exitCode).toMatchObject({
22
+ exitCode: 0,
23
+ });
24
+ })
25
+
26
+ it('Reports back the correct exit code and status', async () => {
27
+ const pty = new SequentialPty();
28
+
29
+ const resultSuccess = await pty.spawnSafe('ls');
30
+ expect(resultSuccess).toMatchObject({
31
+ status: 'success',
32
+ exitCode: 0,
33
+ })
34
+
35
+ const resultFailed = await pty.spawnSafe('which sjkdhsakjdhjkash');
36
+ expect(resultFailed).toMatchObject({
37
+ status: 'error',
38
+ exitCode: 1,
39
+ data: 'sjkdhsakjdhjkash not found' // This might change on different os or shells. Keep for now.
40
+ })
41
+ });
42
+
43
+ it('Can use a different cwd', async () => {
44
+ const pty = new SequentialPty();
45
+
46
+ const resultSuccess = await pty.spawnSafe('pwd', { cwd: '/tmp' });
47
+ expect(resultSuccess).toMatchObject({
48
+ status: 'success',
49
+ exitCode: 0,
50
+ data: '/tmp'
51
+ })
52
+ });
53
+
54
+
55
+ it('Can use multi-line commands', async () => {
56
+ const pty = new SequentialPty();
57
+
58
+ const resultSuccess = await pty.spawnSafe([
59
+ 'pwd',
60
+ '&& ls',
61
+ ], { cwd: '/tmp' });
62
+ expect(resultSuccess).toMatchObject({
63
+ status: 'success',
64
+ exitCode: 0,
65
+ })
66
+ });
67
+
68
+
69
+ it('It can launch a command in interactive mode', { timeout: 30_000 }, 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
+ })
194
+ })
@@ -0,0 +1,42 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+
3
+ import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
4
+
5
+ export class ConfigParser<T extends StringIndexedObject> {
6
+ private readonly desiredConfig: Partial<T> | null;
7
+ private readonly stateConfig: Partial<T> | null;
8
+ private statefulParametersMap: Map<keyof T, StatefulParameterController<T, T[keyof T]>>;
9
+
10
+ constructor(
11
+ desiredConfig: Partial<T> | null,
12
+ stateConfig: Partial<T> | null,
13
+ statefulParameters: Map<keyof T, StatefulParameterController<T, T[keyof T]>>,
14
+ ) {
15
+ this.desiredConfig = desiredConfig;
16
+ this.stateConfig = stateConfig
17
+ this.statefulParametersMap = statefulParameters;
18
+ }
19
+
20
+ get allParameters(): Partial<T> {
21
+ return { ...this.desiredConfig, ...this.stateConfig } as Partial<T>;
22
+ }
23
+
24
+ get allNonStatefulParameters(): Partial<T> {
25
+ const {
26
+ allParameters,
27
+ statefulParametersMap,
28
+ } = this;
29
+
30
+ return Object.fromEntries(
31
+ Object.entries(allParameters).filter(([key]) => !statefulParametersMap.has(key))
32
+ ) as Partial<T>;
33
+ }
34
+
35
+ get allStatefulParameters(): Partial<T> {
36
+ const { allParameters, statefulParametersMap } = this;
37
+
38
+ return Object.fromEntries(
39
+ Object.entries(allParameters).filter(([key]) => statefulParametersMap.has(key))
40
+ ) as Partial<T>;
41
+ }
42
+ }
@@ -0,0 +1,186 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ResourceSettings } from './resource-settings.js';
3
+ import { ParsedResourceSettings } from './parsed-resource-settings.js';
4
+ import { TestConfig } from '../utils/test-utils.test.js';
5
+ import { z } from 'zod';
6
+ import { OS } from 'codify-schemas';
7
+
8
+ describe('Resource options parser tests', () => {
9
+ it('Parses default values from options', () => {
10
+ const option: ResourceSettings<TestConfig> = {
11
+ id: 'typeId',
12
+ parameterSettings: {
13
+ propA: { default: 'propA' },
14
+ propB: { default: 'propB' },
15
+ propC: { isEqual: () => true },
16
+ propD: {},
17
+ }
18
+ }
19
+
20
+ const result = new ParsedResourceSettings(option);
21
+ expect(result.defaultValues).to.deep.eq({
22
+ propA: 'propA',
23
+ propB: 'propB'
24
+ })
25
+ })
26
+
27
+ it('Throws an error when an import.requiredParameters is not declared', () => {
28
+ const schema = {
29
+ '$schema': 'http://json-schema.org/draft-07/schema',
30
+ '$id': 'https://www.codifycli.com/git-clone.json',
31
+ 'title': 'Git-clone resource',
32
+ 'type': 'object',
33
+ 'properties': {
34
+ 'remote': {
35
+ 'type': 'string',
36
+ 'description': 'Remote tracking url to clone repo from. Equivalent to repository and only one should be specified'
37
+ },
38
+ 'repository': {
39
+ 'type': 'string',
40
+ 'description': 'Remote repository to clone repo from. Equivalent to remote and only one should be specified'
41
+ },
42
+ 'parentDirectory': {
43
+ 'type': 'string',
44
+ 'description': 'Parent directory to clone into. The folder name will use default git semantics which extracts the last part of the clone url. Only one of parentDirectory or directory can be specified'
45
+ },
46
+ 'directory': {
47
+ 'type': 'string',
48
+ 'description': 'Directory to clone contents into. This value is directly passed into git clone. This differs from parent directory in that the last part of the path will be the folder name of the repo'
49
+ },
50
+ 'autoVerifySSH': {
51
+ 'type': 'boolean',
52
+ 'description': 'Automatically verifies the ssh connection for ssh git clones. Defaults to true.'
53
+ }
54
+ },
55
+ 'additionalProperties': false,
56
+ 'oneOf': [
57
+ { 'required': ['repository', 'directory'] },
58
+ { 'required': ['repository', 'parentDirectory'] },
59
+ { 'required': ['remote', 'directory'] }
60
+ ]
61
+ }
62
+
63
+ const option: ResourceSettings<TestConfig> = {
64
+ id: 'typeId',
65
+ schema,
66
+ }
67
+
68
+ expect(() => new ParsedResourceSettings(option)).toThrowError();
69
+ })
70
+
71
+ it('Throws an error when an import.requiredParameters is declared improperly', () => {
72
+ const schema = {
73
+ '$schema': 'http://json-schema.org/draft-07/schema',
74
+ 'type': 'object',
75
+ 'properties': {
76
+ 'remote': {
77
+ 'type': 'string',
78
+ },
79
+ },
80
+ 'additionalProperties': false,
81
+ }
82
+
83
+ const option: ResourceSettings<TestConfig> = {
84
+ id: 'typeId',
85
+ schema,
86
+ importAndDestroy: {
87
+ requiredParameters: ['import-error']
88
+ }
89
+ }
90
+
91
+ expect(() => new ParsedResourceSettings(option)).toThrowError();
92
+ })
93
+
94
+ it('Throws an error when an import.refreshKeys is declared improperly', () => {
95
+ const schema = {
96
+ '$schema': 'http://json-schema.org/draft-07/schema',
97
+ 'type': 'object',
98
+ 'properties': {
99
+ 'remote': {
100
+ 'type': 'string',
101
+ },
102
+ },
103
+ 'additionalProperties': false,
104
+ }
105
+
106
+ const option: ResourceSettings<TestConfig> = {
107
+ id: 'typeId',
108
+ schema,
109
+ importAndDestroy: {
110
+ refreshKeys: ['import-error']
111
+ }
112
+ }
113
+
114
+ expect(() => new ParsedResourceSettings(option)).toThrowError();
115
+ })
116
+
117
+ it('Doesn\'t throw an error when an import.refreshValues is declared properly', () => {
118
+ const schema = {
119
+ '$schema': 'http://json-schema.org/draft-07/schema',
120
+ 'type': 'object',
121
+ 'properties': {
122
+ 'remote': {
123
+ 'type': 'string',
124
+ },
125
+ },
126
+ 'additionalProperties': false,
127
+ }
128
+
129
+ const option: ResourceSettings<TestConfig> = {
130
+ id: 'typeId',
131
+ schema,
132
+ importAndDestroy: {
133
+ refreshKeys: ['remote'],
134
+ }
135
+ }
136
+
137
+ expect(() => new ParsedResourceSettings(option)).not.toThrowError()
138
+ })
139
+
140
+ it('Throws an error if defaultRefreshValue is not found in refreshKeys', () => {
141
+ const schema = {
142
+ '$schema': 'http://json-schema.org/draft-07/schema',
143
+ 'type': 'object',
144
+ 'properties': {
145
+ 'remote': {
146
+ 'type': 'string',
147
+ },
148
+ },
149
+ 'additionalProperties': false,
150
+ }
151
+
152
+ const option: ResourceSettings<TestConfig> = {
153
+ id: 'typeId',
154
+ schema,
155
+ importAndDestroy: {
156
+ defaultRefreshValues: {
157
+ repository: 'abc'
158
+ }
159
+ }
160
+ }
161
+
162
+ expect(() => new ParsedResourceSettings(option)).toThrowError()
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
+ })
186
+ })