codify-plugin-lib 1.0.182-beta7 → 1.0.182-beta70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/bin/build.js +189 -0
  2. package/dist/bin/build.js +0 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +3 -0
  5. package/dist/messages/handlers.js +10 -2
  6. package/dist/plan/plan.js +7 -1
  7. package/dist/plugin/plugin.d.ts +2 -1
  8. package/dist/plugin/plugin.js +6 -1
  9. package/dist/pty/background-pty.d.ts +3 -2
  10. package/dist/pty/background-pty.js +7 -14
  11. package/dist/pty/index.d.ts +8 -2
  12. package/dist/pty/seqeuntial-pty.d.ts +3 -2
  13. package/dist/pty/seqeuntial-pty.js +47 -12
  14. package/dist/resource/parsed-resource-settings.d.ts +5 -2
  15. package/dist/resource/parsed-resource-settings.js +16 -2
  16. package/dist/resource/resource-controller.js +5 -5
  17. package/dist/resource/resource-settings.d.ts +13 -3
  18. package/dist/resource/resource-settings.js +2 -2
  19. package/dist/test.d.ts +1 -0
  20. package/dist/test.js +5 -0
  21. package/dist/utils/file-utils.d.ts +14 -7
  22. package/dist/utils/file-utils.js +65 -51
  23. package/dist/utils/functions.js +2 -2
  24. package/dist/utils/index.d.ts +21 -1
  25. package/dist/utils/index.js +160 -0
  26. package/dist/utils/load-resources.d.ts +1 -0
  27. package/dist/utils/load-resources.js +46 -0
  28. package/dist/utils/package-json-utils.d.ts +12 -0
  29. package/dist/utils/package-json-utils.js +34 -0
  30. package/package.json +5 -4
  31. package/rollup.config.js +24 -0
  32. package/src/index.ts +3 -0
  33. package/src/messages/handlers.test.ts +23 -0
  34. package/src/messages/handlers.ts +11 -2
  35. package/src/plan/plan.ts +11 -1
  36. package/src/plugin/plugin.test.ts +31 -0
  37. package/src/plugin/plugin.ts +8 -2
  38. package/src/pty/background-pty.ts +10 -18
  39. package/src/pty/index.ts +10 -4
  40. package/src/pty/seqeuntial-pty.ts +62 -16
  41. package/src/pty/sequential-pty.test.ts +137 -4
  42. package/src/resource/parsed-resource-settings.test.ts +24 -0
  43. package/src/resource/parsed-resource-settings.ts +26 -8
  44. package/src/resource/resource-controller.test.ts +126 -0
  45. package/src/resource/resource-controller.ts +5 -6
  46. package/src/resource/resource-settings.test.ts +36 -0
  47. package/src/resource/resource-settings.ts +17 -5
  48. package/src/utils/file-utils.test.ts +7 -0
  49. package/src/utils/file-utils.ts +70 -55
  50. package/src/utils/functions.ts +3 -3
  51. package/src/utils/index.ts +197 -1
  52. package/src/utils/internal-utils.test.ts +1 -0
package/bin/build.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ import { Ajv } from 'ajv';
3
+ import { IpcMessageSchema, MessageStatus, ResourceSchema } from 'codify-schemas';
4
+ import mergeJsonSchemas from 'merge-json-schemas';
5
+ import { fork } from 'node:child_process';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ import { SequentialPty, VerbosityLevel } from '../dist/index.js';
10
+
11
+ const ajv = new Ajv({
12
+ strict: true
13
+ });
14
+ const ipcMessageValidator = ajv.compile(IpcMessageSchema);
15
+
16
+ function sendMessageAndAwaitResponse(process, message) {
17
+ return new Promise((resolve, reject) => {
18
+ process.on('message', (response) => {
19
+ if (!ipcMessageValidator(response)) {
20
+ throw new Error(`Invalid message from plugin. ${JSON.stringify(message, null, 2)}`);
21
+ }
22
+
23
+ // Wait for the message response. Other messages such as sudoRequest may be sent before the response returns
24
+ if (response.cmd === message.cmd + '_Response') {
25
+ if (response.status === MessageStatus.SUCCESS) {
26
+ resolve(response.data)
27
+ } else {
28
+ reject(new Error(String(response.data)))
29
+ }
30
+ }
31
+ });
32
+
33
+ // Send message last to ensure listeners are all registered
34
+ process.send(message);
35
+ });
36
+ }
37
+
38
+ function fetchDocumentationMaps() {
39
+ console.log('Building documentation...');
40
+
41
+ const results = new Map();
42
+ const resourcesPath = path.resolve(process.cwd(), 'src', 'resources');
43
+ const resourcesDir = fs.readdirSync(resourcesPath);
44
+
45
+ for (const resource of resourcesDir) {
46
+ const resourcePath = path.join(resourcesPath, resource);
47
+ if (!isDirectory(resourcePath)) continue;
48
+
49
+ const contents = fs.readdirSync(resourcePath);
50
+ const isGroup = contents.some((content) => isDirectory(path.join(resourcePath, content)));
51
+ const isAllDir = contents.every((content) => isDirectory(path.join(resourcePath, content)));
52
+
53
+ if (isGroup && !isAllDir) {
54
+ throw new Error(`Documentation groups must only contain directories. ${resourcePath} does not`);
55
+ }
56
+
57
+ if (!isGroup) {
58
+ if (contents.includes('README.md')) {
59
+ results.set(resource, resource);
60
+ }
61
+ } else {
62
+ for (const innerDir of contents) {
63
+ const innerDirReadme = path.join(resourcePath, innerDir, 'README.md');
64
+ if (isFile(innerDirReadme)) {
65
+ results.set(innerDir, path.relative('./src/resources', path.join(resourcePath, innerDir)));
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ return results;
72
+ }
73
+
74
+ function isDirectory(path) {
75
+ try {
76
+ return fs.statSync(path).isDirectory();
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ function isFile(path) {
83
+ try {
84
+ return fs.statSync(path).isFile();
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ VerbosityLevel.set(3);
91
+ const $ = new SequentialPty();
92
+
93
+ await $.spawn('rm -rf ./dist')
94
+ await $.spawn('npm run rollup -- -f es', { interactive: true });
95
+
96
+ const plugin = fork(
97
+ './dist/index.js',
98
+ [],
99
+ {
100
+ // Use default true to test plugins in secure mode (un-able to request sudo directly)
101
+ detached: true,
102
+ env: { ...process.env },
103
+ execArgv: ['--import', 'tsx/esm'],
104
+ },
105
+ )
106
+
107
+ const initializeResult = await sendMessageAndAwaitResponse(plugin, {
108
+ cmd: 'initialize',
109
+ data: {}
110
+ })
111
+
112
+ const { resourceDefinitions } = initializeResult;
113
+ const resourceTypes = resourceDefinitions.map((i) => i.type);
114
+ const resourceInfoMap = new Map();
115
+
116
+ const schemasMap = new Map()
117
+ for (const type of resourceTypes) {
118
+ const resourceInfo = await sendMessageAndAwaitResponse(plugin, {
119
+ cmd: 'getResourceInfo',
120
+ data: { type }
121
+ })
122
+
123
+ schemasMap.set(type, resourceInfo.schema);
124
+ resourceInfoMap.set(type, resourceInfo);
125
+ }
126
+
127
+ console.log(resourceInfoMap);
128
+
129
+ const mergedSchemas = [...schemasMap.entries()].map(([type, schema]) => {
130
+ // const resolvedSchema = await $RefParser.dereference(schema)
131
+ const resourceSchema = JSON.parse(JSON.stringify(ResourceSchema));
132
+
133
+ delete resourceSchema.$id;
134
+ delete resourceSchema.$schema;
135
+ delete resourceSchema.title;
136
+ delete resourceSchema.oneOf;
137
+ delete resourceSchema.properties.type;
138
+
139
+ if (schema) {
140
+ delete schema.$id;
141
+ delete schema.$schema;
142
+ delete schema.title;
143
+ delete schema.oneOf;
144
+ }
145
+
146
+ return mergeJsonSchemas([schema ?? {}, resourceSchema, { properties: { type: { const: type, type: 'string' } } }]);
147
+ });
148
+
149
+
150
+ await $.spawn('rm -rf ./dist')
151
+ await $.spawn('npm run rollup', { interactive: true }); // re-run rollup without building for es
152
+
153
+ console.log('Generated JSON Schemas for all resources')
154
+
155
+ const distFolder = path.resolve(process.cwd(), 'dist');
156
+ const schemaOutputPath = path.resolve(distFolder, 'schemas.json');
157
+ fs.writeFileSync(schemaOutputPath, JSON.stringify(mergedSchemas, null, 2));
158
+
159
+ console.log('Successfully wrote schema to ./dist/schemas.json');
160
+
161
+ const documentationMap = fetchDocumentationMaps();
162
+
163
+ const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
164
+
165
+ fs.writeFileSync('./dist/manifest.json', JSON.stringify({
166
+ name: packageJson.name,
167
+ version: packageJson.version,
168
+ description: packageJson.description,
169
+ resources: [...resourceInfoMap.values()].map((info) => ({
170
+ type: info.type,
171
+ description: info.description ?? info.schema?.description,
172
+ sensitiveParameters: info.sensitiveParameters,
173
+ schema: info.schema,
174
+ operatingSystems: info.operatingSystems,
175
+ documentationKey: documentationMap.get(info.type),
176
+ })),
177
+ }, null, 2), 'utf8');
178
+
179
+ for (const key of documentationMap.values()) {
180
+ fs.mkdirSync(path.join('dist', 'documentation', key), { recursive: true })
181
+
182
+ fs.copyFileSync(
183
+ path.resolve(path.join('src', 'resources', key, 'README.md')),
184
+ path.resolve(path.join('dist', 'documentation', key, 'README.md')),
185
+ );
186
+ }
187
+
188
+ plugin.kill(9);
189
+ process.exit(0);
package/dist/bin/build.js CHANGED
File without changes
package/dist/index.d.ts CHANGED
@@ -5,7 +5,9 @@ export * from './plan/change-set.js';
5
5
  export * from './plan/plan.js';
6
6
  export * from './plan/plan-types.js';
7
7
  export * from './plugin/plugin.js';
8
+ export * from './pty/background-pty.js';
8
9
  export * from './pty/index.js';
10
+ export * from './pty/seqeuntial-pty.js';
9
11
  export * from './resource/parsed-resource-settings.js';
10
12
  export * from './resource/resource.js';
11
13
  export * from './resource/resource-settings.js';
@@ -14,4 +16,5 @@ export * from './utils/file-utils.js';
14
16
  export * from './utils/functions.js';
15
17
  export * from './utils/index.js';
16
18
  export * from './utils/verbosity-level.js';
19
+ export * from 'zod/v4';
17
20
  export declare function runPlugin(plugin: Plugin): Promise<void>;
package/dist/index.js CHANGED
@@ -5,7 +5,9 @@ export * from './plan/change-set.js';
5
5
  export * from './plan/plan.js';
6
6
  export * from './plan/plan-types.js';
7
7
  export * from './plugin/plugin.js';
8
+ export * from './pty/background-pty.js';
8
9
  export * from './pty/index.js';
10
+ export * from './pty/seqeuntial-pty.js';
9
11
  export * from './resource/parsed-resource-settings.js';
10
12
  export * from './resource/resource.js';
11
13
  export * from './resource/resource-settings.js';
@@ -14,6 +16,7 @@ export * from './utils/file-utils.js';
14
16
  export * from './utils/functions.js';
15
17
  export * from './utils/index.js';
16
18
  export * from './utils/verbosity-level.js';
19
+ export * from 'zod/v4';
17
20
  export async function runPlugin(plugin) {
18
21
  const messageHandler = new MessageHandler(plugin);
19
22
  process.on('message', (message) => messageHandler.onMessage(message));
@@ -1,6 +1,6 @@
1
1
  import { Ajv } from 'ajv';
2
2
  import addFormats from 'ajv-formats';
3
- import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MatchRequestDataSchema, MatchResponseDataSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
3
+ import { ApplyRequestDataSchema, EmptyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MatchRequestDataSchema, MatchResponseDataSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, SetVerbosityRequestDataSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
4
4
  import { SudoError } from '../errors.js';
5
5
  const SupportedRequests = {
6
6
  'initialize': {
@@ -18,6 +18,14 @@ const SupportedRequests = {
18
18
  requestValidator: GetResourceInfoRequestDataSchema,
19
19
  responseValidator: GetResourceInfoResponseDataSchema
20
20
  },
21
+ 'setVerbosityLevel': {
22
+ async handler(plugin, data) {
23
+ await plugin.setVerbosityLevel(data);
24
+ return null;
25
+ },
26
+ requestValidator: SetVerbosityRequestDataSchema,
27
+ responseValidator: EmptyResponseDataSchema,
28
+ },
21
29
  'match': {
22
30
  handler: async (plugin, data) => plugin.match(data),
23
31
  requestValidator: MatchRequestDataSchema,
@@ -39,7 +47,7 @@ const SupportedRequests = {
39
47
  return null;
40
48
  },
41
49
  requestValidator: ApplyRequestDataSchema,
42
- responseValidator: ApplyResponseDataSchema
50
+ responseValidator: EmptyResponseDataSchema
43
51
  },
44
52
  };
45
53
  export class MessageHandler {
package/dist/plan/plan.js CHANGED
@@ -187,6 +187,7 @@ export class Plan {
187
187
  * or wants to set. If a parameter is not specified then it's not managed by Codify.
188
188
  */
189
189
  static filterCurrentParams(params) {
190
+ console.log('Filter current params', params.desired, params.current, params.state, params.settings, params.isStateful);
190
191
  const { desired, current, state, settings, isStateful } = params;
191
192
  if (!current) {
192
193
  return null;
@@ -195,15 +196,20 @@ export class Plan {
195
196
  if (!filteredCurrent) {
196
197
  return null;
197
198
  }
199
+ console.log('Before exit', isStateful, desired);
198
200
  // For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
199
201
  // requires additional filtering for stateful parameter arrays and objects.
200
- if (isStateful) {
202
+ // We also want to filter parameters when in delete mode. We don't want to delete parameters that
203
+ // are not specified in the original config.
204
+ if (isStateful && desired) {
201
205
  return filteredCurrent;
202
206
  }
207
+ console.log('Post exit', isStateful, desired);
203
208
  // TODO: Add object handling here in addition to arrays in the future
204
209
  const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
205
210
  .filter(([k, v]) => isArrayParameterWithFiltering(k, v))
206
211
  .map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
212
+ console.log('Result', { ...filteredCurrent, ...arrayStatefulParameters });
207
213
  return { ...filteredCurrent, ...arrayStatefulParameters };
208
214
  function filterCurrent() {
209
215
  if (!current) {
@@ -1,4 +1,4 @@
1
- import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeRequestData, InitializeResponseData, MatchRequestData, MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
1
+ import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeRequestData, InitializeResponseData, MatchRequestData, MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, SetVerbosityRequestData, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
2
2
  import { Plan } from '../plan/plan.js';
3
3
  import { BackgroundPty } from '../pty/background-pty.js';
4
4
  import { Resource } from '../resource/resource.js';
@@ -17,6 +17,7 @@ export declare class Plugin {
17
17
  validate(data: ValidateRequestData): Promise<ValidateResponseData>;
18
18
  plan(data: PlanRequestData): Promise<PlanResponseData>;
19
19
  apply(data: ApplyRequestData): Promise<void>;
20
+ setVerbosityLevel(data: SetVerbosityRequestData): Promise<void>;
20
21
  kill(): Promise<void>;
21
22
  private resolvePlan;
22
23
  protected crossValidateResources(resources: ResourceJson[]): Promise<void>;
@@ -46,6 +46,7 @@ export class Plugin {
46
46
  type: r.typeId,
47
47
  sensitiveParameters,
48
48
  operatingSystems: r.settings.operatingSystems,
49
+ linuxDistros: r.settings.linuxDistros,
49
50
  };
50
51
  })
51
52
  };
@@ -55,7 +56,7 @@ export class Plugin {
55
56
  throw new Error(`Cannot get info for resource ${data.type}, resource doesn't exist`);
56
57
  }
57
58
  const resource = this.resourceControllers.get(data.type);
58
- const schema = resource.settings.schema;
59
+ const schema = resource.parsedSettings.schema;
59
60
  const requiredPropertyNames = (resource.settings.importAndDestroy?.requiredParameters
60
61
  ?? (typeof resource.settings.allowMultiple === 'object' ? resource.settings.allowMultiple.identifyingParameters : null)
61
62
  ?? schema?.required
@@ -84,6 +85,7 @@ export class Plugin {
84
85
  requiredParameters: requiredPropertyNames,
85
86
  },
86
87
  operatingSystems: resource.settings.operatingSystems,
88
+ linuxDistros: resource.settings.linuxDistros,
87
89
  sensitiveParameters,
88
90
  allowMultiple
89
91
  };
@@ -173,6 +175,9 @@ export class Plugin {
173
175
  throw new ApplyValidationError(plan);
174
176
  }
175
177
  }
178
+ async setVerbosityLevel(data) {
179
+ VerbosityLevel.set(data.verbosityLevel);
180
+ }
176
181
  async kill() {
177
182
  await this.planPty.kill();
178
183
  }
@@ -6,11 +6,12 @@ import { IPty, SpawnOptions, SpawnResult } from './index.js';
6
6
  * without a tty (or even a stdin) attached so interactive commands will not work.
7
7
  */
8
8
  export declare class BackgroundPty implements IPty {
9
+ private historyIgnore;
9
10
  private basePty;
10
11
  private promiseQueue;
11
12
  constructor();
12
- spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult>;
13
- spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult>;
13
+ spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
14
+ spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
14
15
  kill(): Promise<{
15
16
  exitCode: number;
16
17
  signal?: number | undefined;
@@ -17,8 +17,11 @@ EventEmitter.defaultMaxListeners = 1000;
17
17
  * without a tty (or even a stdin) attached so interactive commands will not work.
18
18
  */
19
19
  export class BackgroundPty {
20
+ historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
20
21
  basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
21
- env: process.env, name: nanoid(6),
22
+ env: { ...process.env, ...this.historyIgnore },
23
+ cols: 10_000, // Set to a really large value to prevent wrapping
24
+ name: nanoid(6),
22
25
  handleFlowControl: true
23
26
  });
24
27
  promiseQueue = new PromiseQueue();
@@ -28,11 +31,12 @@ export class BackgroundPty {
28
31
  async spawn(cmd, options) {
29
32
  const spawnResult = await this.spawnSafe(cmd, options);
30
33
  if (spawnResult.status !== 'success') {
31
- throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
34
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join(' ') : cmd, spawnResult.exitCode, spawnResult.data);
32
35
  }
33
36
  return spawnResult;
34
37
  }
35
38
  async spawnSafe(cmd, options) {
39
+ cmd = Array.isArray(cmd) ? cmd.join('\\\n') : cmd;
36
40
  // cid is command id
37
41
  const cid = nanoid(10);
38
42
  debugLog(cid);
@@ -84,7 +88,7 @@ export class BackgroundPty {
84
88
  resolve(null);
85
89
  }
86
90
  });
87
- console.log(`Running command ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
91
+ console.log(`Running command: ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
88
92
  this.basePty.write(`${command}\r`);
89
93
  }));
90
94
  }).finally(async () => {
@@ -104,17 +108,6 @@ export class BackgroundPty {
104
108
  await this.promiseQueue.run(async () => {
105
109
  let outputBuffer = '';
106
110
  return new Promise(resolve => {
107
- // zsh-specific commands
108
- switch (Utils.getShell()) {
109
- case Shell.ZSH: {
110
- this.basePty.write('setopt HIST_NO_STORE;\n');
111
- break;
112
- }
113
- default: {
114
- this.basePty.write('export HISTIGNORE=\'history*\';\n');
115
- break;
116
- }
117
- }
118
111
  this.basePty.write(' unset PS1;\n');
119
112
  this.basePty.write(' unset PS0;\n');
120
113
  this.basePty.write(' echo setup complete\\"\n');
@@ -21,11 +21,17 @@ export declare enum SpawnStatus {
21
21
  *
22
22
  * @property {boolean} [interactive] - Indicates whether the spawned process needs
23
23
  * to be interactive. Only works within apply (not plan). Defaults to true.
24
+ *
25
+ * @property {boolean} [disableWrapping] - Forces the terminal width to 10_000 to disable wrapping.
26
+ * In applys, this is off by default while it is on during plans.
24
27
  */
25
28
  export interface SpawnOptions {
26
29
  cwd?: string;
27
30
  env?: Record<string, unknown>;
28
31
  interactive?: boolean;
32
+ requiresRoot?: boolean;
33
+ stdin?: boolean;
34
+ disableWrapping?: boolean;
29
35
  }
30
36
  export declare class SpawnError extends Error {
31
37
  data: string;
@@ -34,8 +40,8 @@ export declare class SpawnError extends Error {
34
40
  constructor(cmd: string, exitCode: number, data: string);
35
41
  }
36
42
  export interface IPty {
37
- spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult>;
38
- spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult>;
43
+ spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
44
+ spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
39
45
  kill(): Promise<{
40
46
  exitCode: number;
41
47
  signal?: number | undefined;
@@ -6,11 +6,12 @@ import { IPty, SpawnOptions, SpawnResult } from './index.js';
6
6
  * without a tty (or even a stdin) attached so interactive commands will not work.
7
7
  */
8
8
  export declare class SequentialPty implements IPty {
9
- spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult>;
10
- spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult>;
9
+ spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
10
+ spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
11
11
  kill(): Promise<{
12
12
  exitCode: number;
13
13
  signal?: number | undefined;
14
14
  }>;
15
+ externalSpawn(cmd: string, opts: SpawnOptions): Promise<SpawnResult>;
15
16
  private getDefaultShell;
16
17
  }
@@ -1,10 +1,17 @@
1
1
  import pty from '@homebridge/node-pty-prebuilt-multiarch';
2
+ import { Ajv } from 'ajv';
3
+ import { CommandRequestResponseDataSchema, 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
  import { Shell, Utils } from '../utils/index.js';
5
8
  import { VerbosityLevel } from '../utils/verbosity-level.js';
6
9
  import { SpawnError, SpawnStatus } from './index.js';
7
10
  EventEmitter.defaultMaxListeners = 1000;
11
+ const ajv = new Ajv({
12
+ strict: true,
13
+ });
14
+ const validateSudoRequestResponse = ajv.compile(CommandRequestResponseDataSchema);
8
15
  /**
9
16
  * The background pty is a specialized pty designed for speed. It can launch multiple tasks
10
17
  * in parallel by moving them to the background. It attaches unix FIFO pipes to each process
@@ -15,12 +22,20 @@ export class SequentialPty {
15
22
  async spawn(cmd, options) {
16
23
  const spawnResult = await this.spawnSafe(cmd, options);
17
24
  if (spawnResult.status !== 'success') {
18
- throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
25
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data);
19
26
  }
20
27
  return spawnResult;
21
28
  }
22
29
  async spawnSafe(cmd, options) {
23
- console.log(`Running command: ${cmd}` + (options?.cwd ? `(${options?.cwd})` : ''));
30
+ cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd;
31
+ if (cmd.includes('sudo')) {
32
+ throw new Error('Do not directly use sudo. Use the option { requiresRoot: true } instead');
33
+ }
34
+ // If sudo is required, we must delegate to the main codify process.
35
+ if (options?.stdin || options?.requiresRoot) {
36
+ return this.externalSpawn(cmd, options);
37
+ }
38
+ console.log(`Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : ''));
24
39
  return new Promise((resolve) => {
25
40
  const output = [];
26
41
  const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
@@ -30,12 +45,14 @@ export class SequentialPty {
30
45
  ...process.env, ...options?.env,
31
46
  TERM_PROGRAM: 'codify',
32
47
  COMMAND_MODE: 'unix2003',
33
- COLORTERM: 'truecolor', ...historyIgnore
48
+ COLORTERM: 'truecolor',
49
+ ...historyIgnore
34
50
  };
35
51
  // Initial terminal dimensions
36
- const initialCols = process.stdout.columns ?? 80;
52
+ // Set to a really large value to prevent wrapping
53
+ const initialCols = options?.disableWrapping ? 10_000 : process.stdout.columns ?? 80;
37
54
  const initialRows = process.stdout.rows ?? 24;
38
- const args = (options?.interactive ?? false) ? ['-i', '-c', cmd] : ['-c', cmd];
55
+ const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd];
39
56
  // Run the command in a pty for interactivity
40
57
  const mPty = pty.spawn(this.getDefaultShell(), args, {
41
58
  ...options,
@@ -49,20 +66,14 @@ export class SequentialPty {
49
66
  }
50
67
  output.push(data.toString());
51
68
  });
52
- const stdinListener = (data) => {
53
- mPty.write(data.toString());
54
- };
55
69
  const resizeListener = () => {
56
70
  const { columns, rows } = process.stdout;
57
- mPty.resize(columns, rows);
71
+ mPty.resize(columns, options?.disableWrapping ? 10_000 : rows);
58
72
  };
59
73
  // Listen to resize events for the terminal window;
60
74
  process.stdout.on('resize', resizeListener);
61
- // Listen for user input
62
- process.stdin.on('data', stdinListener);
63
75
  mPty.onExit((result) => {
64
76
  process.stdout.off('resize', resizeListener);
65
- process.stdin.off('data', stdinListener);
66
77
  resolve({
67
78
  status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
68
79
  exitCode: result.exitCode,
@@ -78,6 +89,30 @@ export class SequentialPty {
78
89
  signal: 0,
79
90
  };
80
91
  }
92
+ // For safety reasons, requests that require sudo or are interactive must be run via the main client
93
+ async externalSpawn(cmd, opts) {
94
+ return new Promise((resolve) => {
95
+ const requestId = nanoid(8);
96
+ const listener = (data) => {
97
+ if (data.requestId === requestId) {
98
+ process.removeListener('message', listener);
99
+ if (!validateSudoRequestResponse(data.data)) {
100
+ throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
101
+ }
102
+ resolve(data.data);
103
+ }
104
+ };
105
+ process.on('message', listener);
106
+ process.send({
107
+ cmd: MessageCmd.COMMAND_REQUEST,
108
+ data: {
109
+ command: cmd,
110
+ options: opts ?? {},
111
+ },
112
+ requestId
113
+ });
114
+ });
115
+ }
81
116
  getDefaultShell() {
82
117
  return process.env.SHELL;
83
118
  }
@@ -1,5 +1,5 @@
1
1
  import { JSONSchemaType } from 'ajv';
2
- import { OS, StringIndexedObject } from 'codify-schemas';
2
+ import { LinuxDistro, OS, StringIndexedObject } from 'codify-schemas';
3
3
  import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
4
4
  import { ArrayParameterSetting, DefaultParameterSetting, InputTransformation, ResourceSettings } from './resource-settings.js';
5
5
  export interface ParsedStatefulParameterSetting extends DefaultParameterSetting {
@@ -18,15 +18,18 @@ export type ParsedParameterSetting = {
18
18
  export declare class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
19
19
  private cache;
20
20
  id: string;
21
+ description?: string;
21
22
  schema?: Partial<JSONSchemaType<T | any>>;
22
23
  allowMultiple?: {
24
+ identifyingParameters?: string[];
23
25
  matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
24
- requiredParameters?: string[];
26
+ findAllParameters?: () => Promise<Array<Partial<T>>>;
25
27
  } | boolean;
26
28
  removeStatefulParametersBeforeDestroy?: boolean | undefined;
27
29
  dependencies?: string[] | undefined;
28
30
  transformation?: InputTransformation;
29
31
  operatingSystems: Array<OS>;
32
+ linuxDistros?: Array<LinuxDistro>;
30
33
  isSensitive?: boolean;
31
34
  private settings;
32
35
  constructor(settings: ResourceSettings<T>);
@@ -1,20 +1,34 @@
1
+ import { ZodObject, z } from 'zod';
1
2
  import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
2
3
  import { resolveElementEqualsFn, resolveEqualsFn, resolveMatcher, resolveParameterTransformFn } from './resource-settings.js';
3
4
  export class ParsedResourceSettings {
4
5
  cache = new Map();
5
6
  id;
7
+ description;
6
8
  schema;
7
9
  allowMultiple;
8
10
  removeStatefulParametersBeforeDestroy;
9
11
  dependencies;
10
12
  transformation;
11
13
  operatingSystems;
14
+ linuxDistros;
12
15
  isSensitive;
13
16
  settings;
14
17
  constructor(settings) {
15
18
  this.settings = settings;
16
- const { parameterSettings, ...rest } = settings;
19
+ const { parameterSettings, schema, ...rest } = settings;
17
20
  Object.assign(this, rest);
21
+ if (schema) {
22
+ this.schema = schema instanceof ZodObject
23
+ ? z.toJSONSchema(schema.strict(), {
24
+ target: 'draft-7',
25
+ override(ctx) {
26
+ ctx.jsonSchema.title = settings.id;
27
+ ctx.jsonSchema.description = settings.description ?? `${settings.id} resource. Can be used to manage ${settings.id}`;
28
+ }
29
+ })
30
+ : schema;
31
+ }
18
32
  this.validateSettings();
19
33
  }
20
34
  get typeId() {
@@ -118,7 +132,7 @@ export class ParsedResourceSettings {
118
132
  && typeof this.settings.allowMultiple === 'object' && this.settings.allowMultiple?.identifyingParameters?.includes(k))) {
119
133
  throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`);
120
134
  }
121
- const schema = this.settings.schema;
135
+ const schema = this.schema;
122
136
  if (!this.settings.importAndDestroy && (schema?.oneOf
123
137
  && Array.isArray(schema.oneOf)
124
138
  && schema.oneOf.some((s) => s.required))
@@ -17,16 +17,16 @@ export class ResourceController {
17
17
  this.settings = resource.getSettings();
18
18
  this.typeId = this.settings.id;
19
19
  this.dependencies = this.settings.dependencies ?? [];
20
- if (this.settings.schema) {
20
+ this.parsedSettings = new ParsedResourceSettings(this.settings);
21
+ if (this.parsedSettings.schema) {
21
22
  this.ajv = new Ajv({
22
23
  allErrors: true,
23
24
  strict: true,
24
25
  strictRequired: false,
25
26
  allowUnionTypes: true
26
27
  });
27
- this.schemaValidator = this.ajv.compile(this.settings.schema);
28
+ this.schemaValidator = this.ajv.compile(this.parsedSettings.schema);
28
29
  }
29
- this.parsedSettings = new ParsedResourceSettings(this.settings);
30
30
  }
31
31
  async initialize() {
32
32
  return this.resource.initialize();
@@ -374,8 +374,8 @@ ${JSON.stringify(refresh, null, 2)}
374
374
  .sort((a, b) => this.parsedSettings.statefulParameterOrder.get(a.name) - this.parsedSettings.statefulParameterOrder.get(b.name));
375
375
  }
376
376
  getAllParameterKeys() {
377
- return this.settings.schema
378
- ? Object.keys(this.settings.schema?.properties)
377
+ return this.parsedSettings.schema
378
+ ? Object.keys(this.parsedSettings.schema?.properties)
379
379
  : Object.keys(this.parsedSettings.parameterSettings);
380
380
  }
381
381
  getParametersToRefreshForImport(parameters, context) {