codify-plugin-lib 1.0.182-beta2 → 1.0.182-beta21

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.
@@ -7,7 +7,7 @@ import stripAnsi from 'strip-ansi';
7
7
 
8
8
  import { debugLog } from '../utils/debug.js';
9
9
  import { Shell, Utils } from '../utils/index.js';
10
- import { VerbosityLevel } from '../utils/internal-utils.js';
10
+ import { VerbosityLevel } from '../utils/verbosity-level.js';
11
11
  import { IPty, SpawnError, SpawnOptions, SpawnResult } from './index.js';
12
12
  import { PromiseQueue } from './promise-queue.js';
13
13
 
@@ -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
 
@@ -102,7 +104,7 @@ export class BackgroundPty implements IPty {
102
104
  }
103
105
  });
104
106
 
105
- console.log(`Running command ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
107
+ console.log(`Running command: ${cmd}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`)
106
108
  this.basePty.write(`${command}\r`);
107
109
 
108
110
  }));
@@ -128,19 +130,6 @@ export class BackgroundPty implements IPty {
128
130
  let outputBuffer = '';
129
131
 
130
132
  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
133
  this.basePty.write(' unset PS1;\n');
145
134
  this.basePty.write(' unset PS0;\n')
146
135
  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 {
@@ -1,13 +1,21 @@
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
 
5
8
  import { Shell, Utils } from '../utils/index.js';
6
- import { VerbosityLevel } from '../utils/internal-utils.js';
9
+ import { VerbosityLevel } from '../utils/verbosity-level.js';
7
10
  import { IPty, SpawnError, SpawnOptions, SpawnResult, SpawnStatus } from './index.js';
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
@@ -26,11 +34,19 @@ export class SequentialPty implements IPty {
26
34
  }
27
35
 
28
36
  async spawnSafe(cmd: string, options?: SpawnOptions): Promise<SpawnResult> {
37
+ if (cmd.includes('sudo')) {
38
+ throw new Error('Do not directly use sudo. Use the option { requiresRoot: true } instead')
39
+ }
40
+
41
+ // If sudo is required, we must delegate to the main codify process.
42
+ if (options?.stdin || options?.requiresRoot) {
43
+ return this.externalSpawn(cmd, options);
44
+ }
45
+
29
46
  console.log(`Running command: ${cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
30
47
 
31
48
  return new Promise((resolve) => {
32
49
  const output: string[] = [];
33
-
34
50
  const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
35
51
 
36
52
  // If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
@@ -39,14 +55,15 @@ export class SequentialPty implements IPty {
39
55
  ...process.env, ...options?.env,
40
56
  TERM_PROGRAM: 'codify',
41
57
  COMMAND_MODE: 'unix2003',
42
- COLORTERM: 'truecolor', ...historyIgnore
58
+ COLORTERM: 'truecolor',
59
+ ...historyIgnore
43
60
  }
44
61
 
45
62
  // Initial terminal dimensions
46
63
  const initialCols = process.stdout.columns ?? 80;
47
64
  const initialRows = process.stdout.rows ?? 24;
48
65
 
49
- const args = (options?.interactive ?? false) ? ['-i', '-c', `"${cmd}"`] : ['-c', `"${cmd}"`]
66
+ const args = options?.interactive ? ['-i', '-c', cmd] : ['-c', cmd]
50
67
 
51
68
  // Run the command in a pty for interactivity
52
69
  const mPty = pty.spawn(this.getDefaultShell(), args, {
@@ -64,10 +81,6 @@ export class SequentialPty implements IPty {
64
81
  output.push(data.toString());
65
82
  })
66
83
 
67
- const stdinListener = (data: any) => {
68
- mPty.write(data.toString());
69
- };
70
-
71
84
  const resizeListener = () => {
72
85
  const { columns, rows } = process.stdout;
73
86
  mPty.resize(columns, rows);
@@ -75,12 +88,9 @@ export class SequentialPty implements IPty {
75
88
 
76
89
  // Listen to resize events for the terminal window;
77
90
  process.stdout.on('resize', resizeListener);
78
- // Listen for user input
79
- process.stdin.on('data', stdinListener);
80
91
 
81
92
  mPty.onExit((result) => {
82
93
  process.stdout.off('resize', resizeListener);
83
- process.stdin.off('data', stdinListener);
84
94
 
85
95
  resolve({
86
96
  status: result.exitCode === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
@@ -99,6 +109,39 @@ export class SequentialPty implements IPty {
99
109
  }
100
110
  }
101
111
 
112
+ // For safety reasons, requests that require sudo or are interactive must be run via the main client
113
+ async externalSpawn(
114
+ cmd: string,
115
+ opts: SpawnOptions
116
+ ): Promise<SpawnResult> {
117
+ return new Promise((resolve) => {
118
+ const requestId = nanoid(8);
119
+
120
+ const listener = (data: IpcMessageV2) => {
121
+ if (data.requestId === requestId) {
122
+ process.removeListener('message', listener);
123
+
124
+ if (!validateSudoRequestResponse(data.data)) {
125
+ throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
126
+ }
127
+
128
+ resolve(data.data as unknown as CommandRequestResponseData);
129
+ }
130
+ }
131
+
132
+ process.on('message', listener);
133
+
134
+ process.send!(<IpcMessageV2>{
135
+ cmd: MessageCmd.COMMAND_REQUEST,
136
+ data: {
137
+ command: cmd,
138
+ options: opts ?? {},
139
+ },
140
+ requestId
141
+ })
142
+ });
143
+ }
144
+
102
145
  private getDefaultShell(): string {
103
146
  return process.env.SHELL!;
104
147
  }
@@ -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
 
@@ -50,12 +52,128 @@ describe('SequentialPty tests', () => {
50
52
  });
51
53
 
52
54
  it('It can launch a command in interactive mode', async () => {
53
- const pty = new SequentialPty();
55
+ const originalSend = process.send;
56
+ process.send = (req: IpcMessageV2) => {
57
+ expect(req).toMatchObject({
58
+ cmd: MessageCmd.COMMAND_REQUEST,
59
+ requestId: expect.any(String),
60
+ data: {
61
+ command: 'ls',
62
+ options: {
63
+ cwd: '/tmp',
64
+ interactive: true,
65
+ }
66
+ }
67
+ })
68
+
69
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
70
+ const listeners = process.listeners('message');
71
+ listeners[2](({
72
+ cmd: MessageCmd.COMMAND_REQUEST,
73
+ requestId: req.requestId,
74
+ status: MessageStatus.SUCCESS,
75
+ data: {
76
+ status: SpawnStatus.SUCCESS,
77
+ exitCode: 0,
78
+ data: 'My data',
79
+ }
80
+ }))
81
+
82
+ return true;
83
+ }
84
+
85
+ const $ = new SequentialPty();
86
+ const resultSuccess = await $.spawnSafe('ls', { interactive: true, cwd: '/tmp' });
54
87
 
55
- const resultSuccess = await pty.spawnSafe('ls', { interactive: false });
56
88
  expect(resultSuccess).toMatchObject({
57
89
  status: 'success',
58
90
  exitCode: 0,
59
- })
91
+ });
92
+
93
+ process.send = originalSend;
60
94
  });
95
+
96
+ it('It can work with root (sudo)', async () => {
97
+ const originalSend = process.send;
98
+ process.send = (req: IpcMessageV2) => {
99
+ expect(req).toMatchObject({
100
+ cmd: MessageCmd.COMMAND_REQUEST,
101
+ requestId: expect.any(String),
102
+ data: {
103
+ command: 'ls',
104
+ options: {
105
+ interactive: true,
106
+ requiresRoot: true,
107
+ }
108
+ }
109
+ })
110
+
111
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
112
+ const listeners = process.listeners('message');
113
+ listeners[2](({
114
+ cmd: MessageCmd.COMMAND_REQUEST,
115
+ requestId: req.requestId,
116
+ status: MessageStatus.SUCCESS,
117
+ data: {
118
+ status: SpawnStatus.SUCCESS,
119
+ exitCode: 0,
120
+ data: 'My data',
121
+ }
122
+ }))
123
+
124
+ return true;
125
+ }
126
+
127
+ const $ = new SequentialPty();
128
+ const resultSuccess = await $.spawn('ls', { interactive: true, requiresRoot: true });
129
+
130
+ expect(resultSuccess).toMatchObject({
131
+ status: 'success',
132
+ exitCode: 0,
133
+ });
134
+
135
+ process.send = originalSend;
136
+ })
137
+
138
+ it('It can handle errors when in sudo', async () => {
139
+ const originalSend = process.send;
140
+ process.send = (req: IpcMessageV2) => {
141
+ expect(req).toMatchObject({
142
+ cmd: MessageCmd.COMMAND_REQUEST,
143
+ requestId: expect.any(String),
144
+ data: {
145
+ command: 'ls',
146
+ options: {
147
+ requiresRoot: true,
148
+ interactive: true,
149
+ }
150
+ }
151
+ })
152
+
153
+ // This may look confusing but what we're doing here is directly finding the process listener and calling it without going through serialization
154
+ const listeners = process.listeners('message');
155
+ listeners[2](({
156
+ cmd: MessageCmd.COMMAND_REQUEST,
157
+ requestId: req.requestId,
158
+ status: MessageStatus.SUCCESS,
159
+ data: {
160
+ status: SpawnStatus.ERROR,
161
+ exitCode: 127,
162
+ data: 'My data',
163
+ }
164
+ }))
165
+
166
+ return true;
167
+ }
168
+
169
+ const $ = new SequentialPty();
170
+ const resultSuccess = await $.spawnSafe('ls', { interactive: true, requiresRoot: true });
171
+
172
+ expect(resultSuccess).toMatchObject({
173
+ status: SpawnStatus.ERROR,
174
+ exitCode: 127,
175
+ });
176
+
177
+ process.send = originalSend;
178
+ })
61
179
  })
@@ -7,7 +7,7 @@ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
7
7
  import { ParameterChange } from '../plan/change-set.js';
8
8
  import { ResourceController } from './resource-controller.js';
9
9
  import { TestConfig, testPlan, TestResource, TestStatefulParameter } from '../utils/test-utils.test.js';
10
- import { tildify, untildify } from '../utils/internal-utils.js';
10
+ import { tildify, untildify } from '../utils/functions.js';
11
11
  import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
12
12
  import { Plan } from '../plan/plan.js';
13
13
  import os from 'node:os';
@@ -10,7 +10,7 @@ import {
10
10
  resolvePathWithVariables,
11
11
  tildify,
12
12
  untildify
13
- } from '../utils/internal-utils.js';
13
+ } from '../utils/functions.js';
14
14
  import { RefreshContext } from './resource.js';
15
15
 
16
16
  export interface InputTransformation {
@@ -0,0 +1,7 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ describe('File utils tests', { timeout: 100_000_000 }, () => {
4
+ it('Can download a file', async () => {
5
+ // await FileUtils.downloadFile('https://download.jetbrains.com/webstorm/WebStorm-2025.3.1-aarch64.dmg?_gl=1*1huoi7o*_gcl_aw*R0NMLjE3NjU3NDAwMTcuQ2p3S0NBaUEzZm5KQmhBZ0Vpd0F5cW1ZNVhLVENlbHJOcTk2YXdjZVlfMS1wdE91MXc0WDk2bFJkVDM3QURhUFNJMUtwNVVSVUhxWTJob0NuZ0FRQXZEX0J3RQ..*_gcl_au*MjA0MDQ0MjE2My4xNzYzNjQzNzMz*FPAU*MjA0MDQ0MjE2My4xNzYzNjQzNzMz*_ga*MTYxMDg4MTkzMi4xNzYzNjQzNzMz*_ga_9J976DJZ68*czE3NjYzNjI5ODAkbzEyJGcxJHQxNzY2MzYzMDQwJGo2MCRsMCRoMA..', path.join(process.cwd(), 'google.html'));
6
+ })
7
+ })
@@ -0,0 +1,231 @@
1
+ import * as fsSync from 'node:fs';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { Readable } from 'node:stream';
5
+ import { finished } from 'node:stream/promises';
6
+
7
+ import { Utils } from './index.js';
8
+
9
+ const SPACE_REGEX = /^\s*$/
10
+
11
+ export class FileUtils {
12
+ static async downloadFile(url: string, destination: string): Promise<void> {
13
+ console.log(`Downloading file from ${url} to ${destination}`);
14
+ const { body } = await fetch(url)
15
+
16
+ const dirname = path.dirname(destination);
17
+ if (!await fs.stat(dirname).then((s) => s.isDirectory()).catch(() => false)) {
18
+ await fs.mkdir(dirname, { recursive: true });
19
+ }
20
+
21
+ const ws = fsSync.createWriteStream(destination)
22
+ // Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that
23
+ await finished(Readable.fromWeb(body as never).pipe(ws));
24
+
25
+ console.log(`Finished downloading to ${destination}`);
26
+ }
27
+
28
+ static async addToShellRc(line: string): Promise<void> {
29
+ const lineToInsert = addLeadingSpacer(
30
+ addTrailingSpacer(line)
31
+ );
32
+
33
+ await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert)
34
+
35
+ function addLeadingSpacer(line: string): string {
36
+ return line.startsWith('\n')
37
+ ? line
38
+ : '\n' + line;
39
+ }
40
+
41
+ function addTrailingSpacer(line: string): string {
42
+ return line.endsWith('\n')
43
+ ? line
44
+ : line + '\n';
45
+ }
46
+ }
47
+
48
+ static async addAllToShellRc(lines: string[]): Promise<void> {
49
+ const formattedLines = '\n' + lines.join('\n') + '\n';
50
+ const shellRc = Utils.getPrimaryShellRc();
51
+
52
+ console.log(`Adding to ${path.basename(shellRc)}:
53
+ ${lines.join('\n')}`)
54
+
55
+ await fs.appendFile(shellRc, formattedLines)
56
+ }
57
+
58
+ /**
59
+ * This method adds a directory path to the shell rc file if it doesn't already exist.
60
+ *
61
+ * @param value - The directory path to add.
62
+ * @param prepend - Whether to prepend the path to the existing PATH variable.
63
+ */
64
+ static async addPathToShellRc(value: string, prepend: boolean): Promise<void> {
65
+ if (await Utils.isDirectoryOnPath(value)) {
66
+ return;
67
+ }
68
+
69
+ const shellRc = Utils.getPrimaryShellRc();
70
+ console.log(`Saving path: ${value} to ${shellRc}`);
71
+
72
+ if (prepend) {
73
+ await fs.appendFile(shellRc, `\nexport PATH=$PATH:${value};`, { encoding: 'utf8' });
74
+ return;
75
+ }
76
+
77
+ await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
78
+ }
79
+
80
+ static async removeFromFile(filePath: string, search: string): Promise<void> {
81
+ const contents = await fs.readFile(filePath, 'utf8');
82
+ const newContents = contents.replaceAll(search, '');
83
+
84
+ await fs.writeFile(filePath, newContents, 'utf8');
85
+ }
86
+
87
+ static async removeLineFromFile(filePath: string, search: RegExp | string): Promise<void> {
88
+ const file = await fs.readFile(filePath, 'utf8')
89
+ const lines = file.split('\n');
90
+
91
+ let searchRegex;
92
+ let searchString;
93
+
94
+ if (typeof search === 'object') {
95
+ const startRegex = /^([\t ]*)?/;
96
+ const endRegex = /([\t ]*)?/;
97
+
98
+ // Augment regex with spaces criteria to make sure this function is not deleting lines that are comments or has other content.
99
+ searchRegex = search
100
+ ? new RegExp(
101
+ startRegex.source + search.source + endRegex.source,
102
+ search.flags
103
+ )
104
+ : search;
105
+ }
106
+
107
+ if (typeof search === 'string') {
108
+ searchString = search;
109
+ }
110
+
111
+ for (let counter = lines.length; counter >= 0; counter--) {
112
+ if (!lines[counter]) {
113
+ continue;
114
+ }
115
+
116
+ if (searchString && lines[counter].includes(searchString)) {
117
+ lines.splice(counter, 1);
118
+ continue;
119
+ }
120
+
121
+ if (searchRegex && lines[counter].search(searchRegex) !== -1) {
122
+ lines.splice(counter, 1);
123
+ }
124
+ }
125
+
126
+ await fs.writeFile(filePath, lines.join('\n'));
127
+ console.log(`Removed line: ${search} from ${filePath}`)
128
+ }
129
+
130
+ static async removeLineFromShellRc(search: RegExp | string): Promise<void> {
131
+ return FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
132
+ }
133
+
134
+ static async removeAllLinesFromShellRc(searches: Array<RegExp | string>): Promise<void> {
135
+ for (const search of searches) {
136
+ await FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
137
+ }
138
+ }
139
+
140
+ // Append the string to the end of a file ensuring at least 1 lines of space between.
141
+ // Ex result:
142
+ // something something;
143
+ //
144
+ // newline;
145
+ static appendToFileWithSpacing(file: string, textToInsert: string): string {
146
+ const lines = file.trimEnd().split(/\n/);
147
+ if (lines.length === 0) {
148
+ return textToInsert;
149
+ }
150
+
151
+ const endingNewLines = FileUtils.calculateEndingNewLines(lines);
152
+ const numNewLines = endingNewLines === -1
153
+ ? 0
154
+ : Math.max(0, 2 - endingNewLines);
155
+ return lines.join('\n') + '\n'.repeat(numNewLines) + textToInsert
156
+ }
157
+
158
+ static async dirExists(path: string): Promise<boolean> {
159
+ let stat;
160
+ try {
161
+ stat = await fs.stat(path);
162
+ return stat.isDirectory();
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ static async fileExists(path: string): Promise<boolean> {
169
+ let stat;
170
+ try {
171
+ stat = await fs.stat(path);
172
+ return stat.isFile();
173
+ } catch {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ static async exists(path: string): Promise<boolean> {
179
+ try {
180
+ await fs.stat(path);
181
+ return true;
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ static async checkDirExistsOrThrowIfFile(path: string): Promise<boolean> {
188
+ let stat;
189
+ try {
190
+ stat = await fs.stat(path);
191
+ } catch {
192
+ return false;
193
+ }
194
+
195
+ if (stat.isDirectory()) {
196
+ return true;
197
+ }
198
+
199
+ throw new Error(`Directory ${path} already exists and is a file`);
200
+ }
201
+
202
+ static async createDirIfNotExists(path: string): Promise<void> {
203
+ if (!fsSync.existsSync(path)) {
204
+ await fs.mkdir(path, { recursive: true });
205
+ }
206
+ }
207
+
208
+ // This is overly complicated but it can be used to insert into any
209
+ // position in the future
210
+ private static calculateEndingNewLines(lines: string[]): number {
211
+ let counter = 0;
212
+ while (true) {
213
+ const line = lines.at(-counter - 1);
214
+
215
+ if (!line) {
216
+ return -1
217
+ }
218
+
219
+ if (!SPACE_REGEX.test(line)) {
220
+ return counter;
221
+ }
222
+
223
+ counter++;
224
+
225
+ // Short circuit here because we don't need to check over 2;
226
+ if (counter > 2) {
227
+ return counter;
228
+ }
229
+ }
230
+ }
231
+ }
@@ -2,22 +2,6 @@ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- export const VerbosityLevel = new class {
6
- level = 0;
7
-
8
- get() {
9
- return this.level;
10
- }
11
-
12
- set(level: number) {
13
- this.level = level;
14
- }
15
- }
16
-
17
- export function isDebug(): boolean {
18
- return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
19
- }
20
-
21
5
  export function splitUserConfig<T extends StringIndexedObject>(
22
6
  config: ResourceConfig & T
23
7
  ): { parameters: T; coreParameters: ResourceConfig } {
@@ -1,6 +1,11 @@
1
1
  import { OS } from 'codify-schemas';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { getPty } from '../pty/index.js';
5
+
6
+ export function isDebug(): boolean {
7
+ return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
8
+ }
4
9
 
5
10
  export enum Shell {
6
11
  ZSH = 'zsh',
@@ -134,6 +139,13 @@ export const Utils = {
134
139
  path.join(homeDir, '.profile'),
135
140
  ];
136
141
  },
142
+
143
+ async isDirectoryOnPath(directory: string): Promise<boolean> {
144
+ const $ = getPty();
145
+ const { data: pathQuery } = await $.spawn('echo $PATH', { interactive: true });
146
+ const lines = pathQuery.split(':');
147
+ return lines.includes(directory);
148
+ },
137
149
  };
138
150
 
139
151
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { addVariablesToPath, resolvePathWithVariables, splitUserConfig } from './internal-utils.js';
2
+ import { addVariablesToPath, resolvePathWithVariables, splitUserConfig } from './functions.js';
3
3
  import os from 'node:os';
4
4
 
5
5
  describe('Utils tests', () => {
@@ -0,0 +1,11 @@
1
+ export const VerbosityLevel = new class {
2
+ level = 0;
3
+
4
+ get() {
5
+ return this.level;
6
+ }
7
+
8
+ set(level: number) {
9
+ this.level = level;
10
+ }
11
+ }