codify-plugin-lib 1.0.182-beta7 → 1.0.182-beta71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/build.js +189 -0
- package/dist/bin/build.js +0 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/messages/handlers.js +10 -2
- package/dist/plan/plan.js +45 -3
- package/dist/plugin/plugin.d.ts +2 -1
- package/dist/plugin/plugin.js +6 -1
- package/dist/pty/background-pty.d.ts +3 -2
- package/dist/pty/background-pty.js +7 -14
- package/dist/pty/index.d.ts +8 -2
- package/dist/pty/seqeuntial-pty.d.ts +3 -2
- package/dist/pty/seqeuntial-pty.js +47 -12
- package/dist/resource/parsed-resource-settings.d.ts +5 -2
- package/dist/resource/parsed-resource-settings.js +16 -2
- package/dist/resource/resource-controller.js +5 -5
- package/dist/resource/resource-settings.d.ts +13 -3
- package/dist/resource/resource-settings.js +2 -2
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/utils/file-utils.d.ts +14 -7
- package/dist/utils/file-utils.js +65 -51
- package/dist/utils/functions.js +2 -2
- package/dist/utils/index.d.ts +21 -1
- package/dist/utils/index.js +160 -0
- package/dist/utils/load-resources.d.ts +1 -0
- package/dist/utils/load-resources.js +46 -0
- package/dist/utils/package-json-utils.d.ts +12 -0
- package/dist/utils/package-json-utils.js +34 -0
- package/package.json +5 -4
- package/rollup.config.js +24 -0
- package/src/index.ts +3 -0
- package/src/messages/handlers.test.ts +23 -0
- package/src/messages/handlers.ts +11 -2
- package/src/plan/plan.test.ts +46 -0
- package/src/plan/plan.ts +65 -4
- package/src/plugin/plugin.test.ts +31 -0
- package/src/plugin/plugin.ts +8 -2
- package/src/pty/background-pty.ts +10 -18
- package/src/pty/index.ts +10 -4
- package/src/pty/seqeuntial-pty.ts +62 -16
- package/src/pty/sequential-pty.test.ts +137 -4
- package/src/resource/parsed-resource-settings.test.ts +24 -0
- package/src/resource/parsed-resource-settings.ts +26 -8
- package/src/resource/resource-controller.test.ts +126 -0
- package/src/resource/resource-controller.ts +5 -6
- package/src/resource/resource-settings.test.ts +36 -0
- package/src/resource/resource-settings.ts +17 -5
- package/src/utils/file-utils.test.ts +7 -0
- package/src/utils/file-utils.ts +70 -55
- package/src/utils/functions.ts +3 -3
- package/src/utils/index.ts +197 -1
- package/src/utils/internal-utils.test.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.182-
|
|
3
|
+
"version": "1.0.182-beta71",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"prepublishOnly": "tsc"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"codify-
|
|
14
|
+
"codify-build": "./bin/build.js"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [],
|
|
17
17
|
"author": "",
|
|
@@ -22,11 +22,12 @@
|
|
|
22
22
|
"ajv": "^8.12.0",
|
|
23
23
|
"ajv-formats": "^2.1.1",
|
|
24
24
|
"clean-deep": "^3.4.0",
|
|
25
|
-
"codify-schemas": "1.0.86",
|
|
25
|
+
"codify-schemas": "1.0.86-beta11",
|
|
26
26
|
"lodash.isequal": "^4.5.0",
|
|
27
27
|
"nanoid": "^5.0.9",
|
|
28
28
|
"strip-ansi": "^7.1.0",
|
|
29
|
-
"uuid": "^10.0.0"
|
|
29
|
+
"uuid": "^10.0.0",
|
|
30
|
+
"zod": "4.1.13"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@apidevtools/json-schema-ref-parser": "^11.7.2",
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
2
|
+
import json from '@rollup/plugin-json';
|
|
3
|
+
import nodeResolve from '@rollup/plugin-node-resolve';
|
|
4
|
+
import terser from '@rollup/plugin-terser';
|
|
5
|
+
import typescript from '@rollup/plugin-typescript';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
input: 'src/index.ts',
|
|
9
|
+
output: {
|
|
10
|
+
dir: 'dist',
|
|
11
|
+
format: 'cjs',
|
|
12
|
+
inlineDynamicImports: true,
|
|
13
|
+
},
|
|
14
|
+
external: ['@homebridge/node-pty-prebuilt-multiarch'],
|
|
15
|
+
plugins: [
|
|
16
|
+
json(),
|
|
17
|
+
nodeResolve({ exportConditions: ['node'] }),
|
|
18
|
+
typescript({
|
|
19
|
+
exclude: ['**/*.test.ts', '**/*.d.ts', 'test']
|
|
20
|
+
}),
|
|
21
|
+
commonjs(),
|
|
22
|
+
terser()
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,9 @@ export * from './plan/change-set.js'
|
|
|
7
7
|
export * from './plan/plan.js'
|
|
8
8
|
export * from './plan/plan-types.js'
|
|
9
9
|
export * from './plugin/plugin.js'
|
|
10
|
+
export * from './pty/background-pty.js';
|
|
10
11
|
export * from './pty/index.js'
|
|
12
|
+
export * from './pty/seqeuntial-pty.js';
|
|
11
13
|
export * from './resource/parsed-resource-settings.js';
|
|
12
14
|
export * from './resource/resource.js'
|
|
13
15
|
export * from './resource/resource-settings.js'
|
|
@@ -16,6 +18,7 @@ export * from './utils/file-utils.js'
|
|
|
16
18
|
export * from './utils/functions.js'
|
|
17
19
|
export * from './utils/index.js'
|
|
18
20
|
export * from './utils/verbosity-level.js'
|
|
21
|
+
export * from 'zod/v4';
|
|
19
22
|
|
|
20
23
|
export async function runPlugin(plugin: Plugin) {
|
|
21
24
|
const messageHandler = new MessageHandler(plugin);
|
|
@@ -235,6 +235,29 @@ describe('Message handler tests', () => {
|
|
|
235
235
|
process.send = undefined;
|
|
236
236
|
})
|
|
237
237
|
|
|
238
|
+
it('handles changing the verbosity level', async () => {
|
|
239
|
+
const resource = new TestResource()
|
|
240
|
+
const plugin = testPlugin(resource);
|
|
241
|
+
const handler = new MessageHandler(plugin);
|
|
242
|
+
|
|
243
|
+
process.send = (message) => {
|
|
244
|
+
expect(message).toMatchObject({
|
|
245
|
+
cmd: 'setVerbosityLevel_Response',
|
|
246
|
+
status: MessageStatus.SUCCESS,
|
|
247
|
+
})
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
expect(async () => await handler.onMessage({
|
|
252
|
+
cmd: 'setVerbosityLevel',
|
|
253
|
+
data: {
|
|
254
|
+
verbosityLevel: 2,
|
|
255
|
+
}
|
|
256
|
+
})).rejects.to.not.throw;
|
|
257
|
+
|
|
258
|
+
process.send = undefined;
|
|
259
|
+
})
|
|
260
|
+
|
|
238
261
|
it('Supports ipc message v2 (success)', async () => {
|
|
239
262
|
const resource = new TestResource()
|
|
240
263
|
const plugin = testPlugin(resource);
|
package/src/messages/handlers.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Ajv, SchemaObject, ValidateFunction } from 'ajv';
|
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
import {
|
|
4
4
|
ApplyRequestDataSchema,
|
|
5
|
-
|
|
5
|
+
EmptyResponseDataSchema,
|
|
6
6
|
GetResourceInfoRequestDataSchema,
|
|
7
7
|
GetResourceInfoResponseDataSchema,
|
|
8
8
|
ImportRequestDataSchema,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
PlanRequestDataSchema,
|
|
20
20
|
PlanResponseDataSchema,
|
|
21
21
|
ResourceSchema,
|
|
22
|
+
SetVerbosityRequestDataSchema,
|
|
22
23
|
ValidateRequestDataSchema,
|
|
23
24
|
ValidateResponseDataSchema
|
|
24
25
|
} from 'codify-schemas';
|
|
@@ -42,6 +43,14 @@ const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) =
|
|
|
42
43
|
requestValidator: GetResourceInfoRequestDataSchema,
|
|
43
44
|
responseValidator: GetResourceInfoResponseDataSchema
|
|
44
45
|
},
|
|
46
|
+
'setVerbosityLevel': {
|
|
47
|
+
async handler(plugin: Plugin, data: any) {
|
|
48
|
+
await plugin.setVerbosityLevel(data)
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
requestValidator: SetVerbosityRequestDataSchema,
|
|
52
|
+
responseValidator: EmptyResponseDataSchema,
|
|
53
|
+
},
|
|
45
54
|
'match': {
|
|
46
55
|
handler: async (plugin: Plugin, data: any) => plugin.match(data),
|
|
47
56
|
requestValidator: MatchRequestDataSchema,
|
|
@@ -63,7 +72,7 @@ const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) =
|
|
|
63
72
|
return null;
|
|
64
73
|
},
|
|
65
74
|
requestValidator: ApplyRequestDataSchema,
|
|
66
|
-
responseValidator:
|
|
75
|
+
responseValidator: EmptyResponseDataSchema
|
|
67
76
|
},
|
|
68
77
|
}
|
|
69
78
|
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -180,6 +180,52 @@ describe('Plan entity tests', () => {
|
|
|
180
180
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
181
181
|
})
|
|
182
182
|
|
|
183
|
+
it('Filters array parameters in delete mode (when desired is null)', async () => {
|
|
184
|
+
const resource = new class extends TestResource {
|
|
185
|
+
getSettings(): ResourceSettings<any> {
|
|
186
|
+
return {
|
|
187
|
+
id: 'type',
|
|
188
|
+
operatingSystems: [OS.Darwin],
|
|
189
|
+
parameterSettings: {
|
|
190
|
+
propZ: { type: 'array', isElementEqual: (a, b) => b.includes(a) }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
196
|
+
return {
|
|
197
|
+
propZ: [
|
|
198
|
+
'20.15.0',
|
|
199
|
+
'20.15.1'
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const controller = new ResourceController(resource);
|
|
206
|
+
const plan = await controller.plan(
|
|
207
|
+
{ type: 'type' },
|
|
208
|
+
null,
|
|
209
|
+
{ propZ: ['20.15.0'], } as any,
|
|
210
|
+
true
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
214
|
+
expect(plan).toMatchObject({
|
|
215
|
+
id: expect.any(String),
|
|
216
|
+
changeSet: expect.objectContaining({
|
|
217
|
+
operation: ResourceOperation.DESTROY,
|
|
218
|
+
parameterChanges: [
|
|
219
|
+
expect.objectContaining({ operation: 'remove', name: 'propZ', previousValue: ['20.15.0'], newValue: null }),
|
|
220
|
+
],
|
|
221
|
+
}),
|
|
222
|
+
coreParameters: expect.objectContaining({
|
|
223
|
+
type: 'type',
|
|
224
|
+
}),
|
|
225
|
+
isStateful: true,
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
183
229
|
it('Doesn\'t filters array parameters if filtering is disabled', async () => {
|
|
184
230
|
const resource = new class extends TestResource {
|
|
185
231
|
getSettings(): ResourceSettings<any> {
|
package/src/plan/plan.ts
CHANGED
|
@@ -307,6 +307,8 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
307
307
|
settings: ResourceSettings<T>,
|
|
308
308
|
isStateful: boolean,
|
|
309
309
|
}): Partial<T> | null {
|
|
310
|
+
console.log('Filter current params', params.desired, params.current, params.state, params.settings, params.isStateful)
|
|
311
|
+
|
|
310
312
|
const {
|
|
311
313
|
desired,
|
|
312
314
|
current,
|
|
@@ -324,19 +326,37 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
324
326
|
return null
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
console.log('Before exit', isStateful, desired);
|
|
330
|
+
|
|
327
331
|
// For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
|
|
328
332
|
// requires additional filtering for stateful parameter arrays and objects.
|
|
329
|
-
if (isStateful) {
|
|
333
|
+
if (isStateful && desired) {
|
|
330
334
|
return filteredCurrent;
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
// We also want to filter parameters when in delete mode. We don't want to delete parameters that
|
|
338
|
+
// are not specified in the original config.
|
|
339
|
+
if (isStateful && !desired) {
|
|
340
|
+
const arrayStatefulParameters = Object.fromEntries(
|
|
341
|
+
Object.entries(filteredCurrent)
|
|
342
|
+
.filter(([k, v]) => isArrayParameterWithFiltering(k, v))
|
|
343
|
+
.map(([k, v]) => [k, filterArrayParameterForDeletes(k, v)])
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return { ...filteredCurrent, ...arrayStatefulParameters }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log('Post exit', isStateful, desired);
|
|
350
|
+
|
|
333
351
|
// TODO: Add object handling here in addition to arrays in the future
|
|
334
352
|
const arrayStatefulParameters = Object.fromEntries(
|
|
335
353
|
Object.entries(filteredCurrent)
|
|
336
354
|
.filter(([k, v]) => isArrayParameterWithFiltering(k, v))
|
|
337
|
-
.map(([k, v]) => [k,
|
|
355
|
+
.map(([k, v]) => [k, filterArrayParameterForStatelessMode(k, v)])
|
|
338
356
|
)
|
|
339
357
|
|
|
358
|
+
console.log('Result', { ...filteredCurrent, ...arrayStatefulParameters });
|
|
359
|
+
|
|
340
360
|
return { ...filteredCurrent, ...arrayStatefulParameters }
|
|
341
361
|
|
|
342
362
|
function filterCurrent(): Partial<T> | null {
|
|
@@ -378,7 +398,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
378
398
|
|
|
379
399
|
function isArrayParameterWithFiltering(k: string, v: T[keyof T]): boolean {
|
|
380
400
|
const filterParameter = getFilterParameter(k);
|
|
381
|
-
|
|
401
|
+
|
|
382
402
|
if (settings.parameterSettings?.[k]?.type === 'stateful') {
|
|
383
403
|
const statefulSetting = settings.parameterSettings[k] as ParsedStatefulParameterSetting;
|
|
384
404
|
return statefulSetting.nestedSettings.type === 'array' &&
|
|
@@ -392,7 +412,9 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
392
412
|
}
|
|
393
413
|
|
|
394
414
|
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
395
|
-
function
|
|
415
|
+
function filterArrayParameterForStatelessMode(k: string, v: unknown[]): unknown[] {
|
|
416
|
+
console.log('Attempting to filter (key, value)', k, v);
|
|
417
|
+
|
|
396
418
|
const desiredArray = desired![k] as unknown[];
|
|
397
419
|
const matcher = settings.parameterSettings![k]!.type === 'stateful'
|
|
398
420
|
? ((settings.parameterSettings![k] as ParsedStatefulParameterSetting)
|
|
@@ -427,6 +449,45 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
427
449
|
? filterParameter(desiredCopy, currentCopy)
|
|
428
450
|
: defaultFilterMethod(desiredCopy, currentCopy);
|
|
429
451
|
}
|
|
452
|
+
|
|
453
|
+
function filterArrayParameterForDeletes(k: string, v: unknown[]): unknown[] {
|
|
454
|
+
console.log('Attempting to filter (key, value)', k, v);
|
|
455
|
+
|
|
456
|
+
const stateArray = state![k] as unknown[];
|
|
457
|
+
const matcher = settings.parameterSettings![k]!.type === 'stateful'
|
|
458
|
+
? ((settings.parameterSettings![k] as ParsedStatefulParameterSetting)
|
|
459
|
+
.nestedSettings as ParsedArrayParameterSetting)
|
|
460
|
+
.isElementEqual
|
|
461
|
+
: (settings.parameterSettings![k] as ParsedArrayParameterSetting)
|
|
462
|
+
.isElementEqual
|
|
463
|
+
|
|
464
|
+
const stateCopy = [...stateArray];
|
|
465
|
+
const currentCopy = [...v];
|
|
466
|
+
|
|
467
|
+
const defaultFilterMethod = ((state: any[], current: any[]) => {
|
|
468
|
+
const result = [];
|
|
469
|
+
|
|
470
|
+
for (let counter = state.length - 1; counter >= 0; counter--) {
|
|
471
|
+
const idx = currentCopy.findIndex((e2) => matcher(state[counter], e2))
|
|
472
|
+
|
|
473
|
+
if (idx === -1) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
state.splice(counter, 1)
|
|
478
|
+
const [element] = current.splice(idx, 1)
|
|
479
|
+
result.push(element)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return result;
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
const filterParameter = getFilterParameter(k);
|
|
487
|
+
return typeof filterParameter === 'function'
|
|
488
|
+
? filterParameter(stateCopy, currentCopy)
|
|
489
|
+
: defaultFilterMethod(stateCopy, currentCopy);
|
|
490
|
+
}
|
|
430
491
|
}
|
|
431
492
|
|
|
432
493
|
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
@@ -7,6 +7,7 @@ import { spy } from 'sinon';
|
|
|
7
7
|
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
8
8
|
import { TestConfig, TestStatefulParameter } from '../utils/test-utils.test.js';
|
|
9
9
|
import { getPty } from '../pty/index.js';
|
|
10
|
+
import { z } from 'zod';
|
|
10
11
|
|
|
11
12
|
interface TestConfig extends StringIndexedObject {
|
|
12
13
|
propA: string;
|
|
@@ -170,6 +171,36 @@ describe('Plugin tests', () => {
|
|
|
170
171
|
})
|
|
171
172
|
})
|
|
172
173
|
|
|
174
|
+
it('Can get resource info (zod schema)', async () => {
|
|
175
|
+
const schema = z
|
|
176
|
+
.object({
|
|
177
|
+
plugins: z
|
|
178
|
+
.array(z.string())
|
|
179
|
+
.describe(
|
|
180
|
+
'Asdf plugins to install. See: https://github.com/asdf-community for a full list'
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
.strict()
|
|
184
|
+
|
|
185
|
+
const resource = new class extends TestResource {
|
|
186
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
187
|
+
return {
|
|
188
|
+
id: 'typeId',
|
|
189
|
+
operatingSystems: [OS.Darwin],
|
|
190
|
+
schema,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const testPlugin = Plugin.create('testPlugin', [resource as any])
|
|
195
|
+
|
|
196
|
+
const resourceInfo = await testPlugin.getResourceInfo({ type: 'typeId' })
|
|
197
|
+
expect(resourceInfo.import).toMatchObject({
|
|
198
|
+
requiredParameters: [
|
|
199
|
+
'plugins'
|
|
200
|
+
]
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
173
204
|
it('Get resource info to default import to the one specified in the resource settings', async () => {
|
|
174
205
|
const schema = {
|
|
175
206
|
'$schema': 'http://json-schema.org/draft-07/schema',
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
PlanRequestData,
|
|
13
13
|
PlanResponseData,
|
|
14
14
|
ResourceConfig,
|
|
15
|
-
ResourceJson,
|
|
15
|
+
ResourceJson, SetVerbosityRequestData,
|
|
16
16
|
ValidateRequestData,
|
|
17
17
|
ValidateResponseData
|
|
18
18
|
} from 'codify-schemas';
|
|
@@ -77,6 +77,7 @@ export class Plugin {
|
|
|
77
77
|
type: r.typeId,
|
|
78
78
|
sensitiveParameters,
|
|
79
79
|
operatingSystems: r.settings.operatingSystems,
|
|
80
|
+
linuxDistros: r.settings.linuxDistros,
|
|
80
81
|
}
|
|
81
82
|
})
|
|
82
83
|
}
|
|
@@ -89,7 +90,7 @@ export class Plugin {
|
|
|
89
90
|
|
|
90
91
|
const resource = this.resourceControllers.get(data.type)!;
|
|
91
92
|
|
|
92
|
-
const schema = resource.
|
|
93
|
+
const schema = resource.parsedSettings.schema as JSONSchemaType<any> | undefined;
|
|
93
94
|
const requiredPropertyNames = (
|
|
94
95
|
resource.settings.importAndDestroy?.requiredParameters
|
|
95
96
|
?? (typeof resource.settings.allowMultiple === 'object' ? resource.settings.allowMultiple.identifyingParameters : null)
|
|
@@ -124,6 +125,7 @@ export class Plugin {
|
|
|
124
125
|
requiredParameters: requiredPropertyNames,
|
|
125
126
|
},
|
|
126
127
|
operatingSystems: resource.settings.operatingSystems,
|
|
128
|
+
linuxDistros: resource.settings.linuxDistros,
|
|
127
129
|
sensitiveParameters,
|
|
128
130
|
allowMultiple
|
|
129
131
|
}
|
|
@@ -257,6 +259,10 @@ export class Plugin {
|
|
|
257
259
|
}
|
|
258
260
|
}
|
|
259
261
|
|
|
262
|
+
async setVerbosityLevel(data: SetVerbosityRequestData): Promise<void> {
|
|
263
|
+
VerbosityLevel.set(data.verbosityLevel);
|
|
264
|
+
}
|
|
265
|
+
|
|
260
266
|
async kill() {
|
|
261
267
|
await this.planPty.kill();
|
|
262
268
|
}
|
|
@@ -20,8 +20,11 @@ EventEmitter.defaultMaxListeners = 1000;
|
|
|
20
20
|
* without a tty (or even a stdin) attached so interactive commands will not work.
|
|
21
21
|
*/
|
|
22
22
|
export class BackgroundPty implements IPty {
|
|
23
|
+
private historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
|
|
23
24
|
private basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
|
|
24
|
-
env: process.env,
|
|
25
|
+
env: { ...process.env, ...this.historyIgnore },
|
|
26
|
+
cols: 10_000, // Set to a really large value to prevent wrapping
|
|
27
|
+
name: nanoid(6),
|
|
25
28
|
handleFlowControl: true
|
|
26
29
|
});
|
|
27
30
|
|
|
@@ -31,17 +34,19 @@ export class BackgroundPty implements IPty {
|
|
|
31
34
|
this.initialize();
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
async spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
|
|
37
|
+
async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
|
|
35
38
|
const spawnResult = await this.spawnSafe(cmd, options);
|
|
36
39
|
|
|
37
40
|
if (spawnResult.status !== 'success') {
|
|
38
|
-
throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
|
|
41
|
+
throw new SpawnError(Array.isArray(cmd) ? cmd.join(' ') : cmd, spawnResult.exitCode, spawnResult.data);
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
return spawnResult;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
async spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
|
|
47
|
+
async spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
|
|
48
|
+
cmd = Array.isArray(cmd) ? cmd.join('\\\n') : cmd;
|
|
49
|
+
|
|
45
50
|
// cid is command id
|
|
46
51
|
const cid = nanoid(10);
|
|
47
52
|
debugLog(cid);
|
|
@@ -102,7 +107,7 @@ export class BackgroundPty implements IPty {
|
|
|
102
107
|
}
|
|
103
108
|
});
|
|
104
109
|
|
|
105
|
-
console.log(`Running command ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
|
|
110
|
+
console.log(`Running command: ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
|
|
106
111
|
this.basePty.write(`${command}\r`);
|
|
107
112
|
|
|
108
113
|
}));
|
|
@@ -128,19 +133,6 @@ export class BackgroundPty implements IPty {
|
|
|
128
133
|
let outputBuffer = '';
|
|
129
134
|
|
|
130
135
|
return new Promise(resolve => {
|
|
131
|
-
// zsh-specific commands
|
|
132
|
-
switch (Utils.getShell()) {
|
|
133
|
-
case Shell.ZSH: {
|
|
134
|
-
this.basePty.write('setopt HIST_NO_STORE;\n');
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
default: {
|
|
139
|
-
this.basePty.write('export HISTIGNORE=\'history*\';\n');
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
136
|
this.basePty.write(' unset PS1;\n');
|
|
145
137
|
this.basePty.write(' unset PS0;\n')
|
|
146
138
|
this.basePty.write(' echo setup complete\\"\n')
|
package/src/pty/index.ts
CHANGED
|
@@ -25,11 +25,17 @@ export enum SpawnStatus {
|
|
|
25
25
|
*
|
|
26
26
|
* @property {boolean} [interactive] - Indicates whether the spawned process needs
|
|
27
27
|
* to be interactive. Only works within apply (not plan). Defaults to true.
|
|
28
|
+
*
|
|
29
|
+
* @property {boolean} [disableWrapping] - Forces the terminal width to 10_000 to disable wrapping.
|
|
30
|
+
* In applys, this is off by default while it is on during plans.
|
|
28
31
|
*/
|
|
29
32
|
export interface SpawnOptions {
|
|
30
33
|
cwd?: string;
|
|
31
|
-
env?: Record<string, unknown
|
|
32
|
-
interactive?: boolean
|
|
34
|
+
env?: Record<string, unknown>;
|
|
35
|
+
interactive?: boolean;
|
|
36
|
+
requiresRoot?: boolean;
|
|
37
|
+
stdin?: boolean;
|
|
38
|
+
disableWrapping?: boolean;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export class SpawnError extends Error {
|
|
@@ -48,9 +54,9 @@ export class SpawnError extends Error {
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
export interface IPty {
|
|
51
|
-
spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult>
|
|
57
|
+
spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
|
|
52
58
|
|
|
53
|
-
spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult>
|
|
59
|
+
spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
|
|
54
60
|
|
|
55
61
|
kill(): Promise<{ exitCode: number, signal?: number | undefined }>
|
|
56
62
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import pty from '@homebridge/node-pty-prebuilt-multiarch';
|
|
2
|
+
import { Ajv } from 'ajv';
|
|
3
|
+
import { CommandRequestResponseData, CommandRequestResponseDataSchema, IpcMessageV2, MessageCmd } from 'codify-schemas';
|
|
4
|
+
import { nanoid } from 'nanoid';
|
|
2
5
|
import { EventEmitter } from 'node:events';
|
|
3
6
|
import stripAnsi from 'strip-ansi';
|
|
4
7
|
|
|
@@ -8,6 +11,11 @@ import { IPty, SpawnError, SpawnOptions, SpawnResult, SpawnStatus } from './inde
|
|
|
8
11
|
|
|
9
12
|
EventEmitter.defaultMaxListeners = 1000;
|
|
10
13
|
|
|
14
|
+
const ajv = new Ajv({
|
|
15
|
+
strict: true,
|
|
16
|
+
});
|
|
17
|
+
const validateSudoRequestResponse = ajv.compile(CommandRequestResponseDataSchema);
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* The background pty is a specialized pty designed for speed. It can launch multiple tasks
|
|
13
21
|
* in parallel by moving them to the background. It attaches unix FIFO pipes to each process
|
|
@@ -15,22 +23,32 @@ EventEmitter.defaultMaxListeners = 1000;
|
|
|
15
23
|
* without a tty (or even a stdin) attached so interactive commands will not work.
|
|
16
24
|
*/
|
|
17
25
|
export class SequentialPty implements IPty {
|
|
18
|
-
async spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
|
|
26
|
+
async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
|
|
19
27
|
const spawnResult = await this.spawnSafe(cmd, options);
|
|
20
28
|
|
|
21
29
|
if (spawnResult.status !== 'success') {
|
|
22
|
-
throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
|
|
30
|
+
throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
return spawnResult;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
async spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
|
|
29
|
-
|
|
36
|
+
async spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
|
|
37
|
+
cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd;
|
|
38
|
+
|
|
39
|
+
if (cmd.includes('sudo')) {
|
|
40
|
+
throw new Error('Do not directly use sudo. Use the option { requiresRoot: true } instead')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// If sudo is required, we must delegate to the main codify process.
|
|
44
|
+
if (options?.stdin || options?.requiresRoot) {
|
|
45
|
+
return this.externalSpawn(cmd, options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
|
|
30
49
|
|
|
31
50
|
return new Promise((resolve) => {
|
|
32
51
|
const output: string[] = [];
|
|
33
|
-
|
|
34
52
|
const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
|
|
35
53
|
|
|
36
54
|
// If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
|
|
@@ -39,14 +57,16 @@ export class SequentialPty implements IPty {
|
|
|
39
57
|
...process.env, ...options?.env,
|
|
40
58
|
TERM_PROGRAM: 'codify',
|
|
41
59
|
COMMAND_MODE: 'unix2003',
|
|
42
|
-
COLORTERM: 'truecolor',
|
|
60
|
+
COLORTERM: 'truecolor',
|
|
61
|
+
...historyIgnore
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
// Initial terminal dimensions
|
|
46
|
-
|
|
65
|
+
// Set to a really large value to prevent wrapping
|
|
66
|
+
const initialCols = options?.disableWrapping ? 10_000 : process.stdout.columns ?? 80
|
|
47
67
|
const initialRows = process.stdout.rows ?? 24;
|
|
48
68
|
|
|
49
|
-
const args =
|
|
69
|
+
const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd]
|
|
50
70
|
|
|
51
71
|
// Run the command in a pty for interactivity
|
|
52
72
|
const mPty = pty.spawn(this.getDefaultShell(), args, {
|
|
@@ -64,23 +84,16 @@ export class SequentialPty implements IPty {
|
|
|
64
84
|
output.push(data.toString());
|
|
65
85
|
})
|
|
66
86
|
|
|
67
|
-
const stdinListener = (data: any) => {
|
|
68
|
-
mPty.write(data.toString());
|
|
69
|
-
};
|
|
70
|
-
|
|
71
87
|
const resizeListener = () => {
|
|
72
88
|
const { columns, rows } = process.stdout;
|
|
73
|
-
mPty.resize(columns, rows);
|
|
89
|
+
mPty.resize(columns, options?.disableWrapping ? 10_000 : rows);
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
// Listen to resize events for the terminal window;
|
|
77
93
|
process.stdout.on('resize', resizeListener);
|
|
78
|
-
// Listen for user input
|
|
79
|
-
process.stdin.on('data', stdinListener);
|
|
80
94
|
|
|
81
95
|
mPty.onExit((result) => {
|
|
82
96
|
process.stdout.off('resize', resizeListener);
|
|
83
|
-
process.stdin.off('data', stdinListener);
|
|
84
97
|
|
|
85
98
|
resolve({
|
|
86
99
|
status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
@@ -99,6 +112,39 @@ export class SequentialPty implements IPty {
|
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
// For safety reasons, requests that require sudo or are interactive must be run via the main client
|
|
116
|
+
async externalSpawn(
|
|
117
|
+
cmd: string,
|
|
118
|
+
opts: SpawnOptions
|
|
119
|
+
): Promise<SpawnResult> {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const requestId = nanoid(8);
|
|
122
|
+
|
|
123
|
+
const listener = (data: IpcMessageV2) => {
|
|
124
|
+
if (data.requestId === requestId) {
|
|
125
|
+
process.removeListener('message', listener);
|
|
126
|
+
|
|
127
|
+
if (!validateSudoRequestResponse(data.data)) {
|
|
128
|
+
throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
resolve(data.data as unknown as CommandRequestResponseData);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.on('message', listener);
|
|
136
|
+
|
|
137
|
+
process.send!(<IpcMessageV2>{
|
|
138
|
+
cmd: MessageCmd.COMMAND_REQUEST,
|
|
139
|
+
data: {
|
|
140
|
+
command: cmd,
|
|
141
|
+
options: opts ?? {},
|
|
142
|
+
},
|
|
143
|
+
requestId
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
private getDefaultShell(): string {
|
|
103
149
|
return process.env.SHELL!;
|
|
104
150
|
}
|