codify-plugin-lib 1.0.182-beta3 → 1.0.182-beta30

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 (40) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +2 -0
  3. package/dist/plugin/plugin.js +1 -1
  4. package/dist/pty/background-pty.d.ts +3 -2
  5. package/dist/pty/background-pty.js +6 -14
  6. package/dist/pty/index.d.ts +4 -2
  7. package/dist/pty/seqeuntial-pty.d.ts +3 -2
  8. package/dist/pty/seqeuntial-pty.js +44 -10
  9. package/dist/resource/parsed-resource-settings.d.ts +3 -1
  10. package/dist/resource/parsed-resource-settings.js +15 -2
  11. package/dist/resource/resource-controller.js +3 -3
  12. package/dist/resource/resource-settings.d.ts +8 -2
  13. package/dist/resource/resource-settings.js +1 -1
  14. package/dist/test.d.ts +1 -0
  15. package/dist/test.js +5 -0
  16. package/dist/utils/file-utils.d.ts +23 -0
  17. package/dist/utils/file-utils.js +186 -0
  18. package/dist/utils/functions.d.ts +12 -0
  19. package/dist/utils/functions.js +74 -0
  20. package/dist/utils/index.d.ts +4 -0
  21. package/dist/utils/index.js +30 -0
  22. package/package.json +4 -3
  23. package/src/index.ts +2 -0
  24. package/src/plugin/plugin.test.ts +31 -0
  25. package/src/plugin/plugin.ts +1 -1
  26. package/src/pty/background-pty.ts +9 -18
  27. package/src/pty/index.ts +6 -4
  28. package/src/pty/seqeuntial-pty.ts +59 -14
  29. package/src/pty/sequential-pty.test.ts +138 -5
  30. package/src/resource/parsed-resource-settings.test.ts +24 -0
  31. package/src/resource/parsed-resource-settings.ts +24 -7
  32. package/src/resource/resource-controller.test.ts +127 -1
  33. package/src/resource/resource-controller.ts +3 -4
  34. package/src/resource/resource-settings.test.ts +36 -0
  35. package/src/resource/resource-settings.ts +10 -3
  36. package/src/utils/file-utils.test.ts +7 -0
  37. package/src/utils/file-utils.ts +231 -0
  38. package/src/utils/{internal-utils.ts → functions.ts} +3 -3
  39. package/src/utils/index.ts +37 -0
  40. package/src/utils/internal-utils.test.ts +2 -1
@@ -0,0 +1,74 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export function splitUserConfig(config) {
4
+ const coreParameters = {
5
+ type: config.type,
6
+ ...(config.name ? { name: config.name } : {}),
7
+ ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
8
+ ...(config.os ? { os: config.os } : {}),
9
+ };
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ const { type, name, dependsOn, os, ...parameters } = config;
12
+ return {
13
+ parameters: parameters,
14
+ coreParameters,
15
+ };
16
+ }
17
+ export function setsEqual(set1, set2) {
18
+ return set1.size === set2.size && [...set1].every((v) => set2.has(v));
19
+ }
20
+ const homeDirectory = os.homedir();
21
+ export function untildify(pathWithTilde) {
22
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
23
+ }
24
+ export function tildify(pathWithTilde) {
25
+ return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde;
26
+ }
27
+ export function resolvePathWithVariables(pathWithVariables) {
28
+ return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]);
29
+ }
30
+ export function addVariablesToPath(pathWithoutVariables) {
31
+ let result = pathWithoutVariables;
32
+ for (const [key, value] of Object.entries(process.env)) {
33
+ if (!value || !path.isAbsolute(value) || value === '/' || key === 'HOME' || key === 'PATH' || key === 'SHELL' || key === 'PWD') {
34
+ continue;
35
+ }
36
+ result = result.replaceAll(value, `$${key}`);
37
+ }
38
+ return result;
39
+ }
40
+ export function unhome(pathWithHome) {
41
+ return pathWithHome.includes('$HOME') ? pathWithHome.replaceAll('$HOME', os.homedir()) : pathWithHome;
42
+ }
43
+ export function areArraysEqual(isElementEqual, desired, current) {
44
+ if (!desired || !current) {
45
+ return false;
46
+ }
47
+ if (!Array.isArray(desired) || !Array.isArray(current)) {
48
+ throw new Error(`A non-array value:
49
+
50
+ Desired: ${JSON.stringify(desired, null, 2)}
51
+
52
+ Current: ${JSON.stringify(desired, null, 2)}
53
+
54
+ Was provided even though type array was specified.
55
+ `);
56
+ }
57
+ if (desired.length !== current.length) {
58
+ return false;
59
+ }
60
+ const desiredCopy = [...desired];
61
+ const currentCopy = [...current];
62
+ // Algorithm for to check equality between two un-ordered; un-hashable arrays using
63
+ // an isElementEqual method. Time: O(n^2)
64
+ for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
65
+ const idx = currentCopy.findIndex((e2) => (isElementEqual
66
+ ?? ((a, b) => a === b))(desiredCopy[counter], e2));
67
+ if (idx === -1) {
68
+ return false;
69
+ }
70
+ desiredCopy.splice(counter, 1);
71
+ currentCopy.splice(idx, 1);
72
+ }
73
+ return currentCopy.length === 0;
74
+ }
@@ -20,7 +20,11 @@ export declare const Utils: {
20
20
  };
21
21
  isMacOS(): boolean;
22
22
  isLinux(): boolean;
23
+ isArmArch(): Promise<boolean>;
24
+ isHomebrewInstalled(): Promise<boolean>;
25
+ isRosetta2Installed(): Promise<boolean>;
23
26
  getShell(): Shell | undefined;
24
27
  getPrimaryShellRc(): string;
25
28
  getShellRcFiles(): string[];
29
+ isDirectoryOnPath(directory: string): Promise<boolean>;
26
30
  };
@@ -1,5 +1,6 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
+ import { getPty, SpawnStatus } from '../pty/index.js';
3
4
  export function isDebug() {
4
5
  return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
5
6
  }
@@ -28,6 +29,29 @@ export const Utils = {
28
29
  isLinux() {
29
30
  return os.platform() === 'linux';
30
31
  },
32
+ async isArmArch() {
33
+ const $ = getPty();
34
+ if (!Utils.isMacOS()) {
35
+ // On Linux, check uname -m
36
+ const query = await $.spawn('uname -m');
37
+ return query.data.trim() === 'aarch64' || query.data.trim() === 'arm64';
38
+ }
39
+ const query = await $.spawn('sysctl -n machdep.cpu.brand_string');
40
+ return /M(\d)/.test(query.data);
41
+ },
42
+ async isHomebrewInstalled() {
43
+ const $ = getPty();
44
+ const query = await $.spawnSafe('which brew', { interactive: true });
45
+ return query.status === SpawnStatus.SUCCESS;
46
+ },
47
+ async isRosetta2Installed() {
48
+ if (!Utils.isMacOS()) {
49
+ return false;
50
+ }
51
+ const $ = getPty();
52
+ const query = await $.spawnSafe('arch -x86_64 /usr/bin/true 2> /dev/null', { interactive: true });
53
+ return query.status === SpawnStatus.SUCCESS;
54
+ },
31
55
  getShell() {
32
56
  const shell = process.env.SHELL || '';
33
57
  if (shell.endsWith('bash')) {
@@ -108,4 +132,10 @@ export const Utils = {
108
132
  path.join(homeDir, '.profile'),
109
133
  ];
110
134
  },
135
+ async isDirectoryOnPath(directory) {
136
+ const $ = getPty();
137
+ const { data: pathQuery } = await $.spawn('echo $PATH', { interactive: true });
138
+ const lines = pathQuery.split(':');
139
+ return lines.includes(directory);
140
+ },
111
141
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.182-beta3",
3
+ "version": "1.0.182-beta30",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -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-beta7",
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/src/index.ts CHANGED
@@ -12,6 +12,8 @@ export * from './resource/parsed-resource-settings.js';
12
12
  export * from './resource/resource.js'
13
13
  export * from './resource/resource-settings.js'
14
14
  export * from './stateful-parameter/stateful-parameter.js'
15
+ export * from './utils/file-utils.js'
16
+ export * from './utils/functions.js'
15
17
  export * from './utils/index.js'
16
18
  export * from './utils/verbosity-level.js'
17
19
 
@@ -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',
@@ -89,7 +89,7 @@ export class Plugin {
89
89
 
90
90
  const resource = this.resourceControllers.get(data.type)!;
91
91
 
92
- const schema = resource.settings.schema as JSONSchemaType<any> | undefined;
92
+ const schema = resource.parsedSettings.schema as JSONSchemaType<any> | undefined;
93
93
  const requiredPropertyNames = (
94
94
  resource.settings.importAndDestroy?.requiredParameters
95
95
  ?? (typeof resource.settings.allowMultiple === 'object' ? resource.settings.allowMultiple.identifyingParameters : null)
@@ -20,8 +20,10 @@ 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, name: nanoid(6),
25
+ env: { ...process.env, ...this.historyIgnore },
26
+ name: nanoid(6),
25
27
  handleFlowControl: true
26
28
  });
27
29
 
@@ -31,17 +33,19 @@ export class BackgroundPty implements IPty {
31
33
  this.initialize();
32
34
  }
33
35
 
34
- async spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
36
+ async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
35
37
  const spawnResult = await this.spawnSafe(cmd, options);
36
38
 
37
39
  if (spawnResult.status !== 'success') {
38
- throw new SpawnError(cmd, spawnResult.exitCode, spawnResult.data);
40
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join(' ') : cmd, spawnResult.exitCode, spawnResult.data);
39
41
  }
40
42
 
41
43
  return spawnResult;
42
44
  }
43
45
 
44
- async spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
46
+ async spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
47
+ cmd = Array.isArray(cmd) ? cmd.join('\\\n') : cmd;
48
+
45
49
  // cid is command id
46
50
  const cid = nanoid(10);
47
51
  debugLog(cid);
@@ -102,7 +106,7 @@ export class BackgroundPty implements IPty {
102
106
  }
103
107
  });
104
108
 
105
- console.log(`Running command ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
109
+ console.log(`Running command: ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
106
110
  this.basePty.write(`${command}\r`);
107
111
 
108
112
  }));
@@ -128,19 +132,6 @@ export class BackgroundPty implements IPty {
128
132
  let outputBuffer = '';
129
133
 
130
134
  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
135
  this.basePty.write(' unset PS1;\n');
145
136
  this.basePty.write(' unset PS0;\n')
146
137
  this.basePty.write(' echo setup complete\\"\n')
package/src/pty/index.ts CHANGED
@@ -28,8 +28,10 @@ export enum SpawnStatus {
28
28
  */
29
29
  export interface SpawnOptions {
30
30
  cwd?: string;
31
- env?: Record<string, unknown>,
32
- interactive?: boolean,
31
+ env?: Record<string, unknown>;
32
+ interactive?: boolean;
33
+ requiresRoot?: boolean;
34
+ stdin?: boolean;
33
35
  }
34
36
 
35
37
  export class SpawnError extends Error {
@@ -48,9 +50,9 @@ export class SpawnError extends Error {
48
50
  }
49
51
 
50
52
  export interface IPty {
51
- spawn(cmd: string, options?: SpawnOptions): Promise<SpawnResult>
53
+ spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
52
54
 
53
- spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult>
55
+ spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
54
56
 
55
57
  kill(): Promise<{ exitCode: number, signal?: number | undefined }>
56
58
  }
@@ -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
- console.log(`Running command: ${cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
36
+ async spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
37
+ cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd;
38
+
39
+ if (cmd.includes('sudo')) {
40
+ throw new Error('Do not directly use sudo. Use the option { requiresRoot: true } instead')
41
+ }
42
+
43
+ // If sudo is required, we must delegate to the main codify process.
44
+ if (options?.stdin || options?.requiresRoot) {
45
+ return this.externalSpawn(cmd, options);
46
+ }
47
+
48
+ console.log(`Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
30
49
 
31
50
  return new Promise((resolve) => {
32
51
  const output: string[] = [];
33
-
34
52
  const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
35
53
 
36
54
  // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
@@ -39,14 +57,15 @@ export class SequentialPty implements IPty {
39
57
  ...process.env, ...options?.env,
40
58
  TERM_PROGRAM: 'codify',
41
59
  COMMAND_MODE: 'unix2003',
42
- COLORTERM: 'truecolor', ...historyIgnore
60
+ COLORTERM: 'truecolor',
61
+ ...historyIgnore
43
62
  }
44
63
 
45
64
  // Initial terminal dimensions
46
65
  const initialCols = process.stdout.columns ?? 80;
47
66
  const initialRows = process.stdout.rows ?? 24;
48
67
 
49
- const args = (options?.interactive ?? false) ? ['-i', '-c', `"${cmd}"`] : ['-c', `"${cmd}"`]
68
+ const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd]
50
69
 
51
70
  // Run the command in a pty for interactivity
52
71
  const mPty = pty.spawn(this.getDefaultShell(), args, {
@@ -64,10 +83,6 @@ export class SequentialPty implements IPty {
64
83
  output.push(data.toString());
65
84
  })
66
85
 
67
- const stdinListener = (data: any) => {
68
- mPty.write(data.toString());
69
- };
70
-
71
86
  const resizeListener = () => {
72
87
  const { columns, rows } = process.stdout;
73
88
  mPty.resize(columns, rows);
@@ -75,12 +90,9 @@ export class SequentialPty implements IPty {
75
90
 
76
91
  // Listen to resize events for the terminal window;
77
92
  process.stdout.on('resize', resizeListener);
78
- // Listen for user input
79
- process.stdin.on('data', stdinListener);
80
93
 
81
94
  mPty.onExit((result) => {
82
95
  process.stdout.off('resize', resizeListener);
83
- process.stdin.off('data', stdinListener);
84
96
 
85
97
  resolve({
86
98
  status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
@@ -99,6 +111,39 @@ export class SequentialPty implements IPty {
99
111
  }
100
112
  }
101
113
 
114
+ // For safety reasons, requests that require sudo or are interactive must be run via the main client
115
+ async externalSpawn(
116
+ cmd: string,
117
+ opts: SpawnOptions
118
+ ): Promise<SpawnResult> {
119
+ return new Promise((resolve) => {
120
+ const requestId = nanoid(8);
121
+
122
+ const listener = (data: IpcMessageV2) => {
123
+ if (data.requestId === requestId) {
124
+ process.removeListener('message', listener);
125
+
126
+ if (!validateSudoRequestResponse(data.data)) {
127
+ throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
128
+ }
129
+
130
+ resolve(data.data as unknown as CommandRequestResponseData);
131
+ }
132
+ }
133
+
134
+ process.on('message', listener);
135
+
136
+ process.send!(<IpcMessageV2>{
137
+ cmd: MessageCmd.COMMAND_REQUEST,
138
+ data: {
139
+ command: cmd,
140
+ options: opts ?? {},
141
+ },
142
+ requestId
143
+ })
144
+ });
145
+ }
146
+
102
147
  private getDefaultShell(): string {
103
148
  return process.env.SHELL!;
104
149
  }
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { SequentialPty } from './seqeuntial-pty.js';
3
- import { VerbosityLevel } from '../utils/internal-utils.js';
3
+ import { VerbosityLevel } from '../utils/verbosity-level.js';
4
+ import { MessageStatus, SpawnStatus } from 'codify-schemas/src/types/index.js';
5
+ import { IpcMessageV2, MessageCmd } from 'codify-schemas';
4
6
 
5
7
  describe('SequentialPty tests', () => {
6
8
  it('Can launch a simple command', async () => {
@@ -33,8 +35,8 @@ describe('SequentialPty tests', () => {
33
35
  const resultFailed = await pty.spawnSafe('which sjkdhsakjdhjkash');
34
36
  expect(resultFailed).toMatchObject({
35
37
  status: 'error',
36
- exitCode: 127,
37
- data: 'zsh:1: command not found: which sjkdhsakjdhjkash' // This might change on different os or shells. Keep for now.
38
+ exitCode: 1,
39
+ data: 'sjkdhsakjdhjkash not found' // This might change on different os or shells. Keep for now.
38
40
  })
39
41
  });
40
42
 
@@ -49,13 +51,144 @@ describe('SequentialPty tests', () => {
49
51
  })
50
52
  });
51
53
 
52
- it('It can launch a command in interactive mode', async () => {
54
+
55
+ it('Can use multi-line commands', async () => {
53
56
  const pty = new SequentialPty();
54
57
 
55
- const resultSuccess = await pty.spawnSafe('ls', { interactive: false });
58
+ const resultSuccess = await pty.spawnSafe([
59
+ 'pwd',
60
+ '&& ls',
61
+ ], { cwd: '/tmp' });
56
62
  expect(resultSuccess).toMatchObject({
57
63
  status: 'success',
58
64
  exitCode: 0,
59
65
  })
60
66
  });
67
+
68
+
69
+ it('It can launch a command in interactive mode', async () => {
70
+ const originalSend = process.send;
71
+ process.send = (req: IpcMessageV2) => {
72
+ expect(req).toMatchObject({
73
+ cmd: MessageCmd.COMMAND_REQUEST,
74
+ requestId: expect.any(String),
75
+ data: {
76
+ command: 'ls',
77
+ options: {
78
+ cwd: '/tmp',
79
+ interactive: true,
80
+ }
81
+ }
82
+ })
83
+
84
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
85
+ const listeners = process.listeners('message');
86
+ listeners[2](({
87
+ cmd: MessageCmd.COMMAND_REQUEST,
88
+ requestId: req.requestId,
89
+ status: MessageStatus.SUCCESS,
90
+ data: {
91
+ status: SpawnStatus.SUCCESS,
92
+ exitCode: 0,
93
+ data: 'My data',
94
+ }
95
+ }))
96
+
97
+ return true;
98
+ }
99
+
100
+ const $ = new SequentialPty();
101
+ const resultSuccess = await $.spawnSafe('ls', { interactive: true, cwd: '/tmp' });
102
+
103
+ expect(resultSuccess).toMatchObject({
104
+ status: 'success',
105
+ exitCode: 0,
106
+ });
107
+
108
+ process.send = originalSend;
109
+ });
110
+
111
+ it('It can work with root (sudo)', async () => {
112
+ const originalSend = process.send;
113
+ process.send = (req: IpcMessageV2) => {
114
+ expect(req).toMatchObject({
115
+ cmd: MessageCmd.COMMAND_REQUEST,
116
+ requestId: expect.any(String),
117
+ data: {
118
+ command: 'ls',
119
+ options: {
120
+ interactive: true,
121
+ requiresRoot: true,
122
+ }
123
+ }
124
+ })
125
+
126
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
127
+ const listeners = process.listeners('message');
128
+ listeners[2](({
129
+ cmd: MessageCmd.COMMAND_REQUEST,
130
+ requestId: req.requestId,
131
+ status: MessageStatus.SUCCESS,
132
+ data: {
133
+ status: SpawnStatus.SUCCESS,
134
+ exitCode: 0,
135
+ data: 'My data',
136
+ }
137
+ }))
138
+
139
+ return true;
140
+ }
141
+
142
+ const $ = new SequentialPty();
143
+ const resultSuccess = await $.spawn('ls', { interactive: true, requiresRoot: true });
144
+
145
+ expect(resultSuccess).toMatchObject({
146
+ status: 'success',
147
+ exitCode: 0,
148
+ });
149
+
150
+ process.send = originalSend;
151
+ })
152
+
153
+ it('It can handle errors when in sudo', async () => {
154
+ const originalSend = process.send;
155
+ process.send = (req: IpcMessageV2) => {
156
+ expect(req).toMatchObject({
157
+ cmd: MessageCmd.COMMAND_REQUEST,
158
+ requestId: expect.any(String),
159
+ data: {
160
+ command: 'ls',
161
+ options: {
162
+ requiresRoot: true,
163
+ interactive: true,
164
+ }
165
+ }
166
+ })
167
+
168
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
169
+ const listeners = process.listeners('message');
170
+ listeners[2](({
171
+ cmd: MessageCmd.COMMAND_REQUEST,
172
+ requestId: req.requestId,
173
+ status: MessageStatus.SUCCESS,
174
+ data: {
175
+ status: SpawnStatus.ERROR,
176
+ exitCode: 127,
177
+ data: 'My data',
178
+ }
179
+ }))
180
+
181
+ return true;
182
+ }
183
+
184
+ const $ = new SequentialPty();
185
+ const resultSuccess = await $.spawnSafe('ls', { interactive: true, requiresRoot: true });
186
+
187
+ expect(resultSuccess).toMatchObject({
188
+ status: SpawnStatus.ERROR,
189
+ exitCode: 127,
190
+ });
191
+
192
+ process.send = originalSend;
193
+ })
61
194
  })
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ResourceSettings } from './resource-settings.js';
3
3
  import { ParsedResourceSettings } from './parsed-resource-settings.js';
4
4
  import { TestConfig } from '../utils/test-utils.test.js';
5
+ import { z } from 'zod';
6
+ import { OS, ResourceConfig, ResourceSchema } from 'codify-schemas';
5
7
 
6
8
  describe('Resource options parser tests', () => {
7
9
  it('Parses default values from options', () => {
@@ -159,4 +161,26 @@ describe('Resource options parser tests', () => {
159
161
 
160
162
  expect(() => new ParsedResourceSettings(option)).toThrowError()
161
163
  })
164
+
165
+ it('Can handle a zod schema', () => {
166
+
167
+ const schema = z.object({
168
+ propA: z.string(),
169
+ repository: z.string(),
170
+ })
171
+
172
+ const option: ResourceSettings<z.infer<typeof schema>> = {
173
+ id: 'typeId',
174
+ operatingSystems: [OS.Darwin],
175
+ schema,
176
+ importAndDestroy: {
177
+ defaultRefreshValues: {
178
+ repository: 'abc'
179
+ }
180
+ }
181
+ }
182
+
183
+ console.log(new ParsedResourceSettings(option))
184
+
185
+ })
162
186
  })