From e079ae56477a3a4f96548e21354d043675b8028a Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 12 Dec 2024 21:31:02 -0500 Subject: [PATCH 01/15] Added support for IpcMessageV2 support --- package.json | 2 +- src/messages/handlers.test.ts | 66 ++++++++++++++++++++++++++++++++++- src/messages/handlers.ts | 27 ++++++++++---- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 31a0d0b..2e3b46f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.52", + "codify-schemas": "1.0.53", "@npmcli/promise-spawn": "^7.0.1", "uuid": "^10.0.0", "lodash.isequal": "^4.5.0" diff --git a/src/messages/handlers.test.ts b/src/messages/handlers.test.ts index be6a5c2..57466ef 100644 --- a/src/messages/handlers.test.ts +++ b/src/messages/handlers.test.ts @@ -207,7 +207,6 @@ describe('Message handler tests', () => { it('handles errors for apply (destroy)', async () => { const resource = new TestResource() const plugin = testPlugin(resource); - const handler = new MessageHandler(plugin); process.send = (message) => { @@ -228,6 +227,71 @@ describe('Message handler tests', () => { } } })).rejects.to.not.throw; + + process.send = undefined; + }) + + it('Supports ipc message v2 (success)', async () => { + const resource = new TestResource() + const plugin = testPlugin(resource); + const handler = new MessageHandler(plugin); + + process.send = (message) => { + console.log(message) + expect(message).toMatchObject({ + cmd: 'plan_Response', + requestId: 'abcdef', + status: MessageStatus.SUCCESS, + }) + return true; + } + + await expect(handler.onMessage({ + cmd: 'plan', + requestId: 'abcdef', + data: { + desired: { + type: 'type', + name: 'name', + prop1: 'A', + prop2: 'B', + }, + isStateful: false, + } + })).resolves.to.eq(undefined); + + process.send = undefined; + }) + + it('Supports ipc message v2 (error)', async () => { + const resource = new TestResource() + const plugin = testPlugin(resource); + const handler = new MessageHandler(plugin); + + process.send = (message) => { + expect(message).toMatchObject({ + cmd: 'apply_Response', + requestId: 'abcdef', + status: MessageStatus.ERROR, + }) + return true; + } + + await expect(handler.onMessage({ + cmd: 'apply', // Supposed to be a plan so that's why it throws + requestId: 'abcdef', + data: { + desired: { + type: 'type', + name: 'name', + prop1: 'A', + prop2: 'B', + }, + isStateful: false, + } + })).resolves.to.eq(undefined); + + process.send = undefined; }) }); diff --git a/src/messages/handlers.ts b/src/messages/handlers.ts index 84689a0..1fa8298 100644 --- a/src/messages/handlers.ts +++ b/src/messages/handlers.ts @@ -11,6 +11,8 @@ import { InitializeResponseDataSchema, IpcMessage, IpcMessageSchema, + IpcMessageV2, + IpcMessageV2Schema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, @@ -61,7 +63,8 @@ const SupportedRequests: Record; private responseValidators: Map; @@ -71,7 +74,9 @@ export class MessageHandler { this.ajv.addSchema(ResourceSchema); this.plugin = plugin; - this.messageSchemaValidator = this.ajv.compile(IpcMessageSchema); + this.messageSchemaValidatorV1 = this.ajv.compile(IpcMessageSchema); + this.messageSchemaValidatorV2 = this.ajv.compile(IpcMessageV2Schema); + this.requestValidators = new Map( Object.entries(SupportedRequests) .map(([k, v]) => [k, this.ajv.compile(v.requestValidator)]) @@ -84,8 +89,8 @@ export class MessageHandler { async onMessage(message: unknown): Promise { try { - if (!this.validateMessage(message)) { - throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidator.errors, null, 2)}`); + if (!this.validateMessageV2(message) && !this.validateMessage(message)) { + throw new Error(`Plugin: ${this.plugin}. Message is malformed: ${JSON.stringify(this.messageSchemaValidatorV1.errors, null, 2)}`); } if (!this.requestValidators.has(message.cmd)) { @@ -107,6 +112,8 @@ export class MessageHandler { process.send!({ cmd: message.cmd + '_Response', data: result, + // @ts-expect-error TS2239 + requestId: message.requestId || undefined, status: MessageStatus.SUCCESS, }) @@ -116,7 +123,11 @@ export class MessageHandler { } private validateMessage(message: unknown): message is IpcMessage { - return this.messageSchemaValidator(message); + return this.messageSchemaValidatorV1(message); + } + + private validateMessageV2(message: unknown): message is IpcMessageV2 { + return this.messageSchemaValidatorV2(message); } private handleErrors(message: unknown, e: Error) { @@ -128,12 +139,14 @@ export class MessageHandler { return; } - // @ts-ignore + // @ts-expect-error TS2239 const cmd = message.cmd + '_Response'; if (e instanceof SudoError) { return process.send?.({ cmd, + // @ts-expect-error TS2239 + requestId: message.requestId || undefined, data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`, status: MessageStatus.ERROR, }) @@ -143,6 +156,8 @@ export class MessageHandler { process.send?.({ cmd, + // @ts-expect-error TS2239 + requestId: message.requestId || undefined, data: isDebug ? e.stack : e.message, status: MessageStatus.ERROR, }) From 0e637bdb0ab6357693bb7af01de02e6f343ed64e Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 12 Dec 2024 21:34:16 -0500 Subject: [PATCH 02/15] Fixed package-lock.json --- package-lock.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6e0acf..e72c917 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "codify-plugin-lib", - "version": "1.0.100", + "version": "1.0.107", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.100", + "version": "1.0.107", "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.52", + "codify-schemas": "1.0.53", "lodash.isequal": "^4.5.0", "uuid": "^10.0.0" }, @@ -2207,10 +2207,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.52", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.52.tgz", - "integrity": "sha512-sbVPxlNOJN+DZnVJqhUVVljcxDTSxt9iAmp3pGU+6YxwzaB31KnI99I0oWBMSe2XT5Lt3eD2hdVkIsI9xCQ25A==", - "license": "ISC", + "version": "1.0.53", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.53.tgz", + "integrity": "sha512-4FqoHlWyAIKdu4UR3H3UagSDzVgbEKrTJVGQTG7CbWgog2bihwA9Mr4PLlxF2M0ppHHKOfTE2vF8ikHEnBsWUQ==", "dependencies": { "ajv": "^8.12.0" } From d5e18704944b992ec840a87bfd2dbe83e617e77e Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 23 Dec 2024 18:54:31 -0500 Subject: [PATCH 03/15] Parallelized stateful parameter refreshes --- package-lock.json | 4 ++-- package.json | 2 +- src/resource/resource-controller.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e72c917..0379a00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codify-plugin-lib", - "version": "1.0.107", + "version": "1.0.109", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.107", + "version": "1.0.109", "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^7.0.1", diff --git a/package.json b/package.json index 2e3b46f..932b3f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.107", + "version": "1.0.109", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index c3a3566..8e1b329 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -362,14 +362,14 @@ ${JSON.stringify(refresh, null, 2)} ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)! ) - for (const [key, desiredValue] of sortedEntries) { + await Promise.all(sortedEntries.map(async ([key, desiredValue]) => { const statefulParameter = this.parsedSettings.statefulParameters.get(key); if (!statefulParameter) { throw new Error(`Stateful parameter ${key} was not found`); } (result as Record)[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters) - } + })) return result; } From 405f8a96aad55cc817c1afff91d454ba0e3d51f6 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 29 Dec 2024 20:03:14 -0500 Subject: [PATCH 04/15] Added new, faster, better background pty --- package-lock.json | 177 +++++++++++++++++++++++++++++---- package.json | 5 +- src/pty/background-pty.test.ts | 69 +++++++++++++ src/pty/background-pty.ts | 147 +++++++++++++++++++++++++++ src/pty/index.ts | 33 ++++++ src/pty/promise-queue.ts | 33 ++++++ src/pty/vitest.config.ts | 12 +++ src/utils/debug.ts | 11 ++ vitest.config.ts | 3 +- 9 files changed, 471 insertions(+), 19 deletions(-) create mode 100644 src/pty/background-pty.test.ts create mode 100644 src/pty/background-pty.ts create mode 100644 src/pty/index.ts create mode 100644 src/pty/promise-queue.ts create mode 100644 src/pty/vitest.config.ts create mode 100644 src/utils/debug.ts diff --git a/package-lock.json b/package-lock.json index 0379a00..f0f9ad8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,9 @@ "ajv-formats": "^2.1.1", "codify-schemas": "1.0.53", "lodash.isequal": "^4.5.0", + "nanoid": "^5.0.9", + "node-pty": "^1.0.0", + "strip-ansi": "^7.1.0", "uuid": "^10.0.0" }, "devDependencies": { @@ -827,6 +830,27 @@ "node": ">=18.0.0" } }, + "node_modules/@oclif/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@oclif/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@oclif/prettier-config": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@oclif/prettier-config/-/prettier-config-0.2.1.tgz", @@ -1759,12 +1783,14 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -3239,6 +3265,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3285,6 +3320,18 @@ "node": "*" } }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4741,11 +4788,15 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" + }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", + "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==", "funding": [ { "type": "github", @@ -4753,10 +4804,10 @@ } ], "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/natural-compare": { @@ -4813,6 +4864,15 @@ "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", "dev": true }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "dependencies": { + "nan": "^2.17.0" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5223,6 +5283,24 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5956,6 +6034,27 @@ "node": ">=8.0.0" } }, + "node_modules/stdout-stderr/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stdout-stderr/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stream-combiner": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", @@ -5988,6 +6087,27 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -6038,15 +6158,17 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-bom": { @@ -6847,6 +6969,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 932b3f6..bc2b915 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "codify-schemas": "1.0.53", "@npmcli/promise-spawn": "^7.0.1", "uuid": "^10.0.0", - "lodash.isequal": "^4.5.0" + "lodash.isequal": "^4.5.0", + "nanoid": "^5.0.9", + "node-pty": "^1.0.0", + "strip-ansi": "^7.1.0" }, "devDependencies": { "@oclif/prettier-config": "^0.2.1", diff --git a/src/pty/background-pty.test.ts b/src/pty/background-pty.test.ts new file mode 100644 index 0000000..cc744d0 --- /dev/null +++ b/src/pty/background-pty.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; +import { BackgroundPty } from './background-pty.js'; + +describe.sequential('BackgroundPty tests', () => { + it('Can launch a simple command', async () => { + const pty = new BackgroundPty(); + + const result = await pty.spawnSafe('ls'); + expect(result).toMatchObject({ + status: 'success', + exitCode: 0, + }) + + + const exitCode = await pty.kill(); + expect(exitCode).toMatchObject({ + exitCode: 1, + signal: 0, + }); + }) + + it('Can launch 100 commands in parallel', { timeout: 15000 }, async () => { + const pty = new BackgroundPty(); + + const fn = async () => pty.spawnSafe('ls'); + + const results = await Promise.all( + Array.from({ length: 100 }, (_, i) => i + 1) + .map(() => fn()) + ) + + expect(results.length).to.eq(100); + expect(results.every((r) => r.exitCode === 0)) + + await pty.kill(); + }) + + it('Reports back the correct exit code and status', async () => { + const pty = new BackgroundPty(); + + const resultSuccess = await pty.spawnSafe('ls'); + expect(resultSuccess).toMatchObject({ + status: 'success', + exitCode: 0, + }) + + const resultFailed = await pty.spawnSafe('which sjkdhsakjdhjkash'); + expect(resultFailed).toMatchObject({ + status: 'error', + exitCode: 1, + data: 'sjkdhsakjdhjkash not found' // This might change on different os or shells. Keep for now. + }) + + await pty.kill(); + }); + + it('Can use a different cwd', async () => { + const pty = new BackgroundPty(); + + const resultSuccess = await pty.spawnSafe('pwd', { cwd: '/tmp' }); + expect(resultSuccess).toMatchObject({ + status: 'success', + exitCode: 0, + data: '/tmp' + }) + + await pty.kill(); + }); +}) diff --git a/src/pty/background-pty.ts b/src/pty/background-pty.ts new file mode 100644 index 0000000..ca70f81 --- /dev/null +++ b/src/pty/background-pty.ts @@ -0,0 +1,147 @@ +import { nanoid } from 'nanoid'; +import * as cp from 'node:child_process'; +import { EventEmitter } from 'node:events'; +import * as fs from 'node:fs/promises'; +import * as net from 'node:net'; +import pty from 'node-pty'; +import stripAnsi from 'strip-ansi'; + +import { IPty, SpawnError, SpawnOptions, SpawnResult } from './index.js'; +import { PromiseQueue } from './promise-queue.js'; +import { debugLog } from '../utils/debug.js'; + +EventEmitter.defaultMaxListeners = 1000; + +/** + * The background pty is a specialized pty designed for speed. It can launch multiple tasks + * in parallel by moving them to the background. It attaches unix FIFO pipes to each process + * to listen to stdout and stderr. One limitation of the BackgroundPty is that the tasks run + * without a tty (or even a stdin) attached so interactive commands will not work. + */ +export class BackgroundPty implements IPty { + private basePty = pty.spawn('zsh', ['-i'], { + env: process.env, name: nanoid(6), + handleFlowControl: true + }); + + private promiseQueue = new PromiseQueue(); + + constructor() { + this.initialize(); + } + + async spawn(cmd: string, options?: SpawnOptions): Promise { + const spawnResult = await this.spawnSafe(cmd, options); + + if (spawnResult.status !== 'success') { + throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data); + } + + return spawnResult; + } + + async spawnSafe(cmd: string, options?: SpawnOptions): Promise { + // cid is command id + const cid = nanoid(10); + debugLog(cid); + + await new Promise((resolve) => { + // 600 permissions means only the current user will be able to rw from the FIFO + // Create in /tmp so it could be automatically cleaned up if the clean-up was missed + const mkfifoSpawn = cp.spawn('mkfifo', ['-m', '600', `/tmp/${cid}`]); + mkfifoSpawn.on('close', () => { + resolve(null); + }) + }) + + // Use read and write so that the pipe doesn't close + const fileHandle = await fs.open(`/tmp/${cid}`, fs.constants.O_RDWR | fs.constants.O_NONBLOCK); + let pipe: net.Socket; + + return new Promise((resolve) => { + pipe = new net.Socket({ fd: fileHandle.fd }); + + // pipe.pipe(process.stdout); + + let output = ''; + pipe.on('data', (data) => { + output += data.toString(); + + if (output.includes('%%%done%%%"')) { + const truncOutput = output.replace('%%%done%%%"\n', ''); + const [data, exit] = truncOutput.split('%%%'); + + // Clean up trailing \n newline if it exists + let strippedData = stripAnsi(data); + if (strippedData.endsWith('\n')) { + strippedData = strippedData.slice(0, -1); + } + + resolve({ + status: Number.parseInt(exit ?? 1, 10) === 0 ? 'success' : 'error', + exitCode: Number.parseInt(exit ?? 1, 10), + data: strippedData, + }); + } + }) + + this.promiseQueue.run(async () => new Promise((resolve) => { + const cdCommand = options?.cwd ? `cd ${options.cwd}; ` : ''; + // Redirecting everything to the pipe and running in theb background avoids most if not all back-pressure problems + // Done is used to denote the end of the command + // Use the \\" at the end differentiate between command and response. \\" will evaluate to " in the terminal + const command = `((${cdCommand}${cmd}; echo %%%$?%%%done%%%\\") > "/tmp/${cid}" 2>&1 &); echo %%%done%%%${cid}\\";` + + let output = ''; + const listener = this.basePty.onData((data: any) => { + output += data; + + if (output.includes(`%%%done%%%${cid}"`)) { + listener.dispose(); + resolve(null); + } + }); + + // console.log(`Running command ${cmd}`) + this.basePty.write(`${command}\r`); + + })); + }).finally(async () => { + // console.log('finally'); + // await fileHandle?.close(); + await fs.rm(`/tmp/${cid}`); + }) + } + + async kill(): Promise<{ exitCode: number, signal?: number | undefined }> { + return new Promise((resolve) => { + this.basePty.onExit((status) => { + resolve(status); + }) + + this.basePty.kill() + }) + } + + private async initialize() { + // this.basePty.onData((data: string) => process.stdout.write(data)); + + await this.promiseQueue.run(async () => { + let outputBuffer = ''; + + return new Promise(resolve => { + this.basePty.write('unset PS1;\n'); + this.basePty.write('unset PS0;\n') + this.basePty.write('echo setup complete\\"\n') + + const listener = this.basePty.onData((data: string) => { + outputBuffer += data; + if (outputBuffer.includes('setup complete"')) { + listener.dispose(); + resolve(null); + } + }) + }) + }) + } +} diff --git a/src/pty/index.ts b/src/pty/index.ts new file mode 100644 index 0000000..b5930b6 --- /dev/null +++ b/src/pty/index.ts @@ -0,0 +1,33 @@ +export interface SpawnResult { + status: 'success' | 'error'; + exitCode: number; + data: string; +} + +export interface SpawnOptions { + cwd?: string; + env?: Record, +} + +export class SpawnError extends Error { + data: string; + cmd: string; + exitCode: number; + + constructor(cmd: string, exitCode: number, data: string) { + super(`Spawn Error: on command "${cmd}" with exit code: ${exitCode}\nOutput:\n${data}`); + + this.data = data; + this.cmd = cmd; + this.exitCode = exitCode; + } + +} + +export interface IPty { + spawn(cmd: string, options?: SpawnOptions): Promise + + spawnSafe(cmd: string, options?: SpawnOptions): Promise + + kill(): Promise<{ exitCode: number, signal?: number | undefined }> +} diff --git a/src/pty/promise-queue.ts b/src/pty/promise-queue.ts new file mode 100644 index 0000000..e005299 --- /dev/null +++ b/src/pty/promise-queue.ts @@ -0,0 +1,33 @@ +import { nanoid } from 'nanoid'; +import EventEmitter from 'node:events'; + +export class PromiseQueue { + // Cid stands for command id; + private queue: Array<{ cid: string, fn: () => Promise | any }> = []; + private eventBus = new EventEmitter() + + async run(fn: () => Promise | T): Promise { + const cid = nanoid(); + this.queue.push({ cid, fn }) + + if (this.queue.length !== 1) { + await new Promise((resolve) => { + const listener = () => { + if (this.queue[0].cid === cid) { + this.eventBus.removeListener('dequeue', listener); + resolve(null); + } + } + + this.eventBus.on('dequeue', listener); + }); + } + + const result = await fn(); + + this.queue.shift(); + this.eventBus.emit('dequeue'); + + return result; + } +} diff --git a/src/pty/vitest.config.ts b/src/pty/vitest.config.ts new file mode 100644 index 0000000..d7d5fb7 --- /dev/null +++ b/src/pty/vitest.config.ts @@ -0,0 +1,12 @@ +import { defaultExclude, defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + pool: 'forks', + fileParallelism: false, + exclude: [ + ...defaultExclude, + './src/utils/test-utils.test.ts', + ] + }, +}); diff --git a/src/utils/debug.ts b/src/utils/debug.ts new file mode 100644 index 0000000..7f4eaee --- /dev/null +++ b/src/utils/debug.ts @@ -0,0 +1,11 @@ +export function debugLog(message: any): void { + if (process.env.DEBUG) { + console.log(message); + } +} + +export function debugWrite(message: any): void { + if (process.env.DEBUG) { + process.stdout.write(message); + } +} diff --git a/vitest.config.ts b/vitest.config.ts index 21533de..0316135 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,8 @@ export default defineConfig({ test: { exclude: [ ...defaultExclude, - './src/utils/test-utils.test.ts' + './src/utils/test-utils.test.ts', + './src/pty/*' ] }, }); From b5c627f0cd823a7f1d8f5e83b9f81573a0a6c82b Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 30 Dec 2024 09:09:17 -0500 Subject: [PATCH 05/15] Added exports for pty and async storage --- package.json | 2 +- src/index.ts | 1 + src/plugin/plugin.ts | 16 ++-- src/pty/background-pty.test.ts | 2 +- src/pty/index.test.ts | 129 +++++++++++++++++++++++++++++++++ src/pty/index.ts | 11 +++ src/utils/pty-local-storage.ts | 3 + src/utils/utils.ts | 76 ------------------- tsconfig.json | 3 +- 9 files changed, 159 insertions(+), 84 deletions(-) create mode 100644 src/pty/index.test.ts create mode 100644 src/utils/pty-local-storage.ts diff --git a/package.json b/package.json index bc2b915..385e6ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.109", + "version": "1.0.112", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 7062e64..1942f33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from './plan/change-set.js' export * from './plan/plan.js' export * from './plan/plan-types.js' export * from './plugin/plugin.js' +export * from './pty/index.js' export * from './resource/parsed-resource-settings.js'; export * from './resource/resource.js' export * from './resource/resource-settings.js' diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index bbd1188..d6147b4 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -16,9 +16,12 @@ import { import { Plan } from '../plan/plan.js'; import { Resource } from '../resource/resource.js'; import { ResourceController } from '../resource/resource-controller.js'; +import { ptyLocalStorage } from '../utils/pty-local-storage.js'; +import { BackgroundPty } from '../pty/background-pty.js'; export class Plugin { planStorage: Map>; + planPty = new BackgroundPty(); constructor( public name: string, @@ -119,11 +122,14 @@ export class Plugin { throw new Error(`Resource type not found: ${type}`); } - const plan = await this.resourceControllers.get(type)!.plan( - data.desired ?? null, - data.state ?? null, - data.isStateful - ); + const plan = await ptyLocalStorage.run(this.planPty, async () => { + return this.resourceControllers.get(type)!.plan( + data.desired ?? null, + data.state ?? null, + data.isStateful + ); + }) + this.planStorage.set(plan.id, plan); return plan.toResponse(); diff --git a/src/pty/background-pty.test.ts b/src/pty/background-pty.test.ts index cc744d0..901695e 100644 --- a/src/pty/background-pty.test.ts +++ b/src/pty/background-pty.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { BackgroundPty } from './background-pty.js'; -describe.sequential('BackgroundPty tests', () => { +describe('BackgroundPty tests', () => { it('Can launch a simple command', async () => { const pty = new BackgroundPty(); diff --git a/src/pty/index.test.ts b/src/pty/index.test.ts new file mode 100644 index 0000000..ae1075f --- /dev/null +++ b/src/pty/index.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, vitest } from 'vitest'; +import { TestConfig, TestResource } from '../utils/test-utils.test.js'; +import { getPty, IPty } from './index.js'; +import { Plugin } from '../plugin/plugin.js' +import { CreatePlan } from '../plan/plan-types.js'; +import { ResourceOperation } from 'codify-schemas'; +import { ResourceSettings } from '../resource/resource-settings.js'; + +describe('General tests for PTYs', () => { + it('Can get pty within refresh', async () => { + const testResource = new class extends TestResource { + async refresh(): Promise | null> { + const $ = getPty(); + const lsResult = await $.spawnSafe('ls'); + + expect(lsResult.exitCode).to.eq(0); + expect(lsResult.data).to.be.not.null; + expect(lsResult.status).to.eq('success'); + + return {}; + } + } + + const spy = vitest.spyOn(testResource, 'refresh') + + const plugin = Plugin.create('test plugin', [testResource]) + const plan = await plugin.plan({ + desired: { + type: 'type' + }, + state: undefined, + isStateful: false, + }) + + expect(plan).toMatchObject({ + operation: 'noop', + resourceType: 'type', + }) + expect(spy).toHaveBeenCalledOnce() + }) + + it('The same pty instance is shared cross multiple resources', async () => { + let pty1: IPty; + let pty2: IPty; + + const testResource1 = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'type1' + } + } + + async refresh(): Promise | null> { + const $ = getPty(); + const lsResult = await $.spawnSafe('ls'); + + expect(lsResult.exitCode).to.eq(0); + pty1 = $; + + return {}; + } + } + + const testResource2 = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'type2', + } + } + + async refresh(): Promise | null> { + const $ = getPty(); + const pwdResult = await $.spawnSafe('pwd'); + + expect(pwdResult.exitCode).to.eq(0); + pty2 = $; + + return {}; + } + } + + const spy1 = vitest.spyOn(testResource1, 'refresh') + const spy2 = vitest.spyOn(testResource2, 'refresh') + + const plugin = Plugin.create('test plugin', [testResource1, testResource2]); + await plugin.plan({ + desired: { + type: 'type1' + }, + state: undefined, + isStateful: false, + }) + + await plugin.plan({ + desired: { + type: 'type2' + }, + state: undefined, + isStateful: false, + }) + + expect(spy1).toHaveBeenCalledOnce(); + expect(spy2).toHaveBeenCalledOnce(); + + // The main check here is that the refresh method for both are sharing the same pty instance. + expect(pty1).to.eq(pty2); + }) + + it('Currently pty not available for apply', async () => { + const testResource = new class extends TestResource { + create(plan: CreatePlan): Promise { + const $ = getPty(); + expect($).to.be.undefined; + } + } + + const spy = vitest.spyOn(testResource, 'create') + + const plugin = Plugin.create('test plugin', [testResource]) + await plugin.apply({ + plan: { + operation: ResourceOperation.CREATE, + resourceType: 'type', + parameters: [], + } + }) + expect(spy).toHaveBeenCalledOnce() + }) +}) diff --git a/src/pty/index.ts b/src/pty/index.ts index b5930b6..901911b 100644 --- a/src/pty/index.ts +++ b/src/pty/index.ts @@ -1,9 +1,16 @@ +import { ptyLocalStorage } from '../utils/pty-local-storage.js'; + export interface SpawnResult { status: 'success' | 'error'; exitCode: number; data: string; } +export enum SpawnStatus { + SUCCESS = 'success', + ERROR = 'error', +} + export interface SpawnOptions { cwd?: string; env?: Record, @@ -31,3 +38,7 @@ export interface IPty { kill(): Promise<{ exitCode: number, signal?: number | undefined }> } + +export function getPty(): IPty { + return ptyLocalStorage.getStore() as IPty; +} diff --git a/src/utils/pty-local-storage.ts b/src/utils/pty-local-storage.ts new file mode 100644 index 0000000..debdf73 --- /dev/null +++ b/src/utils/pty-local-storage.ts @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; + +export const ptyLocalStorage = new AsyncLocalStorage(); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 34ae190..2485dc8 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,82 +1,6 @@ -import promiseSpawn from '@npmcli/promise-spawn'; import { ResourceConfig, StringIndexedObject } from 'codify-schemas'; -import { SpawnOptions } from 'node:child_process'; import os from 'node:os'; -export enum SpawnStatus { - SUCCESS = 'success', - ERROR = 'error', -} - -export interface SpawnResult { - status: SpawnStatus, - data: string; -} - -type CodifySpawnOptions = { - cwd?: string; - stdioString?: boolean; -} & SpawnOptions - -/** - * - * @param cmd Command to run. Ex: `rm -rf` - * @param args Optional additional arguments to append - * @param opts Standard options for node spawn. Additional argument: - * throws determines if a shell will throw a JS error. Defaults to true - * @param extras From PromiseSpawn - * - * @see promiseSpawn - * @see spawn - * - * @returns SpawnResult { status: SUCCESS | ERROR; data: string } - */ -export async function codifySpawn( - cmd: string, - args?: string[], - opts?: Omit & { throws?: boolean }, - extras?: Record, -): Promise { - try { - // TODO: Need to benchmark the effects of using sh vs zsh for shell. - // Seems like zsh shells run slower - const result = await promiseSpawn( - cmd, - args ?? [], - { ...opts, stdio: 'pipe', stdioString: true, shell: opts?.shell ?? process.env.SHELL }, - extras, - ); - - if (isDebug()) { - console.log(`codifySpawn result for: ${cmd}`); - console.log(JSON.stringify(result, null, 2)) - } - - const status = result.code === 0 - ? SpawnStatus.SUCCESS - : SpawnStatus.ERROR; - - return { - status, - data: status === SpawnStatus.SUCCESS ? result.stdout : result.stderr - } - } catch (error) { - const shouldThrow = opts?.throws ?? true; - if (isDebug() || shouldThrow) { - console.error(`CodifySpawn Error for command ${cmd} ${args}`, error); - } - - if (shouldThrow) { - throw error; - } - - return { - status: SpawnStatus.ERROR, - data: error as string, - } - } -} - export function isDebug(): boolean { return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library } diff --git a/tsconfig.json b/tsconfig.json index f0f4dca..8fbf661 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ ], "exclude": [ "node_modules", - "src/**/*.test.ts" + "src/**/*.test.ts", + "**/vitest.config.ts" ] } From edf3cd9e3db3db74c83f49d268e69bc8d3cbdef1 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 30 Dec 2024 11:27:11 -0500 Subject: [PATCH 06/15] Fixed net socket buffer issue by switching to cat --- package.json | 2 +- src/pty/background-pty.ts | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 385e6ca..c0c5c49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.112", + "version": "1.0.114", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/pty/background-pty.ts b/src/pty/background-pty.ts index ca70f81..1e8beef 100644 --- a/src/pty/background-pty.ts +++ b/src/pty/background-pty.ts @@ -2,13 +2,12 @@ import { nanoid } from 'nanoid'; import * as cp from 'node:child_process'; import { EventEmitter } from 'node:events'; import * as fs from 'node:fs/promises'; -import * as net from 'node:net'; import pty from 'node-pty'; import stripAnsi from 'strip-ansi'; +import { debugLog } from '../utils/debug.js'; import { IPty, SpawnError, SpawnOptions, SpawnResult } from './index.js'; import { PromiseQueue } from './promise-queue.js'; -import { debugLog } from '../utils/debug.js'; EventEmitter.defaultMaxListeners = 1000; @@ -54,17 +53,12 @@ export class BackgroundPty implements IPty { }) }) - // Use read and write so that the pipe doesn't close - const fileHandle = await fs.open(`/tmp/${cid}`, fs.constants.O_RDWR | fs.constants.O_NONBLOCK); - let pipe: net.Socket; - return new Promise((resolve) => { - pipe = new net.Socket({ fd: fileHandle.fd }); - - // pipe.pipe(process.stdout); + const cat = cp.spawn('cat', [`/tmp/${cid}`]) + cat.stdout.pipe(process.stdout); let output = ''; - pipe.on('data', (data) => { + cat.stdout.on('data', (data) => { output += data.toString(); if (output.includes('%%%done%%%"')) { @@ -85,6 +79,10 @@ export class BackgroundPty implements IPty { } }) + cat.on('close', () => { + console.log('close'); + }) + this.promiseQueue.run(async () => new Promise((resolve) => { const cdCommand = options?.cwd ? `cd ${options.cwd}; ` : ''; // Redirecting everything to the pipe and running in theb background avoids most if not all back-pressure problems @@ -102,7 +100,7 @@ export class BackgroundPty implements IPty { } }); - // console.log(`Running command ${cmd}`) + console.log(`Running command ${cmd}`) this.basePty.write(`${command}\r`); })); From 4b04dd4766c4b6c9a9ba6d67af74e20f30627b7e Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 31 Dec 2024 10:44:52 -0500 Subject: [PATCH 07/15] Moved validation plan to plugin instead of a separate request. Added pty instances to validation refresh. --- package-lock.json | 12 +- package.json | 4 +- src/common/errors.test.ts | 43 ++++ src/common/errors.ts | 31 +++ src/index.ts | 4 + src/plan/plan.test.ts | 12 +- src/plan/plan.ts | 263 ++++++++++++++----------- src/plugin/plugin.test.ts | 129 ++++++++++-- src/plugin/plugin.ts | 30 ++- src/resource/resource-settings.test.ts | 9 +- vitest.config.ts | 1 + 11 files changed, 386 insertions(+), 152 deletions(-) create mode 100644 src/common/errors.test.ts create mode 100644 src/common/errors.ts diff --git a/package-lock.json b/package-lock.json index f0f9ad8..85c109b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "codify-plugin-lib", - "version": "1.0.109", + "version": "1.0.114", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.109", + "version": "1.0.114", "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.53", + "codify-schemas": "1.0.54", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "node-pty": "^1.0.0", @@ -2233,9 +2233,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.53", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.53.tgz", - "integrity": "sha512-4FqoHlWyAIKdu4UR3H3UagSDzVgbEKrTJVGQTG7CbWgog2bihwA9Mr4PLlxF2M0ppHHKOfTE2vF8ikHEnBsWUQ==", + "version": "1.0.54", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.54.tgz", + "integrity": "sha512-OBFLtYu2EV8nNqQkWuegAXIJ7eyHf2Nkp3WoUJS4ZLG6FA0spmStxALAOkCFbALL1kUV2H6ABCD/Kus4C55Djw==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index c0c5c49..956b04e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.114", + "version": "1.0.116", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -14,7 +14,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.53", + "codify-schemas": "1.0.54", "@npmcli/promise-spawn": "^7.0.1", "uuid": "^10.0.0", "lodash.isequal": "^4.5.0", diff --git a/src/common/errors.test.ts b/src/common/errors.test.ts new file mode 100644 index 0000000..55415dd --- /dev/null +++ b/src/common/errors.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { ApplyValidationError } from './errors.js'; +import { testPlan } from '../utils/test-utils.test.js'; + +describe('Test file for errors file', () => { + it('Can properly format ApplyValidationError', () => { + const plan = testPlan({ + desired: null, + current: [{ propZ: ['a', 'b', 'c'] }], + state: { propZ: ['a', 'b', 'c'] }, + core: { + type: 'homebrew', + name: 'first' + }, + statefulMode: true, + }) + + try { + throw new ApplyValidationError(plan); + } catch (e) { + console.error(e); + expect(e.message).toMatch( + `Failed to apply changes to resource: "homebrew.first". Additional changes are needed to complete apply. +Changes remaining: +{ + "operation": "destroy", + "parameters": [ + { + "name": "propZ", + "operation": "remove", + "currentValue": [ + "a", + "b", + "c" + ], + "desiredValue": null + } + ] +}` + ) + } + }) +}) diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..eefe577 --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1,31 @@ +import { Plan } from '../plan/plan.js'; + +export class ApplyValidationError extends Error { + resourceType: string; + resourceName?: string; + plan: Plan; + + constructor(plan: Plan) { + super(`Failed to apply changes to resource: "${plan.resourceId}". Additional changes are needed to complete apply.\nChanges remaining:\n${ApplyValidationError.prettyPrintPlan(plan)}`); + + this.resourceType = plan.coreParameters.type; + this.resourceName = plan.coreParameters.name; + this.plan = plan; + } + + private static prettyPrintPlan(plan: Plan): string { + const { operation, parameters } = plan.toResponse(); + + const prettyParameters = parameters.map(({ name, operation, previousValue, newValue }) => ({ + name, + operation, + currentValue: previousValue, + desiredValue: newValue, + })); + + return JSON.stringify({ + operation, + parameters: prettyParameters, + }, null, 2); + } +} diff --git a/src/index.ts b/src/index.ts index 1942f33..4c1d3db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,4 +16,8 @@ export * from './utils/utils.js' export async function runPlugin(plugin: Plugin) { const messageHandler = new MessageHandler(plugin); process.on('message', (message) => messageHandler.onMessage(message)) + + process.on('beforeExit', () => { + plugin.kill(); + }) } diff --git a/src/plan/plan.test.ts b/src/plan/plan.test.ts index cd48571..dbecbc2 100644 --- a/src/plan/plan.test.ts +++ b/src/plan/plan.test.ts @@ -19,7 +19,8 @@ describe('Plan entity tests', () => { operation: ParameterOperation.ADD, previousValue: null, newValue: 'propBValue' - }] + }], + statefulMode: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).to.be.null; @@ -47,7 +48,8 @@ describe('Plan entity tests', () => { operation: ParameterOperation.REMOVE, previousValue: 'propBValue', newValue: null, - }] + }], + statefulMode: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).toMatchObject({ @@ -75,7 +77,8 @@ describe('Plan entity tests', () => { operation: ParameterOperation.NOOP, previousValue: 'propBValue', newValue: 'propBValue', - }] + }], + statefulMode: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).toMatchObject({ @@ -112,7 +115,8 @@ describe('Plan entity tests', () => { operation: ParameterOperation.ADD, previousValue: null, newValue: 'propAValue', - }] + }], + statefulMode: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).to.be.null diff --git a/src/plan/plan.ts b/src/plan/plan.ts index fb604a4..333a363 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -23,13 +23,24 @@ import { ChangeSet } from './change-set.js'; */ export class Plan { id: string; + + /** + * List of changes to make + */ changeSet: ChangeSet; - coreParameters: ResourceConfig - constructor(id: string, changeSet: ChangeSet, resourceMetadata: ResourceConfig) { + /** + * Ex: name, type, dependsOn etc. Metadata parameters + */ + coreParameters: ResourceConfig; + + statefulMode: boolean; + + constructor(id: string, changeSet: ChangeSet, resourceMetadata: ResourceConfig, statefulMode: boolean) { this.id = id; this.changeSet = changeSet; this.coreParameters = resourceMetadata; + this.statefulMode = statefulMode; } /** @@ -60,53 +71,10 @@ export class Plan { } } - /** - * When multiples of the same resource are allowed, this matching function will match a given config with one of the - * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use - * the application name and location to match it to our desired configs name and location. - * - * @param params - * @private - */ - private static matchCurrentParameters(params: { - desiredParameters: Partial | null, - currentParametersArray: Partial[] | null, - stateParameters: Partial | null, - settings: ResourceSettings, - statefulMode: boolean, - }): Partial | null { - const { - desiredParameters, - currentParametersArray, - stateParameters, - settings, - statefulMode - } = params; - - if (!settings.allowMultiple) { - return currentParametersArray?.[0] ?? null; - } - - if (!currentParametersArray) { - return null; - } - - if (statefulMode) { - return stateParameters - ? settings.allowMultiple.matcher(stateParameters, currentParametersArray) - : null - } - - return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray); - } - - /** - * The type (id) of the resource - * - * @return string - */ - getResourceType(): string { - return this.coreParameters.type + get resourceId(): string { + return this.coreParameters.name + ? `${this.coreParameters.type}.${this.coreParameters.name}` + : this.coreParameters.type; } static calculate(params: { @@ -148,6 +116,7 @@ export class Plan { uuidV4(), ChangeSet.empty(), coreParameters, + statefulMode, ) } @@ -156,7 +125,8 @@ export class Plan { return new Plan( uuidV4(), ChangeSet.create(desiredParameters), - coreParameters + coreParameters, + statefulMode, ) } @@ -165,7 +135,8 @@ export class Plan { return new Plan( uuidV4(), ChangeSet.destroy(filteredCurrentParameters), - coreParameters + coreParameters, + statefulMode, ) } @@ -180,7 +151,128 @@ export class Plan { uuidV4(), changeSet, coreParameters, + statefulMode, + ); + } + + /** + * When multiples of the same resource are allowed, this matching function will match a given config with one of the + * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use + * the application name and location to match it to our desired configs name and location. + * + * @param params + * @private + */ + private static matchCurrentParameters(params: { + desiredParameters: Partial | null, + currentParametersArray: Partial[] | null, + stateParameters: Partial | null, + settings: ResourceSettings, + statefulMode: boolean, + }): Partial | null { + const { + desiredParameters, + currentParametersArray, + stateParameters, + settings, + statefulMode + } = params; + + if (!settings.allowMultiple) { + return currentParametersArray?.[0] ?? null; + } + + if (!currentParametersArray) { + return null; + } + + if (statefulMode) { + return stateParameters + ? settings.allowMultiple.matcher(stateParameters, currentParametersArray) + : null + } + + return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray); + } + + /** + * The type (id) of the resource + * + * @return string + */ + getResourceType(): string { + return this.coreParameters.type + } + + // 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted + static fromResponse(data: ApplyRequestData['plan'], defaultValues?: Partial>): Plan { + if (!data) { + throw new Error('Data is empty'); + } + + addDefaultValues(); + + return new Plan( + uuidV4(), + new ChangeSet( + data.operation, + data.parameters + ), + { + type: data.resourceType, + name: data.resourceName, + }, + data.statefulMode ); + + function addDefaultValues(): void { + Object.entries(defaultValues ?? {}) + .forEach(([key, defaultValue]) => { + const configValueExists = data! + .parameters + .some((p) => p.name === key); + + // Only set default values if the value does not exist in the config + if (configValueExists) { + return; + } + + switch (data!.operation) { + case ResourceOperation.CREATE: { + data!.parameters.push({ + name: key, + operation: ParameterOperation.ADD, + previousValue: null, + newValue: defaultValue, + }); + break; + } + + case ResourceOperation.DESTROY: { + data!.parameters.push({ + name: key, + operation: ParameterOperation.REMOVE, + previousValue: defaultValue, + newValue: null, + }); + break; + } + + case ResourceOperation.MODIFY: + case ResourceOperation.RECREATE: + case ResourceOperation.NOOP: { + data!.parameters.push({ + name: key, + operation: ParameterOperation.NOOP, + previousValue: defaultValue, + newValue: defaultValue, + }); + break; + } + } + }); + } + } /** @@ -322,74 +414,9 @@ export class Plan { // TODO: This needs to be revisited. I don't think this is valid anymore. // 1. For all scenarios, there shouldn't be an apply without a plan beforehand - // 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted - static fromResponse(data: ApplyRequestData['plan'], defaultValues?: Partial>): Plan { - if (!data) { - throw new Error('Data is empty'); - } - - addDefaultValues(); - - return new Plan( - uuidV4(), - new ChangeSet( - data.operation, - data.parameters - ), - { - type: data.resourceType, - name: data.resourceName, - }, - ); - - function addDefaultValues(): void { - Object.entries(defaultValues ?? {}) - .forEach(([key, defaultValue]) => { - const configValueExists = data! - .parameters - .some((p) => p.name === key); - - // Only set default values if the value does not exist in the config - if (configValueExists) { - return; - } - - switch (data!.operation) { - case ResourceOperation.CREATE: { - data!.parameters.push({ - name: key, - operation: ParameterOperation.ADD, - previousValue: null, - newValue: defaultValue, - }); - break; - } - - case ResourceOperation.DESTROY: { - data!.parameters.push({ - name: key, - operation: ParameterOperation.REMOVE, - previousValue: defaultValue, - newValue: null, - }); - break; - } - - case ResourceOperation.MODIFY: - case ResourceOperation.RECREATE: - case ResourceOperation.NOOP: { - data!.parameters.push({ - name: key, - operation: ParameterOperation.NOOP, - previousValue: defaultValue, - newValue: defaultValue, - }); - break; - } - } - }); - } + requiresChanges(): boolean { + return this.changeSet.operation !== ResourceOperation.NOOP; } /** diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index 2e5af33..ce4d360 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from 'vitest'; import { Plugin } from './plugin.js'; -import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas'; +import { ApplyRequestData, ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas'; import { Resource } from '../resource/resource.js'; import { Plan } from '../plan/plan.js'; import { spy } from 'sinon'; import { ResourceSettings } from '../resource/resource-settings.js'; +import { TestConfig } from '../utils/test-utils.test.js'; +import { ApplyValidationError } from '../common/errors.js'; +import { getPty } from '../pty/index.js'; interface TestConfig extends StringIndexedObject { propA: string; @@ -38,15 +41,22 @@ class TestResource extends Resource { describe('Plugin tests', () => { it('Can apply resource', async () => { - const resource= spy(new TestResource()) + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + return { + propA: 'abc', + } + } + }) const plugin = Plugin.create('testPlugin', [resource as any]) - const plan = { + const plan: ApplyRequestData['plan'] = { operation: ResourceOperation.CREATE, resourceType: 'testResource', parameters: [ { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null }, - ] + ], + statefulMode: false, }; await plugin.apply({ plan }); @@ -54,15 +64,20 @@ describe('Plugin tests', () => { }); it('Can destroy resource', async () => { - const resource = spy(new TestResource()); + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + return null; + } + }); const testPlugin = Plugin.create('testPlugin', [resource as any]) - const plan = { + const plan: ApplyRequestData['plan'] = { operation: ResourceOperation.DESTROY, resourceType: 'testResource', parameters: [ { name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' }, - ] + ], + statefulMode: true, }; await testPlugin.apply({ plan }) @@ -70,15 +85,22 @@ describe('Plugin tests', () => { }); it('Can re-create resource', async () => { - const resource = spy(new TestResource()) + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + return { + propA: 'def', + } + } + }) const testPlugin = Plugin.create('testPlugin', [resource as any]) - const plan = { + const plan: ApplyRequestData['plan'] = { operation: ResourceOperation.RECREATE, resourceType: 'testResource', parameters: [ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' }, - ] + ], + statefulMode: false, }; await testPlugin.apply({ plan }) @@ -87,15 +109,22 @@ describe('Plugin tests', () => { }); it('Can modify resource', async () => { - const resource = spy(new TestResource()) + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + return { + propA: 'def', + } + } + }) const testPlugin = Plugin.create('testPlugin', [resource as any]) - const plan = { + const plan: ApplyRequestData['plan'] = { operation: ResourceOperation.MODIFY, resourceType: 'testResource', parameters: [ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' }, - ] + ], + statefulMode: false, }; await testPlugin.apply({ plan }) @@ -178,4 +207,78 @@ describe('Plugin tests', () => { requiredParameters: [] }) }) + + it('Fails an apply if the validation fails', async () => { + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + return { + propA: 'abc', + } + } + }) + const testPlugin = Plugin.create('testPlugin', [resource as any]) + + const plan: ApplyRequestData['plan'] = { + operation: ResourceOperation.MODIFY, + resourceType: 'testResource', + parameters: [ + { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' }, + ], + statefulMode: false, + }; + + await expect(() => testPlugin.apply({ plan })) + .rejects + .toThrowError(new ApplyValidationError(Plan.fromResponse(plan))); + expect(resource.modify.calledOnce).to.be.true; + }) + + it('Allows the usage of pty in refresh (plan)', async () => { + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + expect(getPty()).to.not.be.undefined; + expect(getPty()).to.not.be.null; + + return null; + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + await testPlugin.plan({ + desired: { + type: 'testResource' + }, + state: undefined, + isStateful: false, + }) + + expect(resource.refresh.calledOnce).to.be.true; + }); + + it('Allows the usage of pty in validation refresh (apply)', async () => { + const resource = spy(new class extends TestResource { + async refresh(): Promise | null> { + expect(getPty()).to.not.be.undefined; + expect(getPty()).to.not.be.null; + + return { + propA: 'abc' + }; + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + + const plan: ApplyRequestData['plan'] = { + operation: ResourceOperation.CREATE, + resourceType: 'testResource', + parameters: [ + { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null }, + ], + statefulMode: false, + }; + + await testPlugin.apply({ plan }) + expect(resource.refresh.calledOnce).to.be.true; + }) }); diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index d6147b4..9c69af8 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -13,11 +13,13 @@ import { ValidateResponseData } from 'codify-schemas'; +import { ApplyValidationError } from '../common/errors.js'; import { Plan } from '../plan/plan.js'; +import { BackgroundPty } from '../pty/background-pty.js'; +import { getPty } from '../pty/index.js'; import { Resource } from '../resource/resource.js'; import { ResourceController } from '../resource/resource-controller.js'; import { ptyLocalStorage } from '../utils/pty-local-storage.js'; -import { BackgroundPty } from '../pty/background-pty.js'; export class Plugin { planStorage: Map>; @@ -122,13 +124,11 @@ export class Plugin { throw new Error(`Resource type not found: ${type}`); } - const plan = await ptyLocalStorage.run(this.planPty, async () => { - return this.resourceControllers.get(type)!.plan( + const plan = await ptyLocalStorage.run(this.planPty, async () => this.resourceControllers.get(type)!.plan( data.desired ?? null, data.state ?? null, data.isStateful - ); - }) + )) this.planStorage.set(plan.id, plan); @@ -148,6 +148,25 @@ export class Plugin { } await resource.apply(plan); + + const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => { + const result = await resource.plan( + plan.desiredConfig, + plan.currentConfig, + plan.statefulMode + ); + + await getPty().kill(); + return result; + }) + + if (validationPlan.requiresChanges()) { + throw new ApplyValidationError(plan); + } + } + + async kill() { + await this.planPty.kill(); } private resolvePlan(data: ApplyRequestData): Plan { @@ -170,5 +189,4 @@ export class Plugin { } protected async crossValidateResources(configs: ResourceConfig[]): Promise {} - } diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index 692192e..8bd70dd 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -499,7 +499,8 @@ describe('Resource parameter tests', () => { { name: 'propA', operation: ParameterOperation.ADD, previousValue: null, newValue: null }, { name: 'propB', operation: ParameterOperation.ADD, previousValue: null, newValue: null }, { name: 'propC', operation: ParameterOperation.ADD, previousValue: null, newValue: null }, - ] + ], + statefulMode: false, }, {}) as any ); @@ -521,7 +522,8 @@ describe('Resource parameter tests', () => { { name: 'propA', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null }, { name: 'propB', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null }, { name: 'propC', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null }, - ] + ], + statefulMode: false, }, {}) as any ); @@ -539,7 +541,8 @@ describe('Resource parameter tests', () => { { name: 'propA', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null }, { name: 'propB', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null }, { name: 'propC', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null }, - ] + ], + statefulMode: false, }, {}) as any ); diff --git a/vitest.config.ts b/vitest.config.ts index 0316135..5051350 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,7 @@ import { defaultExclude, defineConfig } from 'vitest/config'; export default defineConfig({ test: { + pool: 'forks', exclude: [ ...defaultExclude, './src/utils/test-utils.test.ts', From d6a396655fa05b9baa49fbedb7741e8eedb0d2fb Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 31 Dec 2024 11:51:01 -0500 Subject: [PATCH 08/15] fix: Added missing pty to import --- package.json | 2 +- src/plugin/plugin.ts | 8 +++++--- src/pty/background-pty.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 956b04e..b728813 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.116", + "version": "1.0.118", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 9c69af8..31eda75 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -87,9 +87,11 @@ export class Plugin { throw new Error(`Cannot get info for resource ${data.config.type}, resource doesn't exist`); } - const result = await this.resourceControllers - .get(data.config.type!) - ?.import(data.config); + const result = await ptyLocalStorage.run(this.planPty, () => + this.resourceControllers + .get(data.config.type!) + ?.import(data.config) + ) return { request: data.config, diff --git a/src/pty/background-pty.ts b/src/pty/background-pty.ts index 1e8beef..bacc6a8 100644 --- a/src/pty/background-pty.ts +++ b/src/pty/background-pty.ts @@ -117,7 +117,7 @@ export class BackgroundPty implements IPty { resolve(status); }) - this.basePty.kill() + this.basePty.kill('SIGKILL') }) } From 30eb77c9cbfc3c53406ebaf2244d03e4b0f38707 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 7 Jan 2025 09:06:27 -0500 Subject: [PATCH 09/15] fix: Added current config for validation plan if desired could not be found --- package.json | 2 +- src/plugin/plugin.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b728813..21c3282 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.118", + "version": "1.0.121", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 31eda75..2d8bfb8 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -151,10 +151,12 @@ export class Plugin { await resource.apply(plan); + // Validate using desired/desired. If the apply was successful, no changes should be reported back. + // Default back desired back to current if it is not defined (for destroys only) const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => { const result = await resource.plan( plan.desiredConfig, - plan.currentConfig, + plan.desiredConfig ?? plan.currentConfig, plan.statefulMode ); From 821c849383c57d6604bbfe5428c551d64a64dee9 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Wed, 8 Jan 2025 22:50:15 -0500 Subject: [PATCH 10/15] feat: Switched back to homebridge multi-arch build --- package-lock.json | 401 +++++++++++++++++++++++++++++++++++--- package.json | 4 +- src/pty/background-pty.ts | 8 +- 3 files changed, 377 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85c109b..74c67e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { "name": "codify-plugin-lib", - "version": "1.0.114", + "version": "1.0.125", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.114", + "version": "1.0.125", "license": "ISC", "dependencies": { + "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "codify-schemas": "1.0.54", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", - "node-pty": "^1.0.0", "strip-ansi": "^7.1.0", "uuid": "^10.0.0" }, @@ -651,6 +651,17 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@homebridge/node-pty-prebuilt-multiarch": { + "version": "0.12.0-beta.5", + "resolved": "https://registry.npmjs.org/@homebridge/node-pty-prebuilt-multiarch/-/node-pty-prebuilt-multiarch-0.12.0-beta.5.tgz", + "integrity": "sha512-i6J1ryzzDE0XY6caawiq5tymY6wbqoO6cBag31Hu6omDWSG0d93YapYab6uP2GJyelYvTJyxJcapOUI9MxYnEg==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^7.1.0", + "prebuild-install": "^7.1.2", + "semver": "^7.6.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1998,6 +2009,35 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2019,6 +2059,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -2169,6 +2232,11 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2408,6 +2476,20 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2418,6 +2500,14 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2458,6 +2548,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -2527,6 +2625,14 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", @@ -3451,6 +3557,14 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/fancy-test": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/fancy-test/-/fancy-test-3.0.16.tgz", @@ -3661,6 +3775,11 @@ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "dev": true }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3795,6 +3914,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4042,6 +4166,25 @@ "node": ">=4" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4099,8 +4242,12 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/internal-slot": { "version": "1.0.7", @@ -4731,6 +4878,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -4759,11 +4917,15 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/mlly": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", @@ -4788,11 +4950,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" - }, "node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -4810,6 +4967,11 @@ "node": "^18 || >=20" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4858,21 +5020,28 @@ "node": ">= 10.13" } }, + "node_modules/node-abi": { + "version": "3.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, "node_modules/node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==", "dev": true }, - "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", - "hasInstallScript": true, - "dependencies": { - "nan": "^2.17.0" - } - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5019,7 +5188,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -5301,6 +5469,31 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5360,6 +5553,15 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5394,6 +5596,28 @@ "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", "dev": true }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5502,6 +5726,19 @@ "node": ">=8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -5728,6 +5965,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -5746,10 +6002,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -5879,6 +6134,49 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -6064,6 +6362,14 @@ "duplexer": "~0.1.1" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -6295,6 +6601,32 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6454,6 +6786,17 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6608,6 +6951,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -6993,8 +7341,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/yn": { "version": "3.1.1", diff --git a/package.json b/package.json index 21c3282..d2d5172 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.121", + "version": "1.0.125", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -16,10 +16,10 @@ "ajv-formats": "^2.1.1", "codify-schemas": "1.0.54", "@npmcli/promise-spawn": "^7.0.1", + "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", - "node-pty": "^1.0.0", "strip-ansi": "^7.1.0" }, "devDependencies": { diff --git a/src/pty/background-pty.ts b/src/pty/background-pty.ts index bacc6a8..db491d6 100644 --- a/src/pty/background-pty.ts +++ b/src/pty/background-pty.ts @@ -1,8 +1,8 @@ +import pty from '@homebridge/node-pty-prebuilt-multiarch'; import { nanoid } from 'nanoid'; import * as cp from 'node:child_process'; import { EventEmitter } from 'node:events'; import * as fs from 'node:fs/promises'; -import pty from 'node-pty'; import stripAnsi from 'strip-ansi'; import { debugLog } from '../utils/debug.js'; @@ -79,10 +79,6 @@ export class BackgroundPty implements IPty { } }) - cat.on('close', () => { - console.log('close'); - }) - this.promiseQueue.run(async () => new Promise((resolve) => { const cdCommand = options?.cwd ? `cd ${options.cwd}; ` : ''; // Redirecting everything to the pipe and running in theb background avoids most if not all back-pressure problems @@ -105,8 +101,6 @@ export class BackgroundPty implements IPty { })); }).finally(async () => { - // console.log('finally'); - // await fileHandle?.close(); await fs.rm(`/tmp/${cid}`); }) } From d2165054fbcbcb08164008a3f9754875aac29396 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 12 Jan 2025 10:30:45 -0500 Subject: [PATCH 11/15] Removed done from being printed to stdout --- package.json | 2 +- src/pty/background-pty.test.ts | 3 +-- src/pty/background-pty.ts | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d2d5172..5b7be0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.125", + "version": "1.0.127", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/pty/background-pty.test.ts b/src/pty/background-pty.test.ts index 901695e..3475d26 100644 --- a/src/pty/background-pty.test.ts +++ b/src/pty/background-pty.test.ts @@ -14,8 +14,7 @@ describe('BackgroundPty tests', () => { const exitCode = await pty.kill(); expect(exitCode).toMatchObject({ - exitCode: 1, - signal: 0, + exitCode: 0, }); }) diff --git a/src/pty/background-pty.ts b/src/pty/background-pty.ts index db491d6..faa1f3d 100644 --- a/src/pty/background-pty.ts +++ b/src/pty/background-pty.ts @@ -55,7 +55,6 @@ export class BackgroundPty implements IPty { return new Promise((resolve) => { const cat = cp.spawn('cat', [`/tmp/${cid}`]) - cat.stdout.pipe(process.stdout); let output = ''; cat.stdout.on('data', (data) => { @@ -76,6 +75,8 @@ export class BackgroundPty implements IPty { exitCode: Number.parseInt(exit ?? 1, 10), data: strippedData, }); + } else { + process.stdout.write(data); } }) From aad4a4875dd028e1701ce2bd3f5c39b5bb6003c0 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 12 Jan 2025 21:26:28 -0500 Subject: [PATCH 12/15] Refactored statefulMode to isStateful --- package-lock.json | 12 +- package.json | 4 +- src/common/errors.test.ts | 2 +- src/plan/plan.test.ts | 10 +- src/plan/plan.ts | 131 +++++++++--------- src/plugin/plugin.test.ts | 12 +- src/plugin/plugin.ts | 2 +- src/pty/vitest.config.ts | 1 - src/resource/resource-controller.test.ts | 4 +- src/resource/resource-controller.ts | 12 +- src/resource/resource-settings.test.ts | 6 +- .../stateful-parameter-controller.test.ts | 2 +- src/utils/test-utils.test.ts | 4 +- vitest.config.ts | 1 - 14 files changed, 101 insertions(+), 102 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74c67e7..0fa6fe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "codify-plugin-lib", - "version": "1.0.125", + "version": "1.0.128", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.125", + "version": "1.0.128", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.54", + "codify-schemas": "1.0.60", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "strip-ansi": "^7.1.0", @@ -2301,9 +2301,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.54", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.54.tgz", - "integrity": "sha512-OBFLtYu2EV8nNqQkWuegAXIJ7eyHf2Nkp3WoUJS4ZLG6FA0spmStxALAOkCFbALL1kUV2H6ABCD/Kus4C55Djw==", + "version": "1.0.60", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.60.tgz", + "integrity": "sha512-5+h5Ft+6KtI7LkoYbh1un9DOE4AMGotEZlMV+dqlpX5P8nD6eNjn5NkZwc5ChytuAmYzsUM2iEHo5cxUXNsuoA==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index 5b7be0f..64a5cb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.127", + "version": "1.0.129", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -14,7 +14,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.54", + "codify-schemas": "1.0.60", "@npmcli/promise-spawn": "^7.0.1", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", diff --git a/src/common/errors.test.ts b/src/common/errors.test.ts index 55415dd..ae61066 100644 --- a/src/common/errors.test.ts +++ b/src/common/errors.test.ts @@ -12,7 +12,7 @@ describe('Test file for errors file', () => { type: 'homebrew', name: 'first' }, - statefulMode: true, + isStateful: true, }) try { diff --git a/src/plan/plan.test.ts b/src/plan/plan.test.ts index dbecbc2..658a0ca 100644 --- a/src/plan/plan.test.ts +++ b/src/plan/plan.test.ts @@ -20,7 +20,7 @@ describe('Plan entity tests', () => { previousValue: null, newValue: 'propBValue' }], - statefulMode: false, + isStateful: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).to.be.null; @@ -49,7 +49,7 @@ describe('Plan entity tests', () => { previousValue: 'propBValue', newValue: null, }], - statefulMode: false, + isStateful: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).toMatchObject({ @@ -78,7 +78,7 @@ describe('Plan entity tests', () => { previousValue: 'propBValue', newValue: 'propBValue', }], - statefulMode: false, + isStateful: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).toMatchObject({ @@ -116,7 +116,7 @@ describe('Plan entity tests', () => { previousValue: null, newValue: 'propAValue', }], - statefulMode: false, + isStateful: false, }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).to.be.null @@ -142,7 +142,7 @@ describe('Plan entity tests', () => { name: 'name1' }, settings: new ParsedResourceSettings({ id: 'type' }), - statefulMode: false, + isStateful: false, }); expect(plan.toResponse()).toMatchObject({ diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 333a363..a41890a 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -34,13 +34,13 @@ export class Plan { */ coreParameters: ResourceConfig; - statefulMode: boolean; + isStateful: boolean; - constructor(id: string, changeSet: ChangeSet, resourceMetadata: ResourceConfig, statefulMode: boolean) { + constructor(id: string, changeSet: ChangeSet, resourceMetadata: ResourceConfig, isStateful: boolean) { this.id = id; this.changeSet = changeSet; this.coreParameters = resourceMetadata; - this.statefulMode = statefulMode; + this.isStateful = isStateful; } /** @@ -83,7 +83,7 @@ export class Plan { stateParameters: Partial | null, coreParameters: ResourceConfig, settings: ParsedResourceSettings, - statefulMode: boolean, + isStateful: boolean, }): Plan { const { desiredParameters, @@ -91,7 +91,7 @@ export class Plan { stateParameters, coreParameters, settings, - statefulMode + isStateful } = params const currentParameters = Plan.matchCurrentParameters({ @@ -99,7 +99,7 @@ export class Plan { currentParametersArray, stateParameters, settings, - statefulMode + isStateful }); const filteredCurrentParameters = Plan.filterCurrentParams({ @@ -107,7 +107,7 @@ export class Plan { currentParameters, stateParameters, settings, - statefulMode + isStateful }); // Empty @@ -116,7 +116,7 @@ export class Plan { uuidV4(), ChangeSet.empty(), coreParameters, - statefulMode, + isStateful, ) } @@ -126,7 +126,7 @@ export class Plan { uuidV4(), ChangeSet.create(desiredParameters), coreParameters, - statefulMode, + isStateful, ) } @@ -136,7 +136,7 @@ export class Plan { uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters, - statefulMode, + isStateful, ) } @@ -151,59 +151,10 @@ export class Plan { uuidV4(), changeSet, coreParameters, - statefulMode, + isStateful, ); } - /** - * When multiples of the same resource are allowed, this matching function will match a given config with one of the - * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use - * the application name and location to match it to our desired configs name and location. - * - * @param params - * @private - */ - private static matchCurrentParameters(params: { - desiredParameters: Partial | null, - currentParametersArray: Partial[] | null, - stateParameters: Partial | null, - settings: ResourceSettings, - statefulMode: boolean, - }): Partial | null { - const { - desiredParameters, - currentParametersArray, - stateParameters, - settings, - statefulMode - } = params; - - if (!settings.allowMultiple) { - return currentParametersArray?.[0] ?? null; - } - - if (!currentParametersArray) { - return null; - } - - if (statefulMode) { - return stateParameters - ? settings.allowMultiple.matcher(stateParameters, currentParametersArray) - : null - } - - return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray); - } - - /** - * The type (id) of the resource - * - * @return string - */ - getResourceType(): string { - return this.coreParameters.type - } - // 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted static fromResponse(data: ApplyRequestData['plan'], defaultValues?: Partial>): Plan { if (!data) { @@ -222,7 +173,7 @@ export class Plan { type: data.resourceType, name: data.resourceName, }, - data.statefulMode + data.isStateful ); function addDefaultValues(): void { @@ -275,6 +226,55 @@ export class Plan { } + /** + * The type (id) of the resource + * + * @return string + */ + getResourceType(): string { + return this.coreParameters.type + } + + /** + * When multiples of the same resource are allowed, this matching function will match a given config with one of the + * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use + * the application name and location to match it to our desired configs name and location. + * + * @param params + * @private + */ + private static matchCurrentParameters(params: { + desiredParameters: Partial | null, + currentParametersArray: Partial[] | null, + stateParameters: Partial | null, + settings: ResourceSettings, + isStateful: boolean, + }): Partial | null { + const { + desiredParameters, + currentParametersArray, + stateParameters, + settings, + isStateful + } = params; + + if (!settings.allowMultiple) { + return currentParametersArray?.[0] ?? null; + } + + if (!currentParametersArray) { + return null; + } + + if (isStateful) { + return stateParameters + ? settings.allowMultiple.matcher(stateParameters, currentParametersArray) + : null + } + + return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray); + } + /** * Only keep relevant params for the plan. We don't want to change settings that were not already * defined. @@ -288,14 +288,14 @@ export class Plan { currentParameters: Partial | null, stateParameters: Partial | null, settings: ResourceSettings, - statefulMode: boolean, + isStateful: boolean, }): Partial | null { const { desiredParameters: desired, currentParameters: current, stateParameters: state, settings, - statefulMode + isStateful } = params; if (!current) { @@ -309,7 +309,7 @@ export class Plan { // For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode // requires additional filtering for stateful parameter arrays and objects. - if (statefulMode) { + if (isStateful) { return filteredCurrent; } @@ -327,7 +327,7 @@ export class Plan { return null; } - if (statefulMode) { + if (isStateful) { const keys = new Set([...Object.keys(state ?? {}), ...Object.keys(desired ?? {})]); return Object.fromEntries( Object.entries(current) @@ -426,6 +426,7 @@ export class Plan { return { planId: this.id, operation: this.changeSet.operation, + isStateful: this.isStateful, resourceName: this.coreParameters.name, resourceType: this.coreParameters.type, parameters: this.changeSet.parameterChanges, diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index ce4d360..ce027f5 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -56,7 +56,7 @@ describe('Plugin tests', () => { parameters: [ { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null }, ], - statefulMode: false, + isStateful: false, }; await plugin.apply({ plan }); @@ -77,7 +77,7 @@ describe('Plugin tests', () => { parameters: [ { name: 'propA', operation: ParameterOperation.REMOVE, newValue: null, previousValue: 'abc' }, ], - statefulMode: true, + isStateful: true, }; await testPlugin.apply({ plan }) @@ -100,7 +100,7 @@ describe('Plugin tests', () => { parameters: [ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' }, ], - statefulMode: false, + isStateful: false, }; await testPlugin.apply({ plan }) @@ -124,7 +124,7 @@ describe('Plugin tests', () => { parameters: [ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' }, ], - statefulMode: false, + isStateful: false, }; await testPlugin.apply({ plan }) @@ -224,7 +224,7 @@ describe('Plugin tests', () => { parameters: [ { name: 'propA', operation: ParameterOperation.MODIFY, newValue: 'def', previousValue: 'abc' }, ], - statefulMode: false, + isStateful: false, }; await expect(() => testPlugin.apply({ plan })) @@ -275,7 +275,7 @@ describe('Plugin tests', () => { parameters: [ { name: 'propA', operation: ParameterOperation.ADD, newValue: 'abc', previousValue: null }, ], - statefulMode: false, + isStateful: false, }; await testPlugin.apply({ plan }) diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 2d8bfb8..018ace7 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -157,7 +157,7 @@ export class Plugin { const result = await resource.plan( plan.desiredConfig, plan.desiredConfig ?? plan.currentConfig, - plan.statefulMode + plan.isStateful ); await getPty().kill(); diff --git a/src/pty/vitest.config.ts b/src/pty/vitest.config.ts index d7d5fb7..267992f 100644 --- a/src/pty/vitest.config.ts +++ b/src/pty/vitest.config.ts @@ -6,7 +6,6 @@ export default defineConfig({ fileParallelism: false, exclude: [ ...defaultExclude, - './src/utils/test-utils.test.ts', ] }, }); diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 47b62d1..eade056 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -150,7 +150,7 @@ describe('Resource tests', () => { testPlan({ current: [{ propA: 'a', propB: 0 }], state: { propA: 'a', propB: 0 }, - statefulMode: true, + isStateful: true, }) ) @@ -169,7 +169,7 @@ describe('Resource tests', () => { testPlan({ desired: { propA: 'a', propB: 0 }, current: [{ propA: 'b', propB: -1 }], - statefulMode: true + isStateful: true }) ); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 8e1b329..a729bbc 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -106,9 +106,9 @@ export class ResourceController { async plan( desiredConfig: Partial & ResourceConfig | null, stateConfig: Partial & ResourceConfig | null = null, - statefulMode = false, + isStateful = false, ): Promise> { - this.validatePlanInputs(desiredConfig, stateConfig, statefulMode); + this.validatePlanInputs(desiredConfig, stateConfig, isStateful); this.addDefaultValues(desiredConfig); await this.applyTransformParameters(desiredConfig); @@ -143,7 +143,7 @@ export class ResourceController { stateParameters, coreParameters, settings: this.parsedSettings, - statefulMode, + isStateful, }); } @@ -157,7 +157,7 @@ export class ResourceController { stateParameters, coreParameters, settings: this.parsedSettings, - statefulMode + isStateful }) } @@ -377,13 +377,13 @@ ${JSON.stringify(refresh, null, 2)} private validatePlanInputs( desired: Partial & ResourceConfig | null, current: Partial & ResourceConfig | null, - statefulMode: boolean, + isStateful: boolean, ) { if (!desired && !current) { throw new Error('Desired config and current config cannot both be missing') } - if (!statefulMode && !desired) { + if (!isStateful && !desired) { throw new Error('Desired config must be provided in non-stateful mode') } } diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index 8bd70dd..b8c5406 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -500,7 +500,7 @@ describe('Resource parameter tests', () => { { name: 'propB', operation: ParameterOperation.ADD, previousValue: null, newValue: null }, { name: 'propC', operation: ParameterOperation.ADD, previousValue: null, newValue: null }, ], - statefulMode: false, + isStateful: false, }, {}) as any ); @@ -523,7 +523,7 @@ describe('Resource parameter tests', () => { { name: 'propB', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null }, { name: 'propC', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null }, ], - statefulMode: false, + isStateful: false, }, {}) as any ); @@ -542,7 +542,7 @@ describe('Resource parameter tests', () => { { name: 'propB', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null }, { name: 'propC', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null }, ], - statefulMode: false, + isStateful: false, }, {}) as any ); diff --git a/src/stateful-parameter/stateful-parameter-controller.test.ts b/src/stateful-parameter/stateful-parameter-controller.test.ts index ed6f469..74c42c2 100644 --- a/src/stateful-parameter/stateful-parameter-controller.test.ts +++ b/src/stateful-parameter/stateful-parameter-controller.test.ts @@ -34,7 +34,7 @@ describe('Stateful parameter tests', () => { desired: null, current: [{ propZ: ['a', 'b', 'c'] }], state: { propZ: ['a', 'b', 'c'] }, - statefulMode: true, + isStateful: true, }); expect(plan.changeSet.operation).to.eq(ResourceOperation.DESTROY); diff --git a/src/utils/test-utils.test.ts b/src/utils/test-utils.test.ts index c1b451d..d53c44c 100644 --- a/src/utils/test-utils.test.ts +++ b/src/utils/test-utils.test.ts @@ -12,7 +12,7 @@ export function testPlan(params: { state?: Partial | null; core?: ResourceConfig; settings?: ResourceSettings; - statefulMode?: boolean; + isStateful?: boolean; }) { return Plan.calculate({ desiredParameters: params.desired ?? null, @@ -22,7 +22,7 @@ export function testPlan(params: { settings: params.settings ? new ParsedResourceSettings(params.settings) : new ParsedResourceSettings({ id: 'type' }), - statefulMode: params.statefulMode ?? false, + isStateful: params.isStateful ?? false, }) } diff --git a/vitest.config.ts b/vitest.config.ts index 5051350..0316135 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,6 @@ import { defaultExclude, defineConfig } from 'vitest/config'; export default defineConfig({ test: { - pool: 'forks', exclude: [ ...defaultExclude, './src/utils/test-utils.test.ts', From 77189e348a83b1b3ab8c82275154c9158d3d4a07 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 13 Jan 2025 21:46:25 -0500 Subject: [PATCH 13/15] feat: Refactored and updated to using newest schema which passes core parameters separately from other parameters --- package-lock.json | 12 +- package.json | 2 +- src/messages/handlers.test.ts | 12 +- src/plan/plan.test.ts | 31 +-- src/plan/plan.ts | 96 +++++---- src/plugin/plugin.test.ts | 5 +- src/plugin/plugin.ts | 38 ++-- src/resource/config-parser.ts | 47 +---- .../resource-controller-stateful-mode.test.ts | 8 +- src/resource/resource-controller.test.ts | 75 ++++--- src/resource/resource-controller.ts | 106 +++++----- src/resource/resource-settings.test.ts | 190 ++++++++++-------- .../stateful-parameter-controller.test.ts | 27 ++- src/utils/test-utils.test.ts | 8 +- 14 files changed, 342 insertions(+), 315 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fa6fe5..e5822ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "codify-plugin-lib", - "version": "1.0.128", + "version": "1.0.129", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.128", + "version": "1.0.129", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.60", + "codify-schemas": "1.0.61", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "strip-ansi": "^7.1.0", @@ -2301,9 +2301,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.60", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.60.tgz", - "integrity": "sha512-5+h5Ft+6KtI7LkoYbh1un9DOE4AMGotEZlMV+dqlpX5P8nD6eNjn5NkZwc5ChytuAmYzsUM2iEHo5cxUXNsuoA==", + "version": "1.0.61", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.61.tgz", + "integrity": "sha512-kXzSnISLscW2tRtbGZUhm6nAuuhup0isO9xzhflFy+EsNHzGZBVJj730535h1Ft2jtaiywycQPbOJewgz3/UNA==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index 64a5cb3..ff37d0d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.60", + "codify-schemas": "1.0.61", "@npmcli/promise-spawn": "^7.0.1", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", diff --git a/src/messages/handlers.test.ts b/src/messages/handlers.test.ts index 57466ef..5fba07f 100644 --- a/src/messages/handlers.test.ts +++ b/src/messages/handlers.test.ts @@ -25,9 +25,11 @@ describe('Message handler tests', () => { await handler.onMessage({ cmd: 'plan', data: { - desired: { + core: { type: 'resourceType', name: 'name', + }, + desired: { prop1: 'A', prop2: 'B', }, @@ -45,7 +47,6 @@ describe('Message handler tests', () => { const handler = new MessageHandler(plugin); process.send = (message) => { - console.log(message); expect(message).toMatchObject({ cmd: 'plan_Response', status: MessageStatus.ERROR, @@ -168,6 +169,9 @@ describe('Message handler tests', () => { expect(async () => await handler.onMessage({ cmd: 'plan', data: { + core: { + type: 'resourceA', + }, desired: { type: 'resourceA' }, @@ -250,9 +254,11 @@ describe('Message handler tests', () => { cmd: 'plan', requestId: 'abcdef', data: { - desired: { + core: { type: 'type', name: 'name', + }, + desired: { prop1: 'A', prop2: 'B', }, diff --git a/src/plan/plan.test.ts b/src/plan/plan.test.ts index 658a0ca..257b205 100644 --- a/src/plan/plan.test.ts +++ b/src/plan/plan.test.ts @@ -26,7 +26,6 @@ describe('Plan entity tests', () => { expect(plan.currentConfig).to.be.null; expect(plan.desiredConfig).toMatchObject({ - type: 'type', propA: 'defaultA', propB: 'propBValue', }) @@ -53,7 +52,6 @@ describe('Plan entity tests', () => { }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).toMatchObject({ - type: 'type', propA: 'defaultA', propB: 'propBValue', }) @@ -82,13 +80,11 @@ describe('Plan entity tests', () => { }, controller.parsedSettings.defaultValues); expect(plan.currentConfig).toMatchObject({ - type: 'type', propA: 'defaultA', propB: 'propBValue', }) expect(plan.desiredConfig).toMatchObject({ - type: 'type', propA: 'defaultA', propB: 'propBValue', }) @@ -122,7 +118,6 @@ describe('Plan entity tests', () => { expect(plan.currentConfig).to.be.null expect(plan.desiredConfig).toMatchObject({ - type: 'type', propA: 'propAValue', propB: 'propBValue', }) @@ -134,10 +129,10 @@ describe('Plan entity tests', () => { it('Returns the original resource names', () => { const plan = Plan.calculate({ - desiredParameters: { propA: 'propA' }, - currentParametersArray: [{ propA: 'propA2' }], - stateParameters: null, - coreParameters: { + desired: { propA: 'propA' }, + currentArray: [{ propA: 'propA2' }], + state: null, + core: { type: 'type', name: 'name1' }, @@ -174,9 +169,12 @@ describe('Plan entity tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - propZ: ['20.15'], - } as any) + const plan = await controller.plan( + { type: 'type' }, + { propZ: ['20.15'], } as any, + null, + false + ) expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP); }) @@ -208,9 +206,12 @@ describe('Plan entity tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - propZ: ['20.15'], - } as any) + const plan = await controller.plan( + { type: 'type' }, + { propZ: ['20.15'], } as any, + null, + false + ) expect(plan.changeSet).toMatchObject({ operation: ResourceOperation.MODIFY, diff --git a/src/plan/plan.ts b/src/plan/plan.ts index a41890a..323ccaa 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -36,10 +36,10 @@ export class Plan { isStateful: boolean; - constructor(id: string, changeSet: ChangeSet, resourceMetadata: ResourceConfig, isStateful: boolean) { + constructor(id: string, changeSet: ChangeSet, coreParameters: ResourceConfig, isStateful: boolean) { this.id = id; this.changeSet = changeSet; - this.coreParameters = resourceMetadata; + this.coreParameters = coreParameters; this.isStateful = isStateful; } @@ -51,10 +51,7 @@ export class Plan { return null; } - return { - ...this.coreParameters, - ...this.changeSet.desiredParameters, - } + return this.changeSet.desiredParameters; } /** @@ -65,10 +62,7 @@ export class Plan { return null; } - return { - ...this.coreParameters, - ...this.changeSet.currentParameters, - } + return this.changeSet.currentParameters; } get resourceId(): string { @@ -78,71 +72,71 @@ export class Plan { } static calculate(params: { - desiredParameters: Partial | null, - currentParametersArray: Partial[] | null, - stateParameters: Partial | null, - coreParameters: ResourceConfig, + desired: Partial | null, + currentArray: Partial[] | null, + state: Partial | null, + core: ResourceConfig, settings: ParsedResourceSettings, isStateful: boolean, }): Plan { const { - desiredParameters, - currentParametersArray, - stateParameters, - coreParameters, + desired, + currentArray, + state, + core, settings, isStateful } = params - const currentParameters = Plan.matchCurrentParameters({ - desiredParameters, - currentParametersArray, - stateParameters, + const current = Plan.matchCurrentParameters({ + desired, + currentArray, + state, settings, isStateful }); const filteredCurrentParameters = Plan.filterCurrentParams({ - desiredParameters, - currentParameters, - stateParameters, + desired, + current, + state, settings, isStateful }); // Empty - if (!filteredCurrentParameters && !desiredParameters) { + if (!filteredCurrentParameters && !desired) { return new Plan( uuidV4(), ChangeSet.empty(), - coreParameters, + core, isStateful, ) } // CREATE - if (!filteredCurrentParameters && desiredParameters) { + if (!filteredCurrentParameters && desired) { return new Plan( uuidV4(), - ChangeSet.create(desiredParameters), - coreParameters, + ChangeSet.create(desired), + core, isStateful, ) } // DESTROY - if (filteredCurrentParameters && !desiredParameters) { + if (filteredCurrentParameters && !desired) { return new Plan( uuidV4(), ChangeSet.destroy(filteredCurrentParameters), - coreParameters, + core, isStateful, ) } // NO-OP, MODIFY or RE-CREATE const changeSet = ChangeSet.calculateModification( - desiredParameters!, + desired!, filteredCurrentParameters!, settings.parameterSettings, ); @@ -150,7 +144,7 @@ export class Plan { return new Plan( uuidV4(), changeSet, - coreParameters, + core, isStateful, ); } @@ -244,35 +238,35 @@ export class Plan { * @private */ private static matchCurrentParameters(params: { - desiredParameters: Partial | null, - currentParametersArray: Partial[] | null, - stateParameters: Partial | null, + desired: Partial | null, + currentArray: Partial[] | null, + state: Partial | null, settings: ResourceSettings, isStateful: boolean, }): Partial | null { const { - desiredParameters, - currentParametersArray, - stateParameters, + desired, + currentArray, + state, settings, isStateful } = params; if (!settings.allowMultiple) { - return currentParametersArray?.[0] ?? null; + return currentArray?.[0] ?? null; } - if (!currentParametersArray) { + if (!currentArray) { return null; } if (isStateful) { - return stateParameters - ? settings.allowMultiple.matcher(stateParameters, currentParametersArray) + return state + ? settings.allowMultiple.matcher(state, currentArray) : null } - return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray); + return settings.allowMultiple.matcher(desired!, currentArray); } /** @@ -284,16 +278,16 @@ export class Plan { * or wants to set. If a parameter is not specified then it's not managed by Codify. */ private static filterCurrentParams(params: { - desiredParameters: Partial | null, - currentParameters: Partial | null, - stateParameters: Partial | null, + desired: Partial | null, + current: Partial | null, + state: Partial | null, settings: ResourceSettings, isStateful: boolean, }): Partial | null { const { - desiredParameters: desired, - currentParameters: current, - stateParameters: state, + desired, + current, + state, settings, isStateful } = params; diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index ce027f5..2d53d3e 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -245,9 +245,8 @@ describe('Plugin tests', () => { const testPlugin = Plugin.create('testPlugin', [resource as any]); await testPlugin.plan({ - desired: { - type: 'testResource' - }, + core: { type: 'testResource' }, + desired: {}, state: undefined, isStateful: false, }) diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 018ace7..1ab7302 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -9,6 +9,7 @@ import { PlanRequestData, PlanResponseData, ResourceConfig, + ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas'; @@ -83,18 +84,20 @@ export class Plugin { } async import(data: ImportRequestData): Promise { - if (!this.resourceControllers.has(data.config.type)) { - throw new Error(`Cannot get info for resource ${data.config.type}, resource doesn't exist`); + const { core, parameters } = data; + + if (!this.resourceControllers.has(core.type)) { + throw new Error(`Cannot get info for resource ${core.type}, resource doesn't exist`); } const result = await ptyLocalStorage.run(this.planPty, () => this.resourceControllers - .get(data.config.type!) - ?.import(data.config) + .get(core.type!) + ?.import(core, parameters) ) return { - request: data.config, + request: data, result: result ?? [], } } @@ -102,13 +105,15 @@ export class Plugin { async validate(data: ValidateRequestData): Promise { const validationResults = []; for (const config of data.configs) { - if (!this.resourceControllers.has(config.type)) { - throw new Error(`Resource type not found: ${config.type}`); + const { core, parameters } = config; + + if (!this.resourceControllers.has(core.type)) { + throw new Error(`Resource type not found: ${core.type}`); } const validation = await this.resourceControllers - .get(config.type)! - .validate(config); + .get(core.type)! + .validate(core, parameters); validationResults.push(validation); } @@ -120,16 +125,17 @@ export class Plugin { } async plan(data: PlanRequestData): Promise { - const type = data.desired?.type ?? data.state?.type + const { type } = data.core - if (!type || !this.resourceControllers.has(type)) { + if (!this.resourceControllers.has(type)) { throw new Error(`Resource type not found: ${type}`); } const plan = await ptyLocalStorage.run(this.planPty, async () => this.resourceControllers.get(type)!.plan( - data.desired ?? null, - data.state ?? null, - data.isStateful + data.core, + data.desired ?? null, + data.state ?? null, + data.isStateful )) this.planStorage.set(plan.id, plan); @@ -155,6 +161,7 @@ export class Plugin { // Default back desired back to current if it is not defined (for destroys only) const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => { const result = await resource.plan( + plan.coreParameters, plan.desiredConfig, plan.desiredConfig ?? plan.currentConfig, plan.isStateful @@ -192,5 +199,6 @@ export class Plugin { return Plan.fromResponse(planRequest, resource.parsedSettings.defaultValues); } - protected async crossValidateResources(configs: ResourceConfig[]): Promise {} + protected async crossValidateResources(resources: ResourceJson[]): Promise { + } } diff --git a/src/resource/config-parser.ts b/src/resource/config-parser.ts index 83cf019..2c4747a 100644 --- a/src/resource/config-parser.ts +++ b/src/resource/config-parser.ts @@ -1,16 +1,15 @@ -import { ResourceConfig, StringIndexedObject } from 'codify-schemas'; +import { StringIndexedObject } from 'codify-schemas'; import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js'; -import { splitUserConfig } from '../utils/utils.js'; export class ConfigParser { - private readonly desiredConfig: Partial & ResourceConfig | null; - private readonly stateConfig: Partial & ResourceConfig | null; + private readonly desiredConfig: Partial | null; + private readonly stateConfig: Partial | null; private statefulParametersMap: Map>; constructor( - desiredConfig: Partial & ResourceConfig | null, - stateConfig: Partial & ResourceConfig | null, + desiredConfig: Partial | null, + stateConfig: Partial | null, statefulParameters: Map>, ) { this.desiredConfig = desiredConfig; @@ -18,42 +17,8 @@ export class ConfigParser { this.statefulParametersMap = statefulParameters; } - get coreParameters(): ResourceConfig { - const desiredCoreParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).coreParameters : undefined; - const currentCoreParameters = this.stateConfig ? splitUserConfig(this.stateConfig).coreParameters : undefined; - - if (!desiredCoreParameters && !currentCoreParameters) { - throw new Error(`Unable to parse resource core parameters from: - - Desired: ${JSON.stringify(this.desiredConfig, null, 2)} - - Current: ${JSON.stringify(this.stateConfig, null, 2)}`) - } - - return desiredCoreParameters ?? currentCoreParameters!; - } - - get desiredParameters(): Partial | null { - if (!this.desiredConfig) { - return null; - } - - const { parameters } = splitUserConfig(this.desiredConfig); - return parameters; - } - - get stateParameters(): Partial | null { - if (!this.stateConfig) { - return null; - } - - const { parameters } = splitUserConfig(this.stateConfig); - return parameters; - } - - get allParameters(): Partial { - return { ...this.desiredParameters, ...this.stateParameters } as Partial; + return { ...this.desiredConfig, ...this.stateConfig } as Partial; } get allNonStatefulParameters(): Partial { diff --git a/src/resource/resource-controller-stateful-mode.test.ts b/src/resource/resource-controller-stateful-mode.test.ts index 3b0db10..bffc215 100644 --- a/src/resource/resource-controller-stateful-mode.test.ts +++ b/src/resource/resource-controller-stateful-mode.test.ts @@ -19,9 +19,9 @@ describe('Resource tests for stateful plans', () => { const controller = new ResourceController(resource); const plan = await controller.plan( + { type: 'type' }, null, { - type: 'type', propA: 'propA', propB: 10, propC: 'propC', @@ -67,8 +67,8 @@ describe('Resource tests for stateful plans', () => { const controller = new ResourceController(resource); const plan = await controller.plan( + { type: 'resource' }, { - type: 'resource', propA: 'propA', propB: 10, propC: 'propC', @@ -119,6 +119,7 @@ describe('Resource tests for stateful plans', () => { const controller = new ResourceController(resource) const plan = await controller.plan( + { type: 'type' }, { type: 'type', propA: 'propA', @@ -191,15 +192,14 @@ describe('Resource tests for stateful plans', () => { const controller = new ResourceController(resource); const plan = await controller.plan( + { type: 'type' }, { - type: 'type', propA: 'propA', propB: 10, propC: 'propC', propD: 'propD' }, { - type: 'type', propA: 'propA', propC: 'propC' }, diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index eade056..188f092 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -34,11 +34,14 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - await controller.validate({ - type: 'type', - propA: '~/.tool_versions', - propB: 10, - }) + await controller.validate( + { type: 'type' }, + { + type: 'type', + propA: '~/.tool_versions', + propB: 10, + } + ) }) it('Plans successfully', async () => { @@ -55,16 +58,17 @@ describe('Resource tests', () => { const controller = new ResourceController(resource) const resourceSpy = spy(controller); - const result = await resourceSpy.plan({ - type: 'type', - name: 'name', - propA: 'propA', - propB: 10, - }) + const result = await resourceSpy.plan( + { type: 'type', name: 'name' }, + { + propA: 'propA', + propB: 10, + }, + null, + false, + ) expect(result.desiredConfig).to.deep.eq({ - type: 'type', - name: 'name', propA: 'propA', propB: 10, }); @@ -92,15 +96,16 @@ describe('Resource tests', () => { const controller = new ResourceController(resource); const resourceSpy = spy(controller); - const result = await resourceSpy.plan({ - type: 'type', - name: 'name', - propA: 'propA', - propB: 10, - propC: 'somethingAfter' - }) - - console.log(result.changeSet.parameterChanges) + const result = await resourceSpy.plan( + { type: 'type', name: 'name' }, + { + propA: 'propA', + propB: 10, + propC: 'somethingAfter' + }, + null, + false, + ) expect(result.changeSet.operation).to.eq(ResourceOperation.CREATE); expect(result.changeSet.parameterChanges.length).to.eq(3); @@ -115,7 +120,12 @@ describe('Resource tests', () => { const controller = new ResourceController(resource); const resourceSpy = spy(controller); - const result = await resourceSpy.plan({ type: 'type' }) + const result = await resourceSpy.plan( + { type: 'type' }, + {}, + null, + false + ) expect(result.changeSet.operation).to.eq(ResourceOperation.CREATE); expect(result.changeSet.parameterChanges.length).to.eq(0); @@ -195,7 +205,12 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resource', propA: 'a', propB: 0 }) + const plan = await controller.plan( + { type: 'resource' }, + { propA: 'a', propB: 0 }, + null, + false, + ) const resourceSpy = spy(resource); await controller.apply( @@ -267,7 +282,7 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resource' }) + const plan = await controller.plan({ type: 'resource' }, {}, null, false) expect(plan.currentConfig?.propA).to.eq('propAAfter'); expect(plan.desiredConfig?.propA).to.eq('propADefault'); expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE); @@ -294,7 +309,7 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resource' }) + const plan = await controller.plan({ type: 'resource' }, {}, null, false) expect(plan.currentConfig?.propE).to.eq('propEDefault'); expect(plan.desiredConfig?.propE).to.eq('propEDefault'); expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP); @@ -317,7 +332,7 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resource' }) + const plan = await controller.plan({ type: 'resource' }, {}, null, false) expect(plan.currentConfig).to.be.null expect(plan.desiredConfig!.propE).to.eq('propEDefault'); expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE); @@ -345,7 +360,7 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resource', propA: 'propA' }) + const plan = await controller.plan({ type: 'resource' }, { propA: 'propA' }, null, false) expect(plan.currentConfig?.propA).to.eq('propAAfter'); expect(plan.desiredConfig?.propA).to.eq('propA'); expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE); @@ -455,7 +470,7 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'nvm', global: '20.12', nodeVersions: ['18', '20'] }) + const plan = await controller.plan({ type: 'nvm' }, { global: '20.12', nodeVersions: ['18', '20'] }, null, false) expect(plan).toMatchObject({ changeSet: { @@ -507,7 +522,7 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'nvm', global: '20.12', nodeVersions: ['18', '20'] }) + const plan = await controller.plan({ type: 'nvm' }, { global: '20.12', nodeVersions: ['18', '20'] }, null, false) expect(plan).toMatchObject({ changeSet: { diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index a729bbc..9db2d7b 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -2,6 +2,7 @@ import { Ajv, ValidateFunction } from 'ajv'; import { ParameterOperation, ResourceConfig, + ResourceJson, ResourceOperation, StringIndexedObject, ValidateResponseData @@ -10,7 +11,6 @@ import { import { ParameterChange } from '../plan/change-set.js'; import { Plan } from '../plan/plan.js'; import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js'; -import { splitUserConfig } from '../utils/utils.js'; import { ConfigParser } from './config-parser.js'; import { ParsedResourceSettings } from './parsed-resource-settings.js'; import { Resource } from './resource.js'; @@ -54,23 +54,21 @@ export class ResourceController { } async validate( - desiredConfig: Partial & ResourceConfig, + core: ResourceConfig, + parameters: Partial, ): Promise { - const configToValidate = { ...desiredConfig }; - await this.applyTransformParameters(configToValidate); - - const { parameters, coreParameters } = splitUserConfig(configToValidate); + await this.applyTransformParameters(parameters); this.addDefaultValues(parameters); if (this.schemaValidator) { // Schema validator uses pre transformation parameters - const isValid = this.schemaValidator(splitUserConfig(desiredConfig).parameters); + const isValid = this.schemaValidator(parameters); if (!isValid) { return { isValid: false, - resourceName: coreParameters.name, - resourceType: coreParameters.type, + resourceName: core.name, + resourceType: core.type, schemaValidationErrors: this.schemaValidator?.errors ?? [], } } @@ -89,59 +87,57 @@ export class ResourceController { return { customValidationErrorMessage, isValid: false, - resourceName: coreParameters.name, - resourceType: coreParameters.type, + resourceName: core.name, + resourceType: core.type, schemaValidationErrors: this.schemaValidator?.errors ?? [], } } return { isValid: true, - resourceName: coreParameters.name, - resourceType: coreParameters.type, + resourceName: core.name, + resourceType: core.type, schemaValidationErrors: [], } } async plan( - desiredConfig: Partial & ResourceConfig | null, - stateConfig: Partial & ResourceConfig | null = null, + core: ResourceConfig, + desired: Partial | null, + state: Partial | null, isStateful = false, ): Promise> { - this.validatePlanInputs(desiredConfig, stateConfig, isStateful); + this.validatePlanInputs(core, desired, state, isStateful); - this.addDefaultValues(desiredConfig); - await this.applyTransformParameters(desiredConfig); + this.addDefaultValues(desired); + await this.applyTransformParameters(desired); - this.addDefaultValues(stateConfig); - await this.applyTransformParameters(stateConfig); + this.addDefaultValues(state); + await this.applyTransformParameters(state); // Parse data from the user supplied config - const parsedConfig = new ConfigParser(desiredConfig, stateConfig, this.parsedSettings.statefulParameters) + const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters) const { - coreParameters, - desiredParameters, - stateParameters, allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig; // Refresh resource parameters. This refreshes the parameters that configure the resource itself - const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters); + const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters); // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters - if (currentParametersArray === null - || currentParametersArray === undefined + if (currentArray === null + || currentArray === undefined || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true - || currentParametersArray.length === 0 - || currentParametersArray.filter(Boolean).length === 0 + || currentArray.length === 0 + || currentArray.filter(Boolean).length === 0 ) { return Plan.calculate({ - desiredParameters, - currentParametersArray, - stateParameters, - coreParameters, + desired, + currentArray, + state, + core, settings: this.parsedSettings, isStateful, }); @@ -152,10 +148,10 @@ export class ResourceController { const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, allParameters); return Plan.calculate({ - desiredParameters, - currentParametersArray: [{ ...currentParametersArray[0], ...statefulCurrentParameters }] as Partial[], - stateParameters, - coreParameters, + desired, + currentArray: [{ ...currentArray[0], ...statefulCurrentParameters }] as Partial[], + state, + core, settings: this.parsedSettings, isStateful }) @@ -186,9 +182,12 @@ export class ResourceController { } } - async import(config: Partial & ResourceConfig): Promise<(Partial & ResourceConfig)[] | null> { - this.addDefaultValues(config); - await this.applyTransformParameters(config); + async import( + core: ResourceConfig, + parameters: Partial + ): Promise | null> { + this.addDefaultValues(parameters); + await this.applyTransformParameters(parameters); // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here const parametersToRefresh = this.settings.import?.refreshKeys @@ -197,14 +196,14 @@ export class ResourceController { this.settings.import?.refreshKeys.map((k) => [k, null]) ), ...this.settings.import?.defaultRefreshValues, - ...config, + ...parameters, } : { ...Object.fromEntries( this.getAllParameterKeys().map((k) => [k, null]) ), ...this.settings.import?.defaultRefreshValues, - ...config, + ...parameters, }; // Parse data from the user supplied config @@ -212,7 +211,6 @@ export class ResourceController { const { allNonStatefulParameters, allStatefulParameters, - coreParameters, } = parsedConfig; const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters); @@ -220,16 +218,15 @@ export class ResourceController { if (currentParametersArray === null || currentParametersArray === undefined || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true - || currentParametersArray.length === 0 || currentParametersArray.filter(Boolean).length === 0 ) { return currentParametersArray - ?.map((r) => ({ ...coreParameters, ...r })) + ?.map((r) => ({ core, parameters: r })) ?? null; } const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh); - return [{ ...coreParameters, ...currentParametersArray[0], ...statefulCurrentParameters }]; + return [{ core, parameters: { ...currentParametersArray[0], ...statefulCurrentParameters } }]; } private async applyCreate(plan: Plan): Promise { @@ -308,7 +305,7 @@ ${JSON.stringify(refresh, null, 2)} } } - private async applyTransformParameters(config: Partial & ResourceConfig | null): Promise { + private async applyTransformParameters(config: Partial | null): Promise { if (!config) { return; } @@ -322,11 +319,9 @@ ${JSON.stringify(refresh, null, 2)} } if (this.settings.inputTransformation) { - const { parameters, coreParameters } = splitUserConfig(config); - - const transformed = await this.settings.inputTransformation(parameters) + const transformed = await this.settings.inputTransformation({ ...config }) Object.keys(config).forEach((k) => delete config[k]) - Object.assign(config, transformed, coreParameters); + Object.assign(config, transformed); } } @@ -375,10 +370,15 @@ ${JSON.stringify(refresh, null, 2)} } private validatePlanInputs( - desired: Partial & ResourceConfig | null, - current: Partial & ResourceConfig | null, + core: ResourceConfig, + desired: Partial | null, + current: Partial | null, isStateful: boolean, ) { + if (!core || !core.type) { + throw new Error('Core parameters type must be defined'); + } + if (!desired && !current) { throw new Error('Desired config and current config cannot both be missing') } diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index b8c5406..b5fa66d 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -38,16 +38,19 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - type: 'type', - propA: 'a', - propB: 10 - }) + const plan = await controller.plan( + { type: 'type' }, + { + propA: 'a', + propB: 10 + }, + null, + false + ) expect(statefulParameter.refresh.notCalled).to.be.true; expect(plan.currentConfig).to.be.null; expect(plan.desiredConfig).toMatchObject({ - type: 'type', propA: 'a', propB: 10 }) @@ -116,7 +119,12 @@ describe('Resource parameter tests', () => { const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'type', propA: 'a', propB: 0, propC: 'b' }) + const plan = await controller.plan( + { type: 'type' }, + { propA: 'a', propB: 0, propC: 'b' }, + null, + false + ) const resourceSpy = spy(resource); await controller.apply(plan); @@ -156,12 +164,11 @@ describe('Resource parameter tests', () => { const controller = new ResourceController(resource); const plan = await controller.plan({ type: 'type', - }) + }, {}, null, false) expect(statefulParameter.refresh.notCalled).to.be.true; expect(plan.currentConfig).to.be.null; expect(plan.desiredConfig).toMatchObject({ - type: 'type', propA: 'abc', }) expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE); @@ -196,7 +203,7 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'type', propA: ['a', 'b'] } as any) + const plan = await controller.plan({ type: 'type' }, { propA: ['a', 'b'] } as any, null, false) expect(plan).toMatchObject({ changeSet: { @@ -230,7 +237,7 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'type', propA: ['a', 'b', 'c', 'd'] } as any) + const plan = await controller.plan({ type: 'type' }, { propA: ['a', 'b', 'c', 'd'] } as any, null, false) expect(plan).toMatchObject({ changeSet: { @@ -277,21 +284,25 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - type: 'type', - hosts: [ - { - Host: 'new.com', - AddKeysToAgent: 'yes', - IdentityFile: '~/.ssh/id_ed25519' - }, - { - Host: 'github.com', - AddKeysToAgent: 'yes', - UseKeychain: 'yes', - } - ] - }); + const plan = await controller.plan( + { type: 'type' }, + { + hosts: [ + { + Host: 'new.com', + AddKeysToAgent: 'yes', + IdentityFile: '~/.ssh/id_ed25519' + }, + { + Host: 'github.com', + AddKeysToAgent: 'yes', + UseKeychain: 'yes', + } + ] + }, + null, + false + ); expect(plan).toMatchObject({ 'changeSet': { @@ -361,7 +372,7 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'type', propA: ['3.11'] } as any) + const plan = await controller.plan({ type: 'type' }, { propA: ['3.11'] } as any, null, false) expect(plan).toMatchObject({ changeSet: { @@ -421,14 +432,17 @@ describe('Resource parameter tests', () => { }); const controller = new ResourceController(resource) - const plan = await controller.plan({ - type: 'resourceType', - propA: 'propA', - propB: 10, - propC: 'propC', - propD: 'propD', - propE: 'propE', - }); + const plan = await controller.plan( + { type: 'resourceType' }, + { + propA: 'propA', + propB: 10, + propC: 'propC', + propD: 'propD', + propE: 'propE', + }, null, + false + ); expect(plan.currentConfig?.propB).to.be.lessThan(plan.currentConfig?.propC as any); expect(plan.currentConfig?.propC).to.be.lessThan(plan.currentConfig?.propA as any); @@ -571,7 +585,7 @@ describe('Resource parameter tests', () => { }); const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resourceType', propC: 'abc' } as any); + const plan = await controller.plan({ type: 'resourceType' }, { propC: 'abc' } as any, null, false); expect(resource.refresh.called).to.be.true; expect(resource.refresh.getCall(0).firstArg['propA']).to.exist; @@ -606,7 +620,7 @@ describe('Resource parameter tests', () => { }); const controller = new ResourceController(resource); - const plan = await controller.plan(null, { type: 'resourceType', propC: 'abc' } as any, true); + const plan = await controller.plan({ type: 'resourceType' }, null, { propC: 'abc' }, true); expect(resource.refresh.called).to.be.true; expect(resource.refresh.getCall(0).firstArg['propA']).to.exist; @@ -654,7 +668,7 @@ describe('Resource parameter tests', () => { }; const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resourceType', propA: '~/test/folder' } as any); + const plan = await controller.plan({ type: 'resourceType' }, { propA: '~/test/folder' } as any, null, false); expect(plan.changeSet.parameterChanges[0]).toMatchObject({ operation: ParameterOperation.NOOP, @@ -682,7 +696,7 @@ describe('Resource parameter tests', () => { }; const controller = new ResourceController(resource); - const plan = await controller.plan({ type: 'resourceType', propA: 'setting', propB: 64 } as any); + const plan = await controller.plan({ type: 'resourceType' }, { propA: 'setting', propB: 64 } as any, null, false); expect(plan.changeSet.parameterChanges).toMatchObject( expect.arrayContaining([ @@ -741,7 +755,7 @@ describe('Resource parameter tests', () => { const controller = new ResourceController(resource); - const result = await controller.plan({ type: 'resourceType', propA: '10.0' }); + const result = await controller.plan({ type: 'resourceType' }, { propA: '10.0' }, null, false); expect(result.changeSet).toMatchObject({ operation: ResourceOperation.NOOP, }) @@ -771,14 +785,18 @@ describe('Resource parameter tests', () => { const controller = new ResourceController(resource); - const result = await controller.plan({ - type: 'resourceType', - propD: { - testC: 10, - testA: 'a', - testB: 'b', - } - }); + const result = await controller.plan( + { type: 'resourceType' }, + { + propD: { + testC: 10, + testA: 'a', + testB: 'b', + } + }, + null, + false + ); expect(result.changeSet).toMatchObject({ operation: ResourceOperation.NOOP, @@ -808,14 +826,18 @@ describe('Resource parameter tests', () => { const controller = new ResourceController(resource); - const result = await controller.plan({ - type: 'resourceType', - propD: { - testC: 10, - testA: 'a', - testB: 'b', - } - }); + const result = await controller.plan( + { type: 'resourceType' }, + { + propD: { + testC: 10, + testA: 'a', + testB: 'b', + } + }, + null, + false + ); expect(result.changeSet).toMatchObject({ operation: ResourceOperation.RECREATE, @@ -856,8 +878,9 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - await controller.plan({ - type: 'resourceType', + await controller.plan( + { type: 'resourceType' }, + { propD: [ { Host: 'new.com', @@ -874,13 +897,16 @@ describe('Resource parameter tests', () => { PasswordAuthentication: true, } ] - }); + }, + null, + false + ); }) it('Transforms input parameters for stateful parameters', async () => { const sp = new class extends TestStatefulParameter { - getSettings(): ResourceSettings { + getSettings(): any { return { type: 'array', inputTransformation: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( @@ -896,7 +922,7 @@ describe('Resource parameter tests', () => { } } - async refresh(desired: any): Promise | null> { + async refresh(desired: any): Promise { expect(desired[0].AddKeysToAgent).to.eq('yes') expect(desired[1].AddKeysToAgent).to.eq('yes') expect(desired[1].UseKeychain).to.eq('yes') @@ -918,25 +944,29 @@ describe('Resource parameter tests', () => { } const controller = new ResourceController(resource); - await controller.plan({ - type: 'resourceType', - propD: [ - { - Host: 'new.com', - AddKeysToAgent: true, - IdentityFile: 'id_ed25519' - }, - { - Host: 'github.com', - AddKeysToAgent: true, - UseKeychain: true, - }, - { - Match: 'User bob,joe,phil', - PasswordAuthentication: true, - } - ] - }); + await controller.plan( + { type: 'resourceType' }, + { + propD: [ + { + Host: 'new.com', + AddKeysToAgent: true, + IdentityFile: 'id_ed25519' + }, + { + Host: 'github.com', + AddKeysToAgent: true, + UseKeychain: true, + }, + { + Match: 'User bob,joe,phil', + PasswordAuthentication: true, + } + ] + }, + null, + false + ); }) }) diff --git a/src/stateful-parameter/stateful-parameter-controller.test.ts b/src/stateful-parameter/stateful-parameter-controller.test.ts index 74c42c2..0958d59 100644 --- a/src/stateful-parameter/stateful-parameter-controller.test.ts +++ b/src/stateful-parameter/stateful-parameter-controller.test.ts @@ -153,9 +153,12 @@ describe('Stateful parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - nodeVersions: ['20.15'], - } as any) + const plan = await controller.plan( + { type: 'type' }, + { nodeVersions: ['20.15'] } as any, + null, + false + ) expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP); }) @@ -188,9 +191,12 @@ describe('Stateful parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - propA: '20.15', - } as any) + const plan = await controller.plan( + { type: 'type' }, + { propA: '20.15', } as any, + null, + false + ) expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP); }) @@ -226,9 +232,12 @@ describe('Stateful parameter tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.plan({ - propA: ['20.15', '20.18'], - } as any) + const plan = await controller.plan( + { type: 'type' }, + { propA: ['20.15', '20.18'] } as any, + null, + false + ) expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP); }) diff --git a/src/utils/test-utils.test.ts b/src/utils/test-utils.test.ts index d53c44c..6b41369 100644 --- a/src/utils/test-utils.test.ts +++ b/src/utils/test-utils.test.ts @@ -15,10 +15,10 @@ export function testPlan(params: { isStateful?: boolean; }) { return Plan.calculate({ - desiredParameters: params.desired ?? null, - currentParametersArray: params.current ?? null, - stateParameters: params.state ?? null, - coreParameters: params.core ?? { type: 'type' }, + desired: params.desired ?? null, + currentArray: params.current ?? null, + state: params.state ?? null, + core: params.core ?? { type: 'type' }, settings: params.settings ? new ParsedResourceSettings(params.settings) : new ParsedResourceSettings({ id: 'type' }), From 3fcdd4f076f51d23a139056083b9b39cb3b5d7fa Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 16 Jan 2025 20:39:45 -0500 Subject: [PATCH 14/15] fix: Changed validation schema to work directly on user import. Fixed tests --- .github/workflows/unit-test-ci.yaml | 1 - package-lock.json | 12 ++++---- package.json | 8 +++-- src/plugin/plugin.test.ts | 45 +++++++++++++++++++++++++++++ src/pty/background-pty.test.ts | 31 ++++++++++---------- src/pty/index.test.ts | 15 ++++------ src/pty/vitest.config.ts | 11 ------- src/resource/resource-controller.ts | 3 +- src/utils/test-utils.test.ts | 6 ++++ vitest.config.ts | 5 ++-- 10 files changed, 88 insertions(+), 49 deletions(-) delete mode 100644 src/pty/vitest.config.ts diff --git a/.github/workflows/unit-test-ci.yaml b/.github/workflows/unit-test-ci.yaml index dedb907..f2c6b34 100644 --- a/.github/workflows/unit-test-ci.yaml +++ b/.github/workflows/unit-test-ci.yaml @@ -15,5 +15,4 @@ jobs: node-version: '20.x' cache: 'npm' - run: npm ci - - run: tsc - run: npm run test diff --git a/package-lock.json b/package-lock.json index e5822ac..df2a910 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "codify-plugin-lib", - "version": "1.0.129", + "version": "1.0.131", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.129", + "version": "1.0.131", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.61", + "codify-schemas": "1.0.63", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "strip-ansi": "^7.1.0", @@ -2301,9 +2301,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.61", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.61.tgz", - "integrity": "sha512-kXzSnISLscW2tRtbGZUhm6nAuuhup0isO9xzhflFy+EsNHzGZBVJj730535h1Ft2jtaiywycQPbOJewgz3/UNA==", + "version": "1.0.63", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.63.tgz", + "integrity": "sha512-0khOFJOK7UPibAw8Dfsf9XDcK1ad5c+YbSBMGdpTv6L4lwkYiuEgJhgoxM3oL7fzywe96Woj1KdLVecwZDyaZQ==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index ff37d0d..86230df 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "codify-plugin-lib", - "version": "1.0.129", + "version": "1.0.131", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", "type": "module", "scripts": { - "test": "vitest" + "test": "vitest", + "posttest": "tsc", + "prepublishOnly": "tsc" }, "keywords": [], "author": "", @@ -14,7 +16,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.61", + "codify-schemas": "1.0.63", "@npmcli/promise-spawn": "^7.0.1", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index 2d53d3e..046839a 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -280,4 +280,49 @@ describe('Plugin tests', () => { await testPlugin.apply({ plan }) expect(resource.refresh.calledOnce).to.be.true; }) + + it('Maintains types for validate', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'type', + schema: { + '$schema': 'http://json-schema.org/draft-07/schema', + '$id': 'https://www.codifycli.com/ssh-config.json', + 'type': 'object', + 'properties': { + 'hosts': { + 'description': 'The host blocks inside of the ~/.ssh/config file. See http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5 ', + 'type': 'array', + 'items': { + 'type': 'object', + 'description': 'The individual host blocks inside of the ~/.ssh/config file', + 'properties': { + 'UseKeychain': { + 'type': 'boolean', + 'description': 'A UseKeychain option was introduced in macOS Sierra allowing users to specify whether they would like for the passphrase to be stored in the keychain' + }, + } + } + } + } + } + } + }; + } + + const plugin = Plugin.create('testPlugin', [resource as any]); + const result = await plugin.validate({ + configs: [{ + core: { type: 'type' }, + parameters: { + hosts: [{ + UseKeychain: true, + }] + } + }] + }) + + console.log(result); + }) }); diff --git a/src/pty/background-pty.test.ts b/src/pty/background-pty.test.ts index 3475d26..6beb683 100644 --- a/src/pty/background-pty.test.ts +++ b/src/pty/background-pty.test.ts @@ -18,21 +18,22 @@ describe('BackgroundPty tests', () => { }); }) - it('Can launch 100 commands in parallel', { timeout: 15000 }, async () => { - const pty = new BackgroundPty(); - - const fn = async () => pty.spawnSafe('ls'); - - const results = await Promise.all( - Array.from({ length: 100 }, (_, i) => i + 1) - .map(() => fn()) - ) - - expect(results.length).to.eq(100); - expect(results.every((r) => r.exitCode === 0)) - - await pty.kill(); - }) + // This test takes forever so going to disable for now. + // it('Can launch 100 commands in parallel', { timeout: 15000 }, async () => { + // const pty = new BackgroundPty(); + // + // const fn = async () => pty.spawnSafe('ls'); + // + // const results = await Promise.all( + // Array.from({ length: 100 }, (_, i) => i + 1) + // .map(() => fn()) + // ) + // + // expect(results.length).to.eq(100); + // expect(results.every((r) => r.exitCode === 0)) + // + // await pty.kill(); + // }) it('Reports back the correct exit code and status', async () => { const pty = new BackgroundPty(); diff --git a/src/pty/index.test.ts b/src/pty/index.test.ts index ae1075f..c09469f 100644 --- a/src/pty/index.test.ts +++ b/src/pty/index.test.ts @@ -25,9 +25,8 @@ describe('General tests for PTYs', () => { const plugin = Plugin.create('test plugin', [testResource]) const plan = await plugin.plan({ - desired: { - type: 'type' - }, + core: { type: 'type' }, + desired: {}, state: undefined, isStateful: false, }) @@ -84,17 +83,15 @@ describe('General tests for PTYs', () => { const plugin = Plugin.create('test plugin', [testResource1, testResource2]); await plugin.plan({ - desired: { - type: 'type1' - }, + core: { type: 'type1' }, + desired: {}, state: undefined, isStateful: false, }) await plugin.plan({ - desired: { - type: 'type2' - }, + core: { type: 'type2' }, + desired: {}, state: undefined, isStateful: false, }) diff --git a/src/pty/vitest.config.ts b/src/pty/vitest.config.ts deleted file mode 100644 index 267992f..0000000 --- a/src/pty/vitest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defaultExclude, defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - pool: 'forks', - fileParallelism: false, - exclude: [ - ...defaultExclude, - ] - }, -}); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 9db2d7b..c9a636c 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -57,12 +57,13 @@ export class ResourceController { core: ResourceConfig, parameters: Partial, ): Promise { + const originalParameters = structuredClone(parameters); await this.applyTransformParameters(parameters); this.addDefaultValues(parameters); if (this.schemaValidator) { // Schema validator uses pre transformation parameters - const isValid = this.schemaValidator(parameters); + const isValid = this.schemaValidator(originalParameters); if (!isValid) { return { diff --git a/src/utils/test-utils.test.ts b/src/utils/test-utils.test.ts index 6b41369..e044b3d 100644 --- a/src/utils/test-utils.test.ts +++ b/src/utils/test-utils.test.ts @@ -5,6 +5,7 @@ import { Resource } from '../resource/resource.js'; import { CreatePlan, DestroyPlan } from '../plan/plan-types.js'; import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js'; import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js'; +import { describe, it } from 'vitest'; export function testPlan(params: { desired?: Partial | null; @@ -85,3 +86,8 @@ export class TestArrayStatefulParameter extends ArrayStatefulParameter { + it('empty', () => { + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index 0316135..ddf1a11 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,10 +2,9 @@ import { defaultExclude, defineConfig } from 'vitest/config'; export default defineConfig({ test: { + pool: 'forks', exclude: [ - ...defaultExclude, - './src/utils/test-utils.test.ts', - './src/pty/*' + ...defaultExclude ] }, }); From 59260aff0c7b1162fc1946c4808eee65f1ba7a0c Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 16 Jan 2025 20:41:45 -0500 Subject: [PATCH 15/15] fix: Switched runner to macOS since we only use zsh for node-pty --- .github/workflows/unit-test-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test-ci.yaml b/.github/workflows/unit-test-ci.yaml index f2c6b34..7a47382 100644 --- a/.github/workflows/unit-test-ci.yaml +++ b/.github/workflows/unit-test-ci.yaml @@ -7,7 +7,7 @@ on: [ push ] jobs: build-and-test: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4